diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..47389f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,42 @@ +name: 🐛 Bug +description: Signaler un bug +title: "[Bug] " +labels: + - bug +body: + - type: textarea + id: description + attributes: + label: Description + description: DĂ©cris le bug rencontrĂ©. + placeholder: Que s'est-il passĂ© ? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Indique les Ă©tapes pour reproduire le bug. + placeholder: | + 1. Aller sur... + 2. Cliquer sur... + 3. ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Comportement attendu + placeholder: Ce qui devrait se produire... + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Comportement actuel + placeholder: Ce qui se produit actuellement... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 0000000..527fc92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,23 @@ +name: ✹ Feature +description: Proposer une nouvelle fonctionnalitĂ© +title: "[Feature] " +labels: + - enhancement +body: + - type: textarea + id: description + attributes: + label: Description + description: DĂ©cris la fonctionnalitĂ© souhaitĂ©e. + placeholder: Que veux-tu ajouter ? + validations: + required: true + + - type: textarea + id: reason + attributes: + label: Besoin + description: Explique pourquoi cette fonctionnalitĂ© est nĂ©cessaire. + placeholder: Pourquoi cette fonctionnalitĂ© est-elle utile ? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/refactor.yaml b/.github/ISSUE_TEMPLATE/refactor.yaml new file mode 100644 index 0000000..2109fb8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.yaml @@ -0,0 +1,23 @@ +name: ♻ Refactor +description: AmĂ©liorer ou restructurer du code +title: "[Refactor] " +labels: + - refactor +body: + - type: textarea + id: description + attributes: + label: Description + description: DĂ©cris ce qui doit ĂȘtre refactorĂ©. + placeholder: Qu'est-ce qui doit ĂȘtre modifiĂ© ? + validations: + required: true + + - type: textarea + id: reason + attributes: + label: Pourquoi ? + description: Explique pourquoi ce refactor est nĂ©cessaire. + placeholder: Pourquoi faut-il effectuer ce changement ? + validations: + required: true diff --git a/backend/server.ts b/backend/server.ts index 1e0cf1d..7e7399e 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -28,7 +28,7 @@ import teamRoutes from './src/routes/team.routes'; import tentRoutes from './src/routes/tent.routes'; import userRoutes from './src/routes/user.routes'; import bannedRoutes from './src/routes/banned.routes'; -import { server_port } from './src/utils/secret'; +import { server_port } from './src/shared/secrets/secrets'; import { initQcmvss } from './src/database/initdb/initQcmvss'; dotenv.config(); diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index b53fded..924f21d 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; import bigInt from 'big-integer'; import { sign, verify } from 'jsonwebtoken'; -import type { EmailOptions } from '../../types/email'; +import type { EmailOptions } from '../types/email'; import { templateResetPassword } from '../email/email.registry'; import { compileTemplate } from '../email/email.renderer'; import * as auth_service from '../services/auth.service'; @@ -10,9 +10,9 @@ import * as email_service from '../services/email.service'; import * as registration_service from '../services/registration.service'; import * as role_service from '../services/role.service'; import * as user_service from '../services/user.service'; -import { Error, Ok, Unauthorized } from '../utils/responses'; -import { email_from, jwtSecret, service_url } from '../utils/secret'; -import { decodeToken } from '../utils/token'; +import { Error, Ok, Unauthorized } from '../shared/http/responses'; +import { email_from, jwtSecret, service_url } from '../shared/secrets/secrets'; +import { decodeToken } from '../shared/utils/token'; import type { AppRequestHandler } from '../types/http'; import type { LoginBody, diff --git a/backend/src/controllers/banned.controller.ts b/backend/src/controllers/banned.controller.ts index e3ffa8d..8d1f827 100644 --- a/backend/src/controllers/banned.controller.ts +++ b/backend/src/controllers/banned.controller.ts @@ -1,5 +1,5 @@ import * as banned_service from '../services/banned.service'; -import { Error, Ok } from '../utils/responses'; +import { Error, Ok } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { BannedBody, BannedIdParams } from '../dto/banned.dto'; diff --git a/backend/src/controllers/bus.controller.ts b/backend/src/controllers/bus.controller.ts index 5d5e2c7..55f2394 100644 --- a/backend/src/controllers/bus.controller.ts +++ b/backend/src/controllers/bus.controller.ts @@ -1,7 +1,7 @@ import * as bus_service from '../services/bus.service'; import { generateEmailHtml, sendEmail } from '../services/email.service'; -import { Error, Ok } from '../utils/responses'; -import { email_from } from '../utils/secret'; +import { Error, Ok } from '../shared/http/responses'; +import { email_from } from '../shared/secrets/secrets'; import type { AppRequestHandler } from '../types/http'; export const sendBusAttributionEmails: AppRequestHandler = async (_req, res) => { diff --git a/backend/src/controllers/challenge.controller.ts b/backend/src/controllers/challenge.controller.ts index c17da21..505a512 100644 --- a/backend/src/controllers/challenge.controller.ts +++ b/backend/src/controllers/challenge.controller.ts @@ -1,5 +1,5 @@ import * as challenge_service from '../services/challenge.service'; -import { Created, Error, Ok, Unauthorized } from '../utils/responses'; +import { Created, Error, Ok, Unauthorized } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { CreateChallengeBody, diff --git a/backend/src/controllers/discord.controller.ts b/backend/src/controllers/discord.controller.ts index 225acaf..6ad9bd1 100644 --- a/backend/src/controllers/discord.controller.ts +++ b/backend/src/controllers/discord.controller.ts @@ -1,5 +1,5 @@ import * as discord_service from '../services/discord.service'; -import { Error, Ok, ServiceUnavailable } from '../utils/responses'; +import { Error, Ok, ServiceUnavailable } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { DiscordBody } from '../dto/discord.dto'; diff --git a/backend/src/controllers/email.controller.ts b/backend/src/controllers/email.controller.ts index 4c7526d..f449470 100644 --- a/backend/src/controllers/email.controller.ts +++ b/backend/src/controllers/email.controller.ts @@ -1,11 +1,11 @@ -import type { EmailOptions } from '../../types/email'; +import type { EmailOptions } from '../types/email'; import { defaultPreviewData } from '../email/email.preview-data'; import { generateEmailHtml, getRecipients, sendEmail } from '../services/email.service'; import * as registration_service from '../services/registration.service'; import * as user_service from '../services/user.service'; -import { Error, Ok } from '../utils/responses'; -import { email_from, service_url } from '../utils/secret'; -import { getLatestUploadedDocument } from '../utils/uploadDocuments'; +import { Error, Ok } from '../shared/http/responses'; +import { email_from, service_url } from '../shared/secrets/secrets'; +import { getLatestUploadedDocument } from '../shared/storage/uploadDocuments'; import type { AppRequestHandler } from '../types/http'; import type { EmailRequestBody } from '../dto/email.dto'; diff --git a/backend/src/controllers/event.controller.ts b/backend/src/controllers/event.controller.ts index fea9b5f..b32b11c 100644 --- a/backend/src/controllers/event.controller.ts +++ b/backend/src/controllers/event.controller.ts @@ -1,7 +1,7 @@ import * as event_service from '../services/event.service'; import * as team_service from '../services/team.service'; -import { Conflict, Error, Ok, Teapot, Unauthorized } from '../utils/responses'; -import { shotgun_password } from '../utils/secret'; +import { Conflict, Error, Ok, Teapot, Unauthorized } from '../shared/http/responses'; +import { shotgun_password } from '../shared/secrets/secrets'; import type { AppRequestHandler } from '../types/http'; import type { ShotgunBody, ToggleStatusBody } from '../dto/event.dto'; diff --git a/backend/src/controllers/faction.controller.ts b/backend/src/controllers/faction.controller.ts index 811d0cb..03c4520 100644 --- a/backend/src/controllers/faction.controller.ts +++ b/backend/src/controllers/faction.controller.ts @@ -1,5 +1,5 @@ import * as faction_service from '../services/faction.service'; -import { Error, Ok } from '../utils/responses'; +import { Error, Ok } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { FactionQuery, FactionBody } from '../dto/faction.dto'; diff --git a/backend/src/controllers/im_export.controller.ts b/backend/src/controllers/im_export.controller.ts index ba1df4d..979e6d1 100644 --- a/backend/src/controllers/im_export.controller.ts +++ b/backend/src/controllers/im_export.controller.ts @@ -5,14 +5,14 @@ import * as export_service from '../services/im_export.service'; import * as permanence_service from '../services/permanence.service'; import * as team_service from '../services/team.service'; import * as user_service from '../services/user.service'; -import { Error, Ok } from '../utils/responses'; -import { spreadsheet_id } from '../utils/secret'; +import { Error, Ok } from '../shared/http/responses'; +import { spreadsheet_id } from '../shared/secrets/secrets'; import { getLatestUploadedDocument, isSafeUploadSegment, removeUploadedDocuments, toUploadedDocumentStatus, -} from '../utils/uploadDocuments'; +} from '../shared/storage/uploadDocuments'; import type { AppRequestHandler } from '../types/http'; import type { UploadedDocumentParams } from '../dto/im_export.dto'; diff --git a/backend/src/controllers/news.controller.ts b/backend/src/controllers/news.controller.ts index 7756d1f..3ff6ae7 100644 --- a/backend/src/controllers/news.controller.ts +++ b/backend/src/controllers/news.controller.ts @@ -4,8 +4,8 @@ import * as email_service from '../services/email.service'; import { generateEmailHtml } from '../services/email.service'; import * as news_service from '../services/news.service'; import * as user_service from '../services/user.service'; -import { Error, Ok } from '../utils/responses'; -import { email_from } from '../utils/secret'; +import { Error, Ok } from '../shared/http/responses'; +import { email_from } from '../shared/secrets/secrets'; import type { AppRequestHandler } from '../types/http'; import type { NewsBody, NewsQuery } from '../dto/news.dto'; diff --git a/backend/src/controllers/permanence.controller.ts b/backend/src/controllers/permanence.controller.ts index 5220048..92ca549 100644 --- a/backend/src/controllers/permanence.controller.ts +++ b/backend/src/controllers/permanence.controller.ts @@ -1,5 +1,5 @@ import * as permanence_service from '../services/permanence.service'; -import { Error, Ok } from '../utils/responses'; +import { Error, Ok } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { PermanenceBody, PermQuery } from '../dto/permanence.dto'; diff --git a/backend/src/controllers/role.controller.ts b/backend/src/controllers/role.controller.ts index bf946aa..3f788f9 100644 --- a/backend/src/controllers/role.controller.ts +++ b/backend/src/controllers/role.controller.ts @@ -1,5 +1,5 @@ import * as role_service from '../services/role.service'; -import { Error, Ok } from '../utils/responses'; +import { Error, Ok } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { PermissionBody, diff --git a/backend/src/controllers/team.controller.ts b/backend/src/controllers/team.controller.ts index 302b67f..5377d9a 100644 --- a/backend/src/controllers/team.controller.ts +++ b/backend/src/controllers/team.controller.ts @@ -2,7 +2,7 @@ import { type Event } from '../schemas/Basic/event.schema'; import * as event_service from '../services/event.service'; import * as faction_service from '../services/faction.service'; import * as team_service from '../services/team.service'; -import { Error, Ok } from '../utils/responses'; +import { Error, Ok } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; import type { CreateTeamBody, CreateTeamLightBody, ModifyTeamBody, TeamQuery } from '../dto/team.dto'; diff --git a/backend/src/controllers/tent.controller.ts b/backend/src/controllers/tent.controller.ts index 9c68870..410345d 100644 --- a/backend/src/controllers/tent.controller.ts +++ b/backend/src/controllers/tent.controller.ts @@ -1,8 +1,8 @@ import { generateEmailHtml, sendEmail } from '../services/email.service'; import * as tent_service from '../services/tent.service'; import { getUserById } from '../services/user.service'; -import { Error, Ok } from '../utils/responses'; -import { email_from } from '../utils/secret'; +import { Error, Ok } from '../shared/http/responses'; +import { email_from } from '../shared/secrets/secrets'; import type { AppRequestHandler } from '../types/http'; import type { CreateTentBody, ToggleTentBody } from '../dto/tent.dto'; @@ -89,10 +89,8 @@ export const toggleTentConfirmation: AppRequestHandler = async ( const emailOptions = { from: email_from, to: [user1.email, user2.email], - subject: confirmed - ? "🎉 Votre tente a Ă©tĂ© validĂ©e !" - : "â›ș Votre tente a Ă©tĂ© invalidĂ©e", - text: "", // optionnel + subject: confirmed ? '🎉 Votre tente a Ă©tĂ© validĂ©e !' : 'â›ș Votre tente a Ă©tĂ© invalidĂ©e', + text: '', // optionnel html: htmlEmail, }; @@ -100,9 +98,7 @@ export const toggleTentConfirmation: AppRequestHandler = async ( await sendEmail(emailOptions); Ok(res, { - msg: confirmed - ? "Tente validĂ©e et email envoyĂ©." - : "Tente invalidĂ©e et email envoyĂ©.", + msg: confirmed ? 'Tente validĂ©e et email envoyĂ©.' : 'Tente invalidĂ©e et email envoyĂ©.', }); } catch (err: any) { console.error(err); diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index f10ef74..b983c1e 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -1,14 +1,14 @@ import type { - AdminCreateUserDto, - CreateUserContactInformationDto, + CreateUserContactInformationBody, PermissionParams, ProfileBody, SyncBody, UserIdParams, VssSubmissionPayload, + AdminCreateUserBody, } from '../dto/user.dto'; import * as user_service from '../services/user.service'; -import { Error, Ok } from '../utils/responses'; +import { Error, Ok } from '../shared/http/responses'; import type { AppRequestHandler } from '../types/http'; export const getUsersAdmin: AppRequestHandler = async (_req, res) => { @@ -85,7 +85,7 @@ export const getUserContactInformation: AppRequestHandler = async (req, res) => } }; -export const createUserContactInformation: AppRequestHandler = async (req, res) => { +export const createUserContactInformation: AppRequestHandler = async (req, res) => { const userId = req.user?.userId; const contact = req.body; @@ -153,7 +153,7 @@ export const adminUpdateUser: AppRequestHandler } }; -export const adminCreateUser: AppRequestHandler = async (req, res) => { +export const adminCreateUser: AppRequestHandler = async (req, res) => { try { const user = await user_service.adminCreateUser(req.body); diff --git a/backend/src/database/db.ts b/backend/src/database/db.ts index 75c5005..af9e389 100644 --- a/backend/src/database/db.ts +++ b/backend/src/database/db.ts @@ -1,6 +1,6 @@ -import { drizzle } from "drizzle-orm/node-postgres"; -import { Client } from "pg"; -import { postgres_db, postgres_host, postgres_password, postgres_port, postgres_user } from '../utils/secret'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; +import { postgres_db, postgres_host, postgres_password, postgres_port, postgres_user } from '../shared/secrets/secrets'; // ✅ Import de tous tes schĂ©mas ici import * as challenge from '../schemas/Basic/challenge.schema'; @@ -11,16 +11,16 @@ import * as permanence from '../schemas/Basic/permanence.schema'; import * as role from '../schemas/Basic/role.schema'; import * as team from '../schemas/Basic/team.schema'; import * as user from '../schemas/Basic/user.schema'; -import * as busattribution from "../schemas/Relational/busattribution.schema"; +import * as busattribution from '../schemas/Relational/busattribution.schema'; import * as challengValidation from '../schemas/Relational/challengevalidation.schema'; -import * as registration from "../schemas/Relational/registration.schema"; -import * as rolepoints from "../schemas/Relational/rolepoints.schema"; +import * as registration from '../schemas/Relational/registration.schema'; +import * as rolepoints from '../schemas/Relational/rolepoints.schema'; import * as teamFaction from '../schemas/Relational/teamfaction.schema'; import * as teamShotgun from '../schemas/Relational/teamshotgun.schema'; import * as userPermanence from '../schemas/Relational/userpermanences.schema'; import * as userRole from '../schemas/Relational/userroles.schema'; import * as userTeam from '../schemas/Relational/userteams.schema'; -import * as tent from "../schemas/Relational/usertent.schema"; +import * as tent from '../schemas/Relational/usertent.schema'; const schema = { ...user, @@ -40,7 +40,7 @@ const schema = { ...busattribution, ...registration, ...tent, - ...rolepoints + ...rolepoints, }; const client = new Client({ diff --git a/backend/src/database/initdb/initQcmvss.ts b/backend/src/database/initdb/initQcmvss.ts index 7137dcc..798683b 100644 --- a/backend/src/database/initdb/initQcmvss.ts +++ b/backend/src/database/initdb/initQcmvss.ts @@ -1,13 +1,16 @@ +import { and, eq, sql } from 'drizzle-orm'; import { db } from '../db'; import { vssqcmanswerSchema } from '../../schemas/Relational/vssqcmanswer.schema'; import { vssqcmquestionSchema } from '../../schemas/Basic/vssqcmquestion.schema'; type SeedQuestion = { question: string; + questionEn: string; points: number; type: 'single_choice' | 'multiple_choice'; answers: { answer: string; + answerEn: string; is_correct: boolean; }[]; }; @@ -15,177 +18,344 @@ type SeedQuestion = { const qcmQuestions: SeedQuestion[] = [ { question: 'Oui = ', + questionEn: 'Yes = ', points: 1, type: 'single_choice', answers: [ - { answer: 'Non', is_correct: false }, - { answer: 'Toujours oui', is_correct: false }, - { answer: 'Peut-ĂȘtre non plus tard', is_correct: true }, + { answer: 'Non', answerEn: 'No', is_correct: false }, + { answer: 'Toujours oui', answerEn: 'Always yes', is_correct: false }, + { answer: 'Peut-ĂȘtre non plus tard', answerEn: 'Maybe no later', is_correct: true }, ], }, { question: 'Non = ', + questionEn: 'No = ', points: 1, type: 'single_choice', answers: [ - { answer: 'Oui', is_correct: false }, - { answer: 'Non', is_correct: true }, - { answer: "Peut-ĂȘtre oui si j'insiste", is_correct: false }, + { answer: 'Oui', answerEn: 'Yes', is_correct: false }, + { answer: 'Non', answerEn: 'No', is_correct: true }, + { answer: "Peut-ĂȘtre oui si j'insiste", answerEn: 'Maybe yes if I keep insisting', is_correct: false }, ], }, { question: 'En rĂ©sumĂ©, le consentement', + questionEn: 'In short, consent', points: 2, type: 'multiple_choice', answers: [ - { answer: 'concerne une action prĂ©cise', is_correct: true }, - { answer: "ne peut-ĂȘtre considĂ©rĂ© comme Ă©clairĂ© venant d'un personne en Ă©tat d'Ă©briĂ©tĂ©", is_correct: true }, - { answer: 'doit ĂȘtre libre et Ă©clairĂ©', is_correct: true }, - { answer: 'peut ĂȘtre retirĂ© Ă  tout moment', is_correct: true }, + { answer: 'concerne une action prĂ©cise', answerEn: 'concerns a specific act', is_correct: true }, + { + answer: "ne peut-ĂȘtre considĂ©rĂ© comme Ă©clairĂ© venant d'un personne en Ă©tat d'Ă©briĂ©tĂ©", + answerEn: 'cannot be considered informed when coming from an intoxicated person', + is_correct: true, + }, + { answer: 'doit ĂȘtre libre et Ă©clairĂ©', answerEn: 'must be free and informed', is_correct: true }, + { answer: 'peut ĂȘtre retirĂ© Ă  tout moment', answerEn: 'can be withdrawn at any time', is_correct: true }, { answer: "spĂ©cifique, enthousiaste; valable quand la personne chancĂšle sous l'effet de l'alcool", + answerEn: 'specific, enthusiastic; valid when the person is staggering from alcohol', + is_correct: false, + }, + { answer: "peut s'obtenir en insistant", answerEn: 'can be obtained by insisting', is_correct: false }, + { + answer: 'est valable quand la personne est bourrĂ©e', + answerEn: 'is valid when the person is drunk', is_correct: false, }, - { answer: "peut s'obtenir en insistant", is_correct: false }, - { answer: 'est valable quand la personne est bourrĂ©e', is_correct: false }, ], }, { question: "B a embrassĂ© A de force. B Ă©tait complĂštement bourrĂ©. Il s'agit d'une agression sexuelle. La prise d'alcool est alors une condition :", + questionEn: + 'B forced a kiss on A. B was completely drunk. It is a sexual assault. Alcohol consumption is then a:', points: 1, type: 'single_choice', answers: [ - { answer: 'Aggravante', is_correct: true }, - { answer: 'AttĂ©nuante', is_correct: false }, + { answer: 'Aggravante', answerEn: 'Aggravating factor', is_correct: true }, + { answer: 'AttĂ©nuante', answerEn: 'Mitigating factor', is_correct: false }, ], }, { question: 'Parmi les situations suivantes, lesquelles sont des agressions sexuelles :', + questionEn: 'Which of the following situations are sexual assaults:', points: 1, type: 'multiple_choice', answers: [ - { answer: "Se frotter Ă  quelqu'un‱e", is_correct: true }, - { answer: 'Caresser les fesses de son‱sa partenaire endormi‱e', is_correct: true }, - { answer: "Embrasser quelqu'un‱e de force", is_correct: true }, - { answer: "Embrasser par surprise quelqu'un‱e qui danse au milieu de la foule", is_correct: true }, - { answer: "Embrasser quelqu'un‱e tant alcoolisé‹e qu'iel vient de vomir", is_correct: true }, + { answer: "Se frotter Ă  quelqu'un‱e", answerEn: 'Rubbing against someone', is_correct: true }, + { + answer: 'Caresser les fesses de son‱sa partenaire endormi‱e', + answerEn: 'Caressing your sleeping partner’s buttocks', + is_correct: true, + }, + { answer: "Embrasser quelqu'un‱e de force", answerEn: 'Kissing someone by force', is_correct: true }, + { + answer: "Embrasser par surprise quelqu'un‱e qui danse au milieu de la foule", + answerEn: 'Kissing someone by surprise who is dancing in the middle of the crowd', + is_correct: true, + }, + { + answer: "Embrasser quelqu'un‱e tant alcoolisé‹e qu'iel vient de vomir", + answerEn: 'Kissing someone so drunk that they have just vomited', + is_correct: true, + }, ], }, { question: "Un.e de tes amis touche les fesses de B et l'enlace. B a un mouvement de recul. Que peux-tu faire ?", + questionEn: 'One of your friends touches B’s buttocks and hugs them. B steps back. What can you do?', points: 1, type: 'multiple_choice', answers: [ - { answer: "Rien de particulier. B ne s'en souviendra sĂ»rement pas.", is_correct: false }, - { answer: 'Demander Ă  B si elle‱il va bien', is_correct: true }, + { + answer: "Rien de particulier. B ne s'en souviendra sĂ»rement pas.", + answerEn: 'Nothing special. B probably will not remember it anyway.', + is_correct: false, + }, + { answer: 'Demander Ă  B si elle‱il va bien', answerEn: 'Ask B if they are okay', is_correct: true }, { answer: "Prendre cet‱te ami‱e Ă  part et lui faire comprendre qu'il‱elle a mal agi, que B n'avait pas envie d'ĂȘtre touché‹e.", + answerEn: + 'Take that friend aside and make them understand they acted badly, that B did not want to be touched.', + is_correct: true, + }, + { answer: 'Eloigner ton ami‱e de B', answerEn: 'Move your friend away from B', is_correct: true }, + { + answer: "Le signaler Ă  un tiers si tu penses que B peut avoir besoin d'aide", + answerEn: 'Report it to a third party if you think B may need help', is_correct: true, }, - { answer: 'Eloigner ton ami‱e de B', is_correct: true }, - { answer: "Le signaler Ă  un tiers si tu penses que B peut avoir besoin d'aide", is_correct: true }, ], }, { question: "A qui et oĂč peux-tu demander de l'aide si tu en as besoin ?", + questionEn: 'Who can you ask for help, and where, if you need it?', points: 1, type: 'multiple_choice', answers: [ - { answer: 'Dans une zone dĂ©diĂ©e lors des soirĂ©es, appelĂ©e la Safe Zone', is_correct: true }, - { answer: 'Au stand de prĂ©vention', is_correct: true }, - { answer: 'A la team prĂ©vention', is_correct: true }, - { answer: "Aux organisateurs de l'intĂ©gration (en t-shirt touge)", is_correct: true }, - { answer: "A tes chefs d'Ă©quipe", is_correct: true }, - { answer: 'A ta marraine UTTienne/ A ton parrain UTTien', is_correct: true }, - { answer: 'À un‱e ami‱e', is_correct: true }, + { + answer: 'Dans une zone dĂ©diĂ©e lors des soirĂ©es, appelĂ©e la Safe Zone', + answerEn: 'In a dedicated area during parties, called the Safe Zone', + is_correct: true, + }, + { answer: 'Au stand de prĂ©vention', answerEn: 'At the prevention booth', is_correct: true }, + { answer: 'A la team prĂ©vention', answerEn: 'To the prevention team', is_correct: true }, + { + answer: "Aux organisateurs de l'intĂ©gration (en t-shirt touge)", + answerEn: 'To the integration organizers (wearing red T-shirts)', + is_correct: true, + }, + { answer: "A tes chefs d'Ă©quipe", answerEn: 'To your team leaders', is_correct: true }, + { + answer: 'A ta marraine UTTienne/ A ton parrain UTTien', + answerEn: 'To your UTT mentor / buddy', + is_correct: true, + }, + { answer: 'À un‱e ami‱e', answerEn: 'To a friend', is_correct: true }, ], }, { question: 'En cas de VSS, quelles sont les peines maximales lĂ©galement encourue par une personne ayant commis une agression sexuelle ?', + questionEn: + 'In a VSS case, what is the maximum legally punishable sentence for a person who committed sexual assault?', points: 1, type: 'single_choice', answers: [ - { answer: "75 000 € d'amende et 5 ans d'emprisonnement", is_correct: true }, - { answer: "10 000€ d'amende", is_correct: false }, - { answer: '15 ans de prison', is_correct: false }, + { + answer: "75 000 € d'amende et 5 ans d'emprisonnement", + answerEn: 'A €75,000 fine and 5 years in prison', + is_correct: true, + }, + { answer: "10 000€ d'amende", answerEn: 'A €10,000 fine', is_correct: false }, + { answer: '15 ans de prison', answerEn: '15 years in prison', is_correct: false }, ], }, { question: 'Quelles sont les consĂ©quences possibles pour la victime de VSS ?', + questionEn: 'What are the possible consequences for a VSS victim?', points: 1, type: 'multiple_choice', answers: [ - { answer: 'Aucun effet particulier', is_correct: false }, - { answer: 'ProblĂšmes somatiques (nausĂ©es, migraines, fatigue)', is_correct: true }, - { answer: 'Dysfonction sexuelle', is_correct: true }, - { answer: "Crainte de l'intimitĂ©", is_correct: true }, - { answer: 'DĂ©pression majeure', is_correct: true }, - { answer: 'DĂ©tresse psychologique', is_correct: true }, + { answer: 'Aucun effet particulier', answerEn: 'No particular effect', is_correct: false }, + { + answer: 'ProblĂšmes somatiques (nausĂ©es, migraines, fatigue)', + answerEn: 'Somatic problems (nausea, migraines, fatigue)', + is_correct: true, + }, + { answer: 'Dysfonction sexuelle', answerEn: 'Sexual dysfunction', is_correct: true }, + { answer: "Crainte de l'intimitĂ©", answerEn: 'Fear of intimacy', is_correct: true }, + { answer: 'DĂ©pression majeure', answerEn: 'Major depression', is_correct: true }, + { answer: 'DĂ©tresse psychologique', answerEn: 'Psychological distress', is_correct: true }, ], }, { question: 'Que puis-je faire si je suis tĂ©moins de VSS ?', + questionEn: 'What can I do if I witness VSS?', points: 1, type: 'multiple_choice', answers: [ - { answer: "Dire Ă  la victime de faire attention Ă  elle et de mieux s'habiller", is_correct: false }, - { answer: 'Aller voir la team prĂ©vention ou les super-orgas', is_correct: true }, - { answer: 'Appeler France Victime (01 80 52 33 86)', is_correct: true }, - { answer: "Appeler le numĂ©ro d'astreinte", is_correct: true }, + { + answer: "Dire Ă  la victime de faire attention Ă  elle et de mieux s'habiller", + answerEn: 'Tell the victim to be careful and dress better', + is_correct: false, + }, + { + answer: 'Aller voir la team prĂ©vention ou les super-orgas', + answerEn: 'Go see the prevention team or the super-organizers', + is_correct: true, + }, + { + answer: 'Appeler France Victime (01 80 52 33 86)', + answerEn: 'Call France Victime (01 80 52 33 86)', + is_correct: true, + }, + { answer: "Appeler le numĂ©ro d'astreinte", answerEn: 'Call the on-call number', is_correct: true }, ], }, { question: "A quelle sentence s'expose une personne commettant un viol ? \n Article 222-23 Version en vigueur depuis le 23 avril 2021 \n Tout acte de pĂ©nĂ©tration sexuelle, de quelque nature qu'il soit, ou tout acte bucco-gĂ©nital commis sur la personne d'autrui ou sur la personne de l'auteur par violence, contrainte, menace ou surprise est un viol. \n Le viol est puni de quinze ans de rĂ©clusion criminelle.", + questionEn: + 'What sentence does a person committing rape face?\n Article 222-23 Valid since April 23, 2021 \n Any act of sexual penetration, of whatever nature, or any oral-genital act committed on another person or on the perpetrator by violence, coercion, threat, or surprise is rape. \n Rape is punishable by fifteen years of criminal imprisonment.', points: 1, type: 'multiple_choice', answers: [ - { answer: '15 ans de rĂ©clusion criminelle', is_correct: true }, - { answer: "100 000€ d'amence et 20 ans de rĂ©clusion criminelle", is_correct: false }, - { answer: "100 000€ d'amence et 10 ans de rĂ©clusion criminelle", is_correct: false }, + { + answer: '15 ans de rĂ©clusion criminelle', + answerEn: '15 years of criminal imprisonment', + is_correct: true, + }, + { + answer: "100 000€ d'amence et 20 ans de rĂ©clusion criminelle", + answerEn: 'A €100,000 fine and 20 years of criminal imprisonment', + is_correct: false, + }, + { + answer: "100 000€ d'amence et 10 ans de rĂ©clusion criminelle", + answerEn: 'A €100,000 fine and 10 years of criminal imprisonment', + is_correct: false, + }, ], }, { question: "Qu'est-ce qui est considĂ©rĂ© comme un acte de bizutage (et qui est donc interdit) ? ", + questionEn: 'What is considered hazing (and is therefore prohibited)? ', points: 1, type: 'multiple_choice', answers: [ - { answer: "Se dĂ©nuder ou inciter quelqu'un Ă  se dĂ©nuder (Limousin, MarĂ©chal...)", is_correct: true }, - { answer: "Obliger quelqu'un Ă  boire de l'alcool de force lors d'une soirĂ©e", is_correct: true }, + { + answer: "Se dĂ©nuder ou inciter quelqu'un Ă  se dĂ©nuder (Limousin, MarĂ©chal...)", + answerEn: 'Undressing or forcing someone to undress (Limousin, MarĂ©chal...)', + is_correct: true, + }, + { + answer: "Obliger quelqu'un Ă  boire de l'alcool de force lors d'une soirĂ©e", + answerEn: 'Forcing someone to drink alcohol at a party', + is_correct: true, + }, { answer: 'Organiser une chasse au trĂ©sor gĂ©ante Ă  travers toute la ville pour les nouveaux', + answerEn: 'Organizing a giant treasure hunt across the whole city for the newcomers', + is_correct: false, + }, + { + answer: 'Humilier publiquement un nouveau devant le groupe', + answerEn: 'Publicly humiliating a newcomer in front of the group', + is_correct: true, + }, + { + answer: 'Forcer une personne Ă  effectuer des tĂąches dĂ©gradantes ou dangereuses', + answerEn: 'Forcing a person to perform degrading or dangerous tasks', + is_correct: true, + }, + { + answer: "DĂ©fier un nouveau Ă  rĂ©citer l'annuaire tĂ©lĂ©phonique en dansant la macarena", + answerEn: 'Challenging a newcomer to recite the phone book while dancing the Macarena', + is_correct: false, + }, + { + answer: 'Motiver les nouveaux Ă  se dĂ©guiser en canard', + answerEn: 'Encouraging newcomers to dress up as a duck', is_correct: false, }, - { answer: 'Humilier publiquement un nouveau devant le groupe', is_correct: true }, - { answer: 'Forcer une personne Ă  effectuer des tĂąches dĂ©gradantes ou dangereuses', is_correct: true }, - { answer: "DĂ©fier un nouveau Ă  rĂ©citer l'annuaire tĂ©lĂ©phonique en dansant la macarena", is_correct: false }, - { answer: 'Motiver les nouveaux Ă  se dĂ©guiser en canard', is_correct: false }, - { answer: 'Forcer les nouveaux Ă  porter un dĂ©guisement obscĂšne', is_correct: true }, + { + answer: 'Forcer les nouveaux Ă  porter un dĂ©guisement obscĂšne', + answerEn: 'Forcing newcomers to wear an obscene costume', + is_correct: true, + }, ], }, { question: "A quelles sanctions s'expose l'auteur du bizutage ?", + questionEn: 'What penalties does the person responsible for hazing face?', points: 1, type: 'multiple_choice', answers: [ - { answer: "Une exclusion de l'intĂ©gration", is_correct: true }, - { answer: 'Rien du tout', is_correct: false }, + { + answer: "Une exclusion de l'intĂ©gration", + answerEn: 'An exclusion from the integration', + is_correct: true, + }, + { answer: 'Rien du tout', answerEn: 'Nothing at all', is_correct: false }, { answer: "Le bizutage est un dĂ©lit. Il est puni de 6 mois d'emprisonnement et de 7 500 € d'amende.", + answerEn: 'Hazing is a criminal offense. It is punishable by 6 months in prison and a €7,500 fine.', + is_correct: true, + }, + { + answer: 'Si la victime est une personne vulnĂ©rable, les peines sont doublĂ©es', + answerEn: 'If the victim is a vulnerable person, the penalties are doubled', is_correct: true, }, - { answer: 'Si la victime est une personne vulnĂ©rable, les peines sont doublĂ©es', is_correct: true }, - { answer: 'Une mauvaise note', is_correct: false }, + { answer: 'Une mauvaise note', answerEn: 'A bad grade', is_correct: false }, ], }, ]; +const ensureTranslationColumns = async () => { + await db.execute(sql`ALTER TABLE "vssqcmquestion" ADD COLUMN IF NOT EXISTS "question_en" text;`); + await db.execute(sql`ALTER TABLE "vssqcmanswer" ADD COLUMN IF NOT EXISTS "answer_en" text;`); +}; + +const updateQuestionTranslations = async () => { + for (const seedQuestion of qcmQuestions) { + await db + .update(vssqcmquestionSchema) + .set({ question_en: seedQuestion.questionEn }) + .where(eq(vssqcmquestionSchema.question, seedQuestion.question)); + + const [question] = await db + .select({ id: vssqcmquestionSchema.id }) + .from(vssqcmquestionSchema) + .where(eq(vssqcmquestionSchema.question, seedQuestion.question)); + + if (!question) { + continue; + } + + for (const seedAnswer of seedQuestion.answers) { + await db + .update(vssqcmanswerSchema) + .set({ answer_en: seedAnswer.answerEn }) + .where( + and( + eq(vssqcmanswerSchema.questionid, question.id), + eq(vssqcmanswerSchema.answer, seedAnswer.answer), + ), + ); + } + } +}; + export const initQcmvss = async () => { + await ensureTranslationColumns(); + const existingQuestion = await db.select().from(vssqcmquestionSchema).limit(1); if (existingQuestion.length > 0) { + await updateQuestionTranslations(); return; } @@ -194,6 +364,7 @@ export const initQcmvss = async () => { .insert(vssqcmquestionSchema) .values({ question: seedQuestion.question, + question_en: seedQuestion.questionEn, points: seedQuestion.points, type: seedQuestion.type, }) @@ -207,6 +378,7 @@ export const initQcmvss = async () => { seedQuestion.answers.map((seedAnswer) => ({ questionid: createdQuestion.id, answer: seedAnswer.answer, + answer_en: seedAnswer.answerEn, is_correct: seedAnswer.is_correct, })), ); diff --git a/backend/src/database/initdb/initUser.ts b/backend/src/database/initdb/initUser.ts index 48f4fb8..7131f5c 100644 --- a/backend/src/database/initdb/initUser.ts +++ b/backend/src/database/initdb/initUser.ts @@ -1,7 +1,7 @@ -import { userSchema } from "../../schemas/Basic/user.schema"; -import { hashPassword } from "../../services/auth.service"; -import { email_password } from "../../utils/secret"; -import { db } from "../db"; // Assurez-vous que votre instance db est correcte +import { userSchema } from '../../schemas/Basic/user.schema'; +import { hashPassword } from '../../services/auth.service'; +import { email_password } from '../../shared/secrets/secrets'; +import { db } from '../db'; // Assurez-vous que votre instance db est correcte export const initUser = async () => { const existingUser = await db.select().from(userSchema).limit(1); @@ -11,9 +11,9 @@ export const initUser = async () => { // Si il n'y a pas de ligne existante, insĂ©rer une nouvelle ligne if (existingUser.length === 0) { await db.insert(userSchema).values({ - first_name: "Integration UTT", - last_name: "Integration UTT", - email: "integration@utt.fr", + first_name: 'Integration UTT', + last_name: 'Integration UTT', + email: 'integration@utt.fr', majeur: true, password: hashedPassword, permission: 'Admin', diff --git a/backend/src/database/migrations/0027_faulty_hellion.sql b/backend/src/database/migrations/0027_faulty_hellion.sql new file mode 100644 index 0000000..9cfca1d --- /dev/null +++ b/backend/src/database/migrations/0027_faulty_hellion.sql @@ -0,0 +1,2 @@ +ALTER TABLE "vssqcmquestion" ADD COLUMN "question_en" text;--> statement-breakpoint +ALTER TABLE "vssqcmanswer" ADD COLUMN "answer_en" text; \ No newline at end of file diff --git a/backend/src/database/migrations/meta/0027_snapshot.json b/backend/src/database/migrations/meta/0027_snapshot.json new file mode 100644 index 0000000..08603d4 --- /dev/null +++ b/backend/src/database/migrations/meta/0027_snapshot.json @@ -0,0 +1,1509 @@ +{ + "id": "6179fb88-23bd-4656-9980-faa52a9d9b24", + "prevId": "af2f7f68-b5fb-4945-87a6-dcf6b539c002", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.banned_addresses": { + "name": "banned_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "banned_addresses_email_unique": { + "name": "banned_addresses_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenges": { + "name": "challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "challenges_created_by_users_id_fk": { + "name": "challenges_created_by_users_id_fk", + "tableFrom": "challenges", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pre_registration_open": { + "name": "pre_registration_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shotgun_open": { + "name": "shotgun_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sdi_open": { + "name": "sdi_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wei_open": { + "name": "wei_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "food_open": { + "name": "food_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "chall_open": { + "name": "chall_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.factions": { + "name": "factions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "factions_name_unique": { + "name": "factions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.news": { + "name": "news", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permanences": { + "name": "permanences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socialLink": { + "name": "socialLink", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "riCompatible": { + "name": "riCompatible", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_name_unique": { + "name": "teams_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "majeur": { + "name": "majeur", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "male": { + "name": "male", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Nouveau'" + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "vss_form": { + "name": "vss_form", + "type": "vss_form", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vssqcmquestion": { + "name": "vssqcmquestion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "question_en": { + "name": "question_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "question_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bus_attribution": { + "name": "bus_attribution", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bus": { + "name": "bus", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "departure_time": { + "name": "departure_time", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bus_attribution_user_id_users_id_fk": { + "name": "bus_attribution_user_id_users_id_fk", + "tableFrom": "bus_attribution", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenge_validation": { + "name": "challenge_validation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "challenge_id": { + "name": "challenge_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validated_by_admin_id": { + "name": "validated_by_admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validated_at": { + "name": "validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "target_user_id": { + "name": "target_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_team_id": { + "name": "target_team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_faction_id": { + "name": "target_faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_by_admin_id": { + "name": "added_by_admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "challenge_validation_challenge_id_challenges_id_fk": { + "name": "challenge_validation_challenge_id_challenges_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "challenges", + "columnsFrom": [ + "challenge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_validated_by_admin_id_users_id_fk": { + "name": "challenge_validation_validated_by_admin_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "validated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_user_id_users_id_fk": { + "name": "challenge_validation_target_user_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_team_id_teams_id_fk": { + "name": "challenge_validation_target_team_id_teams_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "teams", + "columnsFrom": [ + "target_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_faction_id_factions_id_fk": { + "name": "challenge_validation_target_faction_id_factions_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "factions", + "columnsFrom": [ + "target_faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_added_by_admin_id_users_id_fk": { + "name": "challenge_validation_added_by_admin_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "added_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_tokens": { + "name": "registration_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_tokens_user_id_users_id_fk": { + "name": "registration_tokens_user_id_users_id_fk", + "tableFrom": "registration_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registration_tokens_token_unique": { + "name": "registration_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_points": { + "name": "role_points", + "schema": "", + "columns": { + "role_points": { + "name": "role_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_points_role_points_roles_id_fk": { + "name": "role_points_role_points_roles_id_fk", + "tableFrom": "role_points", + "tableTo": "roles", + "columnsFrom": [ + "role_points" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_points_role_points_pk": { + "name": "role_points_role_points_pk", + "columns": [ + "role_points" + ] + } + }, + "uniqueConstraints": { + "role_points_role_points_unique": { + "name": "role_points_role_points_unique", + "nullsNotDistinct": false, + "columns": [ + "role_points" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_faction": { + "name": "team_faction", + "schema": "", + "columns": { + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_faction_faction_id_factions_id_fk": { + "name": "team_faction_faction_id_factions_id_fk", + "tableFrom": "team_faction", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_faction_team_id_teams_id_fk": { + "name": "team_faction_team_id_teams_id_fk", + "tableFrom": "team_faction", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_faction_faction_id_team_id_pk": { + "name": "team_faction_faction_id_team_id_pk", + "columns": [ + "faction_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_shotgun": { + "name": "team_shotgun", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_shotgun_team_id_teams_id_fk": { + "name": "team_shotgun_team_id_teams_id_fk", + "tableFrom": "team_shotgun", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_informations": { + "name": "user_informations", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "emergency_contact_name": { + "name": "emergency_contact_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emergency_contact_phone": { + "name": "emergency_contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_informations_user_id_users_id_fk": { + "name": "user_informations_user_id_users_id_fk", + "tableFrom": "user_informations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.respo_permanences": { + "name": "respo_permanences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "permanence_id": { + "name": "permanence_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "respo_permanences_user_id_users_id_fk": { + "name": "respo_permanences_user_id_users_id_fk", + "tableFrom": "respo_permanences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respo_permanences_permanence_id_permanences_id_fk": { + "name": "respo_permanences_permanence_id_permanences_id_fk", + "tableFrom": "respo_permanences", + "tableTo": "permanences", + "columnsFrom": [ + "permanence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "respo_permanences_user_id_permanence_id_pk": { + "name": "respo_permanences_user_id_permanence_id_pk", + "columns": [ + "user_id", + "permanence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_permanences": { + "name": "user_permanences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "permanence_id": { + "name": "permanence_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "claimed": { + "name": "claimed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_permanences_user_id_users_id_fk": { + "name": "user_permanences_user_id_users_id_fk", + "tableFrom": "user_permanences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_permanences_permanence_id_permanences_id_fk": { + "name": "user_permanences_permanence_id_permanences_id_fk", + "tableFrom": "user_permanences", + "tableTo": "permanences", + "columnsFrom": [ + "permanence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_permanences_user_id_permanence_id_pk": { + "name": "user_permanences_user_id_permanence_id_pk", + "columns": [ + "user_id", + "permanence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_role_id_roles_id_fk": { + "name": "user_preferences_role_id_roles_id_fk", + "tableFrom": "user_preferences", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id_role_id_pk": { + "name": "user_preferences_user_id_role_id_pk", + "columns": [ + "user_id", + "role_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_user_id_team_id_pk": { + "name": "user_teams_user_id_team_id_pk", + "columns": [ + "user_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_tent": { + "name": "user_tent", + "schema": "", + "columns": { + "user_id_1": { + "name": "user_id_1", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id_2": { + "name": "user_id_2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "confirmed": { + "name": "confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_tent_user_id_1_users_id_fk": { + "name": "user_tent_user_id_1_users_id_fk", + "tableFrom": "user_tent", + "tableTo": "users", + "columnsFrom": [ + "user_id_1" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_tent_user_id_2_users_id_fk": { + "name": "user_tent_user_id_2_users_id_fk", + "tableFrom": "user_tent", + "tableTo": "users", + "columnsFrom": [ + "user_id_2" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_tent_user_id_1_user_id_2_pk": { + "name": "user_tent_user_id_1_user_id_2_pk", + "columns": [ + "user_id_1", + "user_id_2" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vssqcmanswer": { + "name": "vssqcmanswer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "questionid": { + "name": "questionid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer_en": { + "name": "answer_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "vssqcmanswer_questionid_vssqcmquestion_id_fk": { + "name": "vssqcmanswer_questionid_vssqcmquestion_id_fk", + "tableFrom": "vssqcmanswer", + "tableTo": "vssqcmquestion", + "columnsFrom": [ + "questionid" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.vss_form": { + "name": "vss_form", + "schema": "public", + "values": [ + "pending", + "toretry", + "validated", + "rejected" + ] + }, + "public.question_type": { + "name": "question_type", + "schema": "public", + "values": [ + "single_choice", + "multiple_choice" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/src/database/migrations/meta/_journal.json b/backend/src/database/migrations/meta/_journal.json index 73ad2f7..3424d95 100644 --- a/backend/src/database/migrations/meta/_journal.json +++ b/backend/src/database/migrations/meta/_journal.json @@ -1,195 +1,202 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1743857362115, - "tag": "0000_workable_rafael_vega", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1743868246465, - "tag": "0001_stormy_quicksilver", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1743897931065, - "tag": "0002_luxuriant_lockjaw", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1743979532382, - "tag": "0003_material_lorna_dane", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1744064732443, - "tag": "0004_careless_misty_knight", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1744118020611, - "tag": "0005_needy_darwin", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1744126517418, - "tag": "0006_bright_boomerang", - "breakpoints": true - }, - { - "idx": 7, - "version": "7", - "when": 1744212319949, - "tag": "0007_striped_mulholland_black", - "breakpoints": true - }, - { - "idx": 8, - "version": "7", - "when": 1744215076184, - "tag": "0008_striped_bushwacker", - "breakpoints": true - }, - { - "idx": 9, - "version": "7", - "when": 1744241202940, - "tag": "0009_previous_mystique", - "breakpoints": true - }, - { - "idx": 10, - "version": "7", - "when": 1744241242502, - "tag": "0010_fair_mulholland_black", - "breakpoints": true - }, - { - "idx": 11, - "version": "7", - "when": 1744241259194, - "tag": "0011_fearless_nextwave", - "breakpoints": true - }, - { - "idx": 12, - "version": "7", - "when": 1746040673007, - "tag": "0012_productive_giant_man", - "breakpoints": true - }, - { - "idx": 13, - "version": "7", - "when": 1752757642698, - "tag": "0013_curly_angel", - "breakpoints": true - }, - { - "idx": 14, - "version": "7", - "when": 1753638076374, - "tag": "0014_special_reaper", - "breakpoints": true - }, - { - "idx": 15, - "version": "7", - "when": 1753744481956, - "tag": "0015_greedy_genesis", - "breakpoints": true - }, - { - "idx": 16, - "version": "7", - "when": 1754903172897, - "tag": "0016_sudden_ultimatum", - "breakpoints": true - }, - { - "idx": 17, - "version": "7", - "when": 1755637642205, - "tag": "0017_melted_meggan", - "breakpoints": true - }, - { - "idx": 18, - "version": "7", - "when": 1755858317109, - "tag": "0018_puzzling_arachne", - "breakpoints": true - }, - { - "idx": 19, - "version": "7", - "when": 1755906116757, - "tag": "0019_complete_moondragon", - "breakpoints": true - }, - { - "idx": 20, - "version": "7", - "when": 1755907709198, - "tag": "0020_strange_colonel_america", - "breakpoints": true - }, - { - "idx": 21, - "version": "7", - "when": 1756063134903, - "tag": "0021_colossal_madame_web", - "breakpoints": true - }, - { - "idx": 22, - "version": "7", - "when": 1757002717384, - "tag": "0022_light_omega_red", - "breakpoints": true - }, - { - "idx": 23, - "version": "7", - "when": 1782943789540, - "tag": "0023_zippy_colonel_america", - "breakpoints": true - }, - { - "idx": 24, - "version": "7", - "when": 1783776285772, - "tag": "0024_optimal_garia", - "breakpoints": true - }, - { - "idx": 25, - "version": "7", - "when": 1784742510283, - "tag": "0025_perfect_ulik", - "breakpoints": true - }, - { - "idx": 26, - "version": "7", - "when": 1784824629918, - "tag": "0026_swift_excalibur", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1743857362115, + "tag": "0000_workable_rafael_vega", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1743868246465, + "tag": "0001_stormy_quicksilver", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1743897931065, + "tag": "0002_luxuriant_lockjaw", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1743979532382, + "tag": "0003_material_lorna_dane", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1744064732443, + "tag": "0004_careless_misty_knight", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1744118020611, + "tag": "0005_needy_darwin", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1744126517418, + "tag": "0006_bright_boomerang", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1744212319949, + "tag": "0007_striped_mulholland_black", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1744215076184, + "tag": "0008_striped_bushwacker", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1744241202940, + "tag": "0009_previous_mystique", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1744241242502, + "tag": "0010_fair_mulholland_black", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1744241259194, + "tag": "0011_fearless_nextwave", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1746040673007, + "tag": "0012_productive_giant_man", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1752757642698, + "tag": "0013_curly_angel", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1753638076374, + "tag": "0014_special_reaper", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1753744481956, + "tag": "0015_greedy_genesis", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1754903172897, + "tag": "0016_sudden_ultimatum", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1755637642205, + "tag": "0017_melted_meggan", + "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1755858317109, + "tag": "0018_puzzling_arachne", + "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1755906116757, + "tag": "0019_complete_moondragon", + "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1755907709198, + "tag": "0020_strange_colonel_america", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1756063134903, + "tag": "0021_colossal_madame_web", + "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1757002717384, + "tag": "0022_light_omega_red", + "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1782943789540, + "tag": "0023_zippy_colonel_america", + "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1783776285772, + "tag": "0024_optimal_garia", + "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1784742510283, + "tag": "0025_perfect_ulik", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1784824629918, + "tag": "0026_swift_excalibur", + "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1785000000000, + "tag": "0027_faulty_hellion", + "breakpoints": true + } + ] +} diff --git a/backend/src/dto/email.dto.ts b/backend/src/dto/email.dto.ts index e423efc..2d734e4 100644 --- a/backend/src/dto/email.dto.ts +++ b/backend/src/dto/email.dto.ts @@ -1,13 +1,4 @@ -type EmailPayload = { - subject: string; - templateName: string; - recipientsGroups?: string[]; - sendTo?: string[]; - html?: string; - title?: string; - content?: string; - text?: string; -}; +import { type EmailPayload } from '../types/email'; export type EmailRequestBody = { payload?: EmailPayload; diff --git a/backend/src/dto/team.dto.ts b/backend/src/dto/team.dto.ts index ca34e97..43d5ca8 100644 --- a/backend/src/dto/team.dto.ts +++ b/backend/src/dto/team.dto.ts @@ -21,39 +21,3 @@ export type TeamQuery = { teamId?: string; teamID?: string; }; - -export type TeamMemberRow = { - userId: number; -}; - -export type StudentRow = { - userId: number; - email: string; - branch: string; - male?: boolean; -}; - -export type TeamAssignmentNotification = { - email: string; - teamId: number; -}; - -export type TeamRow = { - teamId: number; - name: string; - description: string; - type: string; - socialLink: string; - riCompatible: boolean; -}; - -export type TeamDistributionState = TeamRow & { - size: number; - girlsCount: number; -}; - -export type TeamSizeRow = { - teamId: number; - teamName: string; - size: number; -}; diff --git a/backend/src/dto/user.dto.ts b/backend/src/dto/user.dto.ts index 7e0107a..6d0056b 100644 --- a/backend/src/dto/user.dto.ts +++ b/backend/src/dto/user.dto.ts @@ -1,6 +1,6 @@ -import { type VssSubmissionAnswer } from '../services/user.service'; +import { type VssSubmissionAnswer } from '../types/user'; -export interface AdminCreateUserDto { +export interface AdminCreateUserBody { firstName: string; lastName: string; email: string; @@ -26,7 +26,7 @@ export type ProfileBody = { contact: string; }; -export type CreateUserContactInformationDto = { +export type CreateUserContactInformationBody = { emergency_contact_name: string; emergency_contact_phone: string; }; diff --git a/backend/src/email/email.preview-data.ts b/backend/src/email/email.preview-data.ts index b866057..6a656e8 100644 --- a/backend/src/email/email.preview-data.ts +++ b/backend/src/email/email.preview-data.ts @@ -1,5 +1,5 @@ -import type { TemplateData } from '../../types/email'; -import { service_url } from '../utils/secret'; +import type { TemplateData } from '../types/email'; +import { service_url } from '../shared/secrets/secrets'; export const defaultPreviewData: Record = { custom: { diff --git a/backend/src/email/email.registry.ts b/backend/src/email/email.registry.ts index 140074e..f582ca5 100644 --- a/backend/src/email/email.registry.ts +++ b/backend/src/email/email.registry.ts @@ -1,4 +1,4 @@ -import type { PermanenceEmailData, TemplateRenderer, TeamAssignmentEmailData } from '../../types/email'; +import type { PermanenceEmailData, TemplateRenderer, TeamAssignmentEmailData } from '../types/email'; export const templateResetPassword = 'reset-password.html'; const templateNotebook = 'notebook.html'; diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index a185d88..74330bd 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -1,21 +1,21 @@ -import { type NextFunction, type Request, type Response } from "express"; -import { Unauthorized } from "../utils/responses"; // Assurez-vous que cette fonction est bien dĂ©finie -import { decodeToken } from "../utils/token"; +import { type NextFunction, type Request, type Response } from 'express'; +import { Unauthorized } from '../shared/http/responses'; // Assurez-vous que cette fonction est bien dĂ©finie +import { decodeToken } from '../shared/utils/token'; export const authenticateUser = (req: Request, res: Response, next: NextFunction) => { try { const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return Unauthorized(res, { msg: "Unauthorized: Missing or malformed token" }); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return Unauthorized(res, { msg: 'Unauthorized: Missing or malformed token' }); } - const token = authHeader.split(" ")[1]; + const token = authHeader.split(' ')[1]; const decoded = decodeToken(token); req.user = decoded; // Ajoute les donnĂ©es du token Ă  l'objet `req` next(); } catch { - return Unauthorized(res, { msg: "Unauthorized: Invalid or expired token" }); + return Unauthorized(res, { msg: 'Unauthorized: Invalid or expired token' }); } }; diff --git a/backend/src/middlewares/automation.middleware.ts b/backend/src/middlewares/automation.middleware.ts index 606e75d..655f267 100644 --- a/backend/src/middlewares/automation.middleware.ts +++ b/backend/src/middlewares/automation.middleware.ts @@ -1,6 +1,6 @@ import { type NextFunction, type Request, type Response } from 'express'; -import { Unauthorized } from '../utils/responses'; // Assurez-vous que cette fonction est bien dĂ©finie -import { automation_token } from '../utils/secret'; +import { Unauthorized } from '../shared/http/responses'; // Assurez-vous que cette fonction est bien dĂ©finie +import { automation_token } from '../shared/secrets/secrets'; export const authenticateAutomation = (req: Request, res: Response, next: NextFunction) => { try { diff --git a/backend/src/middlewares/multer.middleware.ts b/backend/src/middlewares/multer.middleware.ts index e654a40..f21d24f 100644 --- a/backend/src/middlewares/multer.middleware.ts +++ b/backend/src/middlewares/multer.middleware.ts @@ -2,8 +2,8 @@ import { type NextFunction, type Request, type Response } from 'express'; import fs from 'fs/promises'; import multer from 'multer'; import path from 'path'; -import { Error } from '../utils/responses'; -import { isSafeUploadSegment, removeUploadedDocuments } from '../utils/uploadDocuments'; +import { Error } from '../shared/http/responses'; +import { isSafeUploadSegment, removeUploadedDocuments } from '../shared/storage/uploadDocuments'; export enum MIMEType { PDF = 'application/pdf', diff --git a/backend/src/middlewares/respoperm.middleware.ts b/backend/src/middlewares/respoperm.middleware.ts index b5f5939..a9e3eed 100644 --- a/backend/src/middlewares/respoperm.middleware.ts +++ b/backend/src/middlewares/respoperm.middleware.ts @@ -1,16 +1,12 @@ -import { type NextFunction, type Request, type Response } from "express"; -import { isUserRespoOfPermanence } from "../services/permanence.service"; -import { Error } from "../utils/responses"; +import { type NextFunction, type Request, type Response } from 'express'; +import { isUserRespoOfPermanence } from '../services/permanence.service'; +import { Error } from '../shared/http/responses'; -export const isRespoMiddleware = async ( - req: Request, - res: Response, - next: NextFunction -) => { +export const isRespoMiddleware = async (req: Request, res: Response, next: NextFunction) => { const userId = req.user?.userId; if (!userId) { - Error(res, { msg: "Utilisateur ou permanence non spĂ©cifiĂ©" }); + Error(res, { msg: 'Utilisateur ou permanence non spĂ©cifiĂ©' }); return; } @@ -23,6 +19,6 @@ export const isRespoMiddleware = async ( next(); // ✅ L'utilisateur est bien respo, on continue } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la vĂ©rification du responsable" }); + Error(res, { msg: 'Erreur lors de la vĂ©rification du responsable' }); } }; diff --git a/backend/src/middlewares/user.middleware.ts b/backend/src/middlewares/user.middleware.ts index b8b83ba..b37c70f 100644 --- a/backend/src/middlewares/user.middleware.ts +++ b/backend/src/middlewares/user.middleware.ts @@ -1,41 +1,35 @@ -import { type NextFunction, type Request, type Response } from "express"; -import { Unauthorized } from "../utils/responses"; // adapte selon ton projet +import { type NextFunction, type Request, type Response } from 'express'; +import { Unauthorized } from '../shared/http/responses'; // adapte selon ton projet -export const checkRole = ( - requiredPermission?: string, - requiredRoles?: string[] -) => { +export const checkRole = (requiredPermission?: string, requiredRoles?: string[]) => { return (req: Request, res: Response, next: NextFunction) => { const user = req.user; if (!user) { - Unauthorized(res, { msg: "AccĂšs non autorisĂ©" }); + Unauthorized(res, { msg: 'AccĂšs non autorisĂ©' }); return; } try { - const isAdmin = user.userPermission === "Admin"; + const isAdmin = user.userPermission === 'Admin'; - const hasPermission = - !requiredPermission || user.userPermission === requiredPermission; + const hasPermission = !requiredPermission || user.userPermission === requiredPermission; const hasRole = !requiredRoles || (Array.isArray(user.userRoles) && - user.userRoles.some((role: { roleName: string }) => - requiredRoles.includes(role.roleName) - )); + user.userRoles.some((role: { roleName: string }) => requiredRoles.includes(role.roleName))); if (!isAdmin && !(hasPermission || hasRole)) { Unauthorized(res, { - msg: "AccĂšs interdit, rĂŽle ou permission insuffisants", + msg: 'AccĂšs interdit, rĂŽle ou permission insuffisants', }); return; } next(); } catch { - Unauthorized(res, { msg: "Token invalide ou expirĂ©" }); + Unauthorized(res, { msg: 'Token invalide ou expirĂ©' }); } }; }; diff --git a/backend/src/schemas/Basic/vssqcmquestion.schema.ts b/backend/src/schemas/Basic/vssqcmquestion.schema.ts index cc9c3c1..fb3e5d0 100644 --- a/backend/src/schemas/Basic/vssqcmquestion.schema.ts +++ b/backend/src/schemas/Basic/vssqcmquestion.schema.ts @@ -5,6 +5,7 @@ export const questionTypeEnum = pgEnum('question_type', ['single_choice', 'multi export const vssqcmquestionSchema = pgTable('vssqcmquestion', { id: serial('id').primaryKey(), question: text('question').notNull(), + question_en: text('question_en'), points: integer('points').notNull(), type: questionTypeEnum('type').notNull(), }); diff --git a/backend/src/schemas/Relational/vssqcmanswer.schema.ts b/backend/src/schemas/Relational/vssqcmanswer.schema.ts index 008c14b..07167dd 100644 --- a/backend/src/schemas/Relational/vssqcmanswer.schema.ts +++ b/backend/src/schemas/Relational/vssqcmanswer.schema.ts @@ -7,6 +7,7 @@ export const vssqcmanswerSchema = pgTable('vssqcmanswer', { .references(() => vssqcmquestionSchema.id, { onDelete: 'cascade' }) .notNull(), answer: text('answer').notNull(), + answer_en: text('answer_en'), is_correct: boolean('is_correct').notNull(), }); diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index c62fd13..665738a 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -3,11 +3,11 @@ import { randomBytes } from 'crypto'; import { eq } from 'drizzle-orm'; import { JSDOM } from 'jsdom'; import jwt from 'jsonwebtoken'; -import { cas_validate_url } from '../../src/utils/secret'; +import { cas_validate_url } from '../shared/secrets/secrets'; import { db } from '../database/db'; import { userSchema } from '../schemas/Basic/user.schema'; import { registrationSchema } from '../schemas/Relational/registration.schema'; -import { jwtSecret } from '../utils/secret'; +import { jwtSecret } from '../shared/secrets/secrets'; import type { AuthTokenUser } from '../types/auth'; import * as role_service from './role.service'; import * as user_service from './user.service'; diff --git a/backend/src/services/discord.service.ts b/backend/src/services/discord.service.ts index 07602c4..a31452f 100644 --- a/backend/src/services/discord.service.ts +++ b/backend/src/services/discord.service.ts @@ -2,36 +2,38 @@ import axios from 'axios'; import { eq } from 'drizzle-orm'; import { db } from '../database/db'; // Import de la connexion PostgreSQL import { userSchema } from '../schemas/Basic/user.schema'; -import { discord_client_id, discord_client_secret, discord_redirect_uri } from '../utils/secret'; +import { discord_client_id, discord_client_secret, discord_redirect_uri } from '../shared/secrets/secrets'; export const syncDiscordUserId = async (code: string, userId: number) => { // Étape 1 : Ă©change le code contre un access token - const tokenResponse = await axios.post('https://discord.com/api/oauth2/token', new URLSearchParams({ - client_id: discord_client_id, - client_secret: discord_client_secret, - grant_type: 'authorization_code', - code, - redirect_uri: discord_redirect_uri, - scope: 'identify' - }), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }); + const tokenResponse = await axios.post( + 'https://discord.com/api/oauth2/token', + new URLSearchParams({ + client_id: discord_client_id, + client_secret: discord_client_secret, + grant_type: 'authorization_code', + code, + redirect_uri: discord_redirect_uri, + scope: 'identify', + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); const access_token = tokenResponse.data.access_token; // Étape 2 : rĂ©cupĂšre les infos utilisateur avec le token const userResponse = await axios.get('https://discord.com/api/users/@me', { headers: { - Authorization: `Bearer ${access_token}` - } + Authorization: `Bearer ${access_token}`, + }, }); //Etape 3 : Update le discord_id de l'user - await db.update(userSchema) - .set({ discord_id: userResponse.data.id, }) - .where(eq(userSchema.id, userId)); + await db.update(userSchema).set({ discord_id: userResponse.data.id }).where(eq(userSchema.id, userId)); return userResponse.data; // { id, username, discriminator, ... } }; diff --git a/backend/src/services/email.service.ts b/backend/src/services/email.service.ts index a345b5b..a4a5e65 100644 --- a/backend/src/services/email.service.ts +++ b/backend/src/services/email.service.ts @@ -1,9 +1,9 @@ import nodemailer from 'nodemailer'; -import type { EmailOptions, TemplateData } from '../../types/email'; +import type { EmailOptions, TemplateData } from '../types/email'; import { templateRenderers } from '../email/email.registry'; import { compileTemplate } from '../email/email.renderer'; import * as user_service from '../services/user.service'; -import { email_from, email_host, email_password, email_user } from '../utils/secret'; +import { email_from, email_host, email_password, email_user } from '../shared/secrets/secrets'; export const getRecipients = async ( recipientsGroups: string[] | undefined, diff --git a/backend/src/services/permanence.service.ts b/backend/src/services/permanence.service.ts index ae76a0b..aa9a36e 100644 --- a/backend/src/services/permanence.service.ts +++ b/backend/src/services/permanence.service.ts @@ -1,55 +1,56 @@ -import { and, eq, inArray, sql } from "drizzle-orm"; -import fs from "fs"; -import Papa from "papaparse"; -import type { PermanenceEmailData } from "../../types/email"; -import type { CsvPermanence, LightUser, Notification, Permanence } from "../../types/permanence"; -import { db } from "../database/db"; -import { AlreadyRegisteredError, PermanenceClosedError, PermanenceFullError, PermanenceNotFoundError, RegisterDeadlineError, UnauthorizedError, UnregisterDeadlineError } from "../errors/permanence.error"; -import { permanenceSchema } from "../schemas/Basic/permanence.schema"; -import { userSchema } from "../schemas/Basic/user.schema"; -import { respoPermanenceSchema, userPermanenceSchema } from "../schemas/Relational/userpermanences.schema"; -import { email_from } from "../utils/secret"; -import { generateEmailHtml, sendEmail } from "./email.service"; +import { and, eq, inArray, sql } from 'drizzle-orm'; +import fs from 'fs'; +import Papa from 'papaparse'; +import type { PermanenceEmailData } from '../types/email'; +import type { CsvPermanence, LightUser, Notification, Permanence } from '../types/permanence'; +import { db } from '../database/db'; +import { + AlreadyRegisteredError, + PermanenceClosedError, + PermanenceFullError, + PermanenceNotFoundError, + RegisterDeadlineError, + UnauthorizedError, + UnregisterDeadlineError, +} from '../errors/permanence.error'; +import { permanenceSchema } from '../schemas/Basic/permanence.schema'; +import { userSchema } from '../schemas/Basic/user.schema'; +import { respoPermanenceSchema, userPermanenceSchema } from '../schemas/Relational/userpermanences.schema'; +import { email_from } from '../shared/secrets/secrets'; +import { generateEmailHtml, sendEmail } from './email.service'; export const getPermanenceById = async (permId: number) => { const permanence = await db.query.permanenceSchema.findFirst({ where: eq(permanenceSchema.id, permId), }); - if (!permanence) throw new PermanenceNotFoundError("Permanence introuvable"); + if (!permanence) throw new PermanenceNotFoundError('Permanence introuvable'); return permanence; }; // ➕ S'inscrire Ă  une permanence -export const registerUserToPermanence = async ( - userId: number, - permId: number -) => { +export const registerUserToPermanence = async (userId: number, permId: number) => { try { // VĂ©rifications lĂ©gĂšres (sans verrouillage) const user = await db.query.userSchema.findFirst({ where: eq(userSchema.id, userId), }); - if ( - !user || - (user.permission !== "Student" && user.permission !== "Admin") - ) { - throw new UnauthorizedError("Unauthorized"); + if (!user || (user.permission !== 'Student' && user.permission !== 'Admin')) { + throw new UnauthorizedError('Unauthorized'); } const permanence = await getPermanenceById(permId); - if (!permanence.is_open) - throw new PermanenceClosedError("Permanence not open"); + if (!permanence.is_open) throw new PermanenceClosedError('Permanence not open'); - const limitDate = new Date(String(permanence.start_at).replace(/Z$/, "")); + const limitDate = new Date(String(permanence.start_at).replace(/Z$/, '')); const now = new Date(); if (now > limitDate) { - throw new RegisterDeadlineError("Too late to register"); + throw new RegisterDeadlineError('Too late to register'); } if (permanence.capacity == 0) { - throw new PermanenceFullError("Permanence full"); + throw new PermanenceFullError('Permanence full'); } // Transaction avec verrouillage de table complet @@ -66,7 +67,7 @@ export const registerUserToPermanence = async ( // 3. Si aucune ligne modifiĂ©e = pas de place disponible if (updateResult.length === 0) { - throw new PermanenceFullError("Permanence full"); + throw new PermanenceFullError('Permanence full'); } // 4. InsĂ©rer l'utilisateur (seulement si l'UPDATE a rĂ©ussi) @@ -78,13 +79,13 @@ export const registerUserToPermanence = async ( } catch (error: any) { // Gestion des erreurs de contraintes de base de donnĂ©es if ( - error.code === "23505" || // Contrainte unique PostgreSQL - error.code === "23000" || // Contrainte d'intĂ©gritĂ© gĂ©nĂ©rale - error.message?.includes("UNIQUE constraint") || - error.message?.includes("duplicate key") || - error.message?.includes("PRIMARY KEY constraint") + error.code === '23505' || // Contrainte unique PostgreSQL + error.code === '23000' || // Contrainte d'intĂ©gritĂ© gĂ©nĂ©rale + error.message?.includes('UNIQUE constraint') || + error.message?.includes('duplicate key') || + error.message?.includes('PRIMARY KEY constraint') ) { - throw new AlreadyRegisteredError("Already registered"); + throw new AlreadyRegisteredError('Already registered'); } // Re-lancer les autres erreurs throw error; @@ -92,27 +93,18 @@ export const registerUserToPermanence = async ( }; // ❌ Se dĂ©sinscrire d'une permanence -export const unregisterUserFromPermanence = async ( - userId: number, - permId: number -) => { +export const unregisterUserFromPermanence = async (userId: number, permId: number) => { const permanence = await getPermanenceById(permId); const now = new Date(); const limitDate = new Date(permanence.start_at); limitDate.setDate(limitDate.getDate() - 1); - if (now > limitDate) - throw new UnregisterDeadlineError("Too late to unregister"); + if (now > limitDate) throw new UnregisterDeadlineError('Too late to unregister'); // DĂ©sinscrire l'utilisateur await db .delete(userPermanenceSchema) - .where( - and( - eq(userPermanenceSchema.user_id, userId), - eq(userPermanenceSchema.permanence_id, permId) - ) - ); + .where(and(eq(userPermanenceSchema.user_id, userId), eq(userPermanenceSchema.permanence_id, permId))); await modifyPermCap(permId, 1); }; @@ -134,7 +126,7 @@ export const createPermanence = async ( end_at: Date, capacity: number, difficulty: number, - respoId: number + respoId: number, ) => { // Étape 1 : CrĂ©ation de la permanence const [newPermanence] = await db @@ -162,19 +154,13 @@ export const createPermanence = async ( export const deletePermanence = async (permId: number) => { // Étape 1 : Supprimer les inscriptions des utilisateurs - await db - .delete(userPermanenceSchema) - .where(eq(userPermanenceSchema.permanence_id, permId)); + await db.delete(userPermanenceSchema).where(eq(userPermanenceSchema.permanence_id, permId)); // Étape 2 : Supprimer les responsables associĂ©s - await db - .delete(respoPermanenceSchema) - .where(eq(respoPermanenceSchema.permanence_id, permId)); + await db.delete(respoPermanenceSchema).where(eq(respoPermanenceSchema.permanence_id, permId)); // Étape 3 : Supprimer la permanence - await db - .delete(permanenceSchema) - .where(eq(permanenceSchema.id, permId)); + await db.delete(permanenceSchema).where(eq(permanenceSchema.id, permId)); }; export const updatePermanence = async ( @@ -186,7 +172,7 @@ export const updatePermanence = async ( end_at: Date, capacity: number, difficulty: number, - respoId: number + respoId: number, ) => { // Étape 1 : Mise Ă  jour de la permanence await db @@ -204,9 +190,7 @@ export const updatePermanence = async ( .where(eq(permanenceSchema.id, permId)); // Étape 2 : Suppression des anciens responsables (si nĂ©cessaire) - await db - .delete(respoPermanenceSchema) - .where(eq(respoPermanenceSchema.permanence_id, permId)); + await db.delete(respoPermanenceSchema).where(eq(respoPermanenceSchema.permanence_id, permId)); // Étape 3 : Ajout du nouveau responsable if (respoId) { @@ -219,18 +203,12 @@ export const updatePermanence = async ( // Ouvrir une permanence (Admin action) export const openPermanence = async (permId: number) => { - await db - .update(permanenceSchema) - .set({ is_open: true }) - .where(eq(permanenceSchema.id, permId)); + await db.update(permanenceSchema).set({ is_open: true }).where(eq(permanenceSchema.id, permId)); }; // Fermer une permanence (Admin action) export const closePermanence = async (permId: number) => { - await db - .update(permanenceSchema) - .set({ is_open: false }) - .where(eq(permanenceSchema.id, permId)); + await db.update(permanenceSchema).set({ is_open: false }).where(eq(permanenceSchema.id, permId)); }; // Modifier la capacitĂ© de la permanence @@ -238,12 +216,9 @@ export const modifyPermCap = async (permId: number, factor: number) => { const perm = await getPermanenceById(permId); const newPermCap = Number(perm.capacity) + factor; - if (newPermCap < 0) throw new Error("Invalid capacity"); + if (newPermCap < 0) throw new Error('Invalid capacity'); - await db - .update(permanenceSchema) - .set({ capacity: newPermCap }) - .where(eq(permanenceSchema.id, permId)); + await db.update(permanenceSchema).set({ capacity: newPermCap }).where(eq(permanenceSchema.id, permId)); }; // Voir ses permanences @@ -257,10 +232,7 @@ export const getMyPermanences = async (userId: number) => { location: permanenceSchema.location, }) .from(userPermanenceSchema) - .innerJoin( - permanenceSchema, - eq(permanenceSchema.id, userPermanenceSchema.permanence_id) - ) + .innerJoin(permanenceSchema, eq(permanenceSchema.id, userPermanenceSchema.permanence_id)) .where(eq(userPermanenceSchema.user_id, userId)); // Ajout du responsables @@ -281,10 +253,9 @@ export const getMyPermanences = async (userId: number) => { ...perm, respo: respo ?? null, }; - }) + }), ); - return results; }; @@ -308,10 +279,9 @@ export const getAllPermanences = async () => { ...perm, respo: respo ?? null, }; - }) + }), ); - return results; }; @@ -338,18 +308,10 @@ export const addUserToPermanence = async (userId: number, permId: number) => { await modifyPermCap(permId, -1); }; -export const removeUserToPermanence = async ( - userId: number, - permId: number -) => { +export const removeUserToPermanence = async (userId: number, permId: number) => { await db .delete(userPermanenceSchema) - .where( - and( - eq(userPermanenceSchema.user_id, userId), - eq(userPermanenceSchema.permanence_id, permId) - ) - ); + .where(and(eq(userPermanenceSchema.user_id, userId), eq(userPermanenceSchema.permanence_id, permId))); await modifyPermCap(permId, 1); }; @@ -377,16 +339,14 @@ export const getAllPermanencesWithUsers = async () => { ...permanence, users: userRelations, }; - }) + }), ); return results; }; -export const importPermanencesFromCSV = async ( - filePath: string -): Promise => { - const fileContent = fs.readFileSync(filePath, "utf8"); +export const importPermanencesFromCSV = async (filePath: string): Promise => { + const fileContent = fs.readFileSync(filePath, 'utf8'); const { data, errors } = Papa.parse(fileContent, { header: true, @@ -394,8 +354,8 @@ export const importPermanencesFromCSV = async ( }); if (errors.length > 0) { - console.error("CSV parsing errors:", errors); - throw new Error("Erreur lors du parsing du CSV."); + console.error('CSV parsing errors:', errors); + throw new Error('Erreur lors du parsing du CSV.'); } const parsedData = data.map((r) => ({ @@ -407,40 +367,26 @@ export const importPermanencesFromCSV = async ( capacity: parseInt(r.capacity, 10), difficulty: parseInt(r.difficulty, 10), is_open: false, - })); await db.insert(permanenceSchema).values(parsedData); }; -export const isUserRespoOfPermanence = async ( - userId: number, -): Promise => { - const respo = await db - .select() - .from(respoPermanenceSchema) - .where( - eq(respoPermanenceSchema.user_id, userId) - ); +export const isUserRespoOfPermanence = async (userId: number): Promise => { + const respo = await db.select().from(respoPermanenceSchema).where(eq(respoPermanenceSchema.user_id, userId)); return respo.length > 0; }; export const getPermanenceDetailsForRespo = async (respoId: number) => { // Étape 1 : Trouver les permanences dont il est respo - const respos = await db - .select() - .from(respoPermanenceSchema) - .where(eq(respoPermanenceSchema.user_id, respoId)); + const respos = await db.select().from(respoPermanenceSchema).where(eq(respoPermanenceSchema.user_id, respoId)); const permanenceIds = respos.map((r) => r.permanence_id); - if (permanenceIds.length === 0) throw new Error("Pas de permanences");; + if (permanenceIds.length === 0) throw new Error('Pas de permanences'); // Étape 2 : RĂ©cupĂ©rer les permanences - const permanences = await db - .select() - .from(permanenceSchema) - .where(inArray(permanenceSchema.id, permanenceIds)); + const permanences = await db.select().from(permanenceSchema).where(inArray(permanenceSchema.id, permanenceIds)); // Étape 3 : RĂ©cupĂ©rer les membres avec infos utiles const results = await Promise.all( @@ -461,7 +407,7 @@ export const getPermanenceDetailsForRespo = async (respoId: number) => { permanence: perm, members, }; - }) + }), ); return results; @@ -471,15 +417,10 @@ export const claimMember = async (userId: number, permId: number, claimed: boole await db .update(userPermanenceSchema) .set({ claimed }) - .where( - and( - eq(userPermanenceSchema.user_id, userId), - eq(userPermanenceSchema.permanence_id, permId) - ) - ); + .where(and(eq(userPermanenceSchema.user_id, userId), eq(userPermanenceSchema.permanence_id, permId))); }; -export const getDailyNotifications = async (): Promise => { +export const getDailyNotifications = async (): Promise => { const permanences = await db.query.permanenceSchema.findMany({ where: sql` ${permanenceSchema.start_at} >= CURRENT_DATE + INTERVAL '1 day' @@ -489,7 +430,7 @@ export const getDailyNotifications = async (): Promise => { }); return getMembersFromPermanences(permanences); -} +}; export const getHourlyNotifications = async (): Promise => { const permanences = await db.query.permanenceSchema.findMany({ @@ -504,10 +445,14 @@ export const getHourlyNotifications = async (): Promise => { }; // Cette fonction est vouĂ©e Ă  disparaitre lors du passage Ă  Prisma, avec un simple "with" -export const getMembersFromPermanences = async (permanences: Permanence[]): Promise<{ - permanence: Permanence, - members: LightUser[] -}[]> => { +export const getMembersFromPermanences = async ( + permanences: Permanence[], +): Promise< + { + permanence: Permanence; + members: LightUser[]; + }[] +> => { return await Promise.all( permanences.map(async (perm) => { const members = await db @@ -523,57 +468,46 @@ export const getMembersFromPermanences = async (permanences: Permanence[]): Prom return { permanence: perm, - members: members + members: members, }; - }) + }), ); -} +}; -export const sendNotifications = async ( - notifications: Notification[] -) => { +export const sendNotifications = async (notifications: Notification[]) => { for (const notification of notifications) { - const permanenceEmailData: PermanenceEmailData = { permName: notification.permanence.name, - permBeginDate: - new Intl.DateTimeFormat("fr-FR", { - day: "2-digit", - month: "long", - }).format(notification.permanence.start_at), - permBeginHour: - new Intl.DateTimeFormat("fr-FR", { - hour: "2-digit", - minute: "2-digit", - }).format(notification.permanence.start_at), - permEndDate: - new Intl.DateTimeFormat("fr-FR", { - day: "2-digit", - month: "long", - }).format(notification.permanence.end_at), - permEndHour: - new Intl.DateTimeFormat("fr-FR", { - hour: "2-digit", - minute: "2-digit", - }).format(notification.permanence.end_at), + permBeginDate: new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: 'long', + }).format(notification.permanence.start_at), + permBeginHour: new Intl.DateTimeFormat('fr-FR', { + hour: '2-digit', + minute: '2-digit', + }).format(notification.permanence.start_at), + permEndDate: new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: 'long', + }).format(notification.permanence.end_at), + permEndHour: new Intl.DateTimeFormat('fr-FR', { + hour: '2-digit', + minute: '2-digit', + }).format(notification.permanence.end_at), permLocation: notification.permanence.location, - permDescription: notification.permanence.description - } - const subject = `[RAPPEL] Permanence - ${notification.permanence.name}` + permDescription: notification.permanence.description, + }; + const subject = `[RAPPEL] Permanence - ${notification.permanence.name}`; - const htmlEmail = generateEmailHtml( - "templateNotifyPermanenceReminder", - permanenceEmailData - ); + const htmlEmail = generateEmailHtml('templateNotifyPermanenceReminder', permanenceEmailData); for (const member of notification.members) { try { - const emailOptions = { from: email_from, to: [member.email], subject: subject, - text: "", + text: '', html: htmlEmail, }; diff --git a/backend/src/services/team.service.ts b/backend/src/services/team.service.ts index 9a66594..aba6cf5 100644 --- a/backend/src/services/team.service.ts +++ b/backend/src/services/team.service.ts @@ -8,7 +8,7 @@ import { teamShotgunSchema } from '../schemas/Relational/teamshotgun.schema'; import { userTeamsSchema } from '../schemas/Relational/userteams.schema'; import { getFaction } from './faction.service'; import * as user_service from '../services/user.service'; -import type { StudentRow, TeamMemberRow, TeamRow, TeamAssignmentNotification } from '../dto/team.dto'; +import type { StudentRow, TeamMemberRow, TeamRow, TeamAssignmentNotification } from '../types/team'; import sendEmailToNewAssignedStudents from './team/email.team'; import assignUsersToTeams from './team/assignation.team'; diff --git a/backend/src/services/team/assignation.team.ts b/backend/src/services/team/assignation.team.ts index 6e0e009..9d11b28 100644 --- a/backend/src/services/team/assignation.team.ts +++ b/backend/src/services/team/assignation.team.ts @@ -2,7 +2,7 @@ import { eq, inArray } from 'drizzle-orm'; import { db } from '../../database/db'; import { userSchema } from '../../schemas/Basic/user.schema'; import { userTeamsSchema } from '../../schemas/Relational/userteams.schema'; -import type { StudentRow, TeamRow, TeamDistributionState, TeamAssignmentNotification } from '../../dto/team.dto'; +import type { StudentRow, TeamRow, TeamDistributionState, TeamAssignmentNotification } from '../../types/team'; /** * DĂ©termine si un(e) Ă©tudiant(e) est une fille. diff --git a/backend/src/services/team/email.team.ts b/backend/src/services/team/email.team.ts index a147162..8e6cf33 100644 --- a/backend/src/services/team/email.team.ts +++ b/backend/src/services/team/email.team.ts @@ -4,10 +4,10 @@ import { factionSchema } from '../../schemas/Basic/faction.schema'; import { teamSchema } from '../../schemas/Basic/team.schema'; import { teamFactionSchema } from '../../schemas/Relational/teamfaction.schema'; import { generateEmailHtml, sendEmail } from '../email.service'; -import type { TeamAssignmentNotification } from '../../dto/team.dto'; -import { email_from, email_concurrency } from '../../utils/secret'; -import type { TeamAssignmentEmailData } from '../../../types/email'; -import getPLimit from '../../utils/pLimit'; +import type { TeamAssignmentNotification } from '../../types/team'; +import { email_from, email_concurrency } from '../../shared/secrets/secrets'; +import type { TeamAssignmentEmailData } from '../../types/email'; +import getPLimit from '../../shared/utils/pLimit'; /** * Envoie un email de notification Ă  chaque Ă©tudiant venant d'ĂȘtre diff --git a/backend/src/services/user.service.ts b/backend/src/services/user.service.ts index 11d9092..6737f05 100644 --- a/backend/src/services/user.service.ts +++ b/backend/src/services/user.service.ts @@ -2,40 +2,22 @@ import bcrypt from 'bcryptjs'; import { eq } from 'drizzle-orm'; import * as randomstring from 'randomstring'; import { db } from '../database/db'; // Import de la connexion PostgreSQL -import type { AdminCreateUserDto, CreateUserContactInformationDto, VssSubmissionPayload } from '../dto/user.dto'; +import type { AdminCreateUserBody, CreateUserContactInformationBody, VssSubmissionPayload } from '../dto/user.dto'; import { type User, userSchema } from '../schemas/Basic/user.schema'; import { vssqcmquestionSchema } from '../schemas/Basic/vssqcmquestion.schema'; import { vssqcmanswerSchema } from '../schemas/Relational/vssqcmanswer.schema'; import { registrationSchema } from '../schemas/Relational/registration.schema'; import * as auth_service from '../services/auth.service'; -import * as SIEP_Utils from '../utils/siep'; +import * as SIEP_Utils from '../shared/integrations/siep'; import * as Banned_Service from './banned.service'; import { createRegistrationToken } from './auth.service'; import { getFaction } from './faction.service'; import { getUserRoles } from './role.service'; import { getTeam, getTeamFaction, getUserTeam } from './team.service'; import { userInformationSchema } from '../schemas/Relational/userinformation.schema'; -import { addUserToRespondentStudentsList } from '../utils/billetweb'; +import { addUserToRespondentStudentsList } from '../shared/integrations/billetweb'; import { generateEmailHtml, sendEmail } from './email.service'; -import { email_from } from '../utils/secret'; - -export type VssQuestionnaireAnswer = { - id: number; - answer: string; -}; - -export type VssQuestionnaireQuestion = { - id: number; - question: string; - points: number; - type: 'single_choice' | 'multiple_choice'; - answers: VssQuestionnaireAnswer[]; -}; - -export type VssSubmissionAnswer = { - questionId: number; - answerIds: number[]; -}; +import { email_from } from '../shared/secrets/secrets'; // Fonction pour rĂ©cupĂ©rer un utilisateur par email export const getUserByEmail = async (email: string) => { @@ -161,7 +143,7 @@ export const updateUserStudent = async (firstName: string, lastName: string, ema } }; -export const adminCreateUser = async (data: AdminCreateUserDto) => { +export const adminCreateUser = async (data: AdminCreateUserBody) => { const email = data.email.toLowerCase(); const userInDb = await getUserByEmail(email); @@ -268,13 +250,13 @@ export const getUserContactInformation = async (userId: number) => { } }; -export const createUserContactInformation = async (userId: number, contact: CreateUserContactInformationDto) => { +export const createUserContactInformation = async (userId: number, contact: CreateUserContactInformationBody) => { try { if (!contact.emergency_contact_name || !contact.emergency_contact_phone) { throw new Error("Le nom et le numĂ©ro de tĂ©lĂ©phone du contact d'urgence sont requis."); } - if (!/^\+?\d{10,15}$/.test(contact.emergency_contact_phone)) { + if (!/^\+?(\s?\d){9,15}$/.test(contact.emergency_contact_phone)) { throw new Error("Le numĂ©ro de tĂ©lĂ©phone du contact d'urgence n'est pas valide."); } @@ -345,18 +327,25 @@ export const getVssQuestionnaire = async () => { const questions = await db.select().from(vssqcmquestionSchema); const answers = await db.select().from(vssqcmanswerSchema); - return questions.map((question) => ({ - id: question.id, - question: question.question, - points: question.points, - type: question.type, - answers: answers - .filter((answer) => answer.questionid === question.id) - .map((answer) => ({ - id: answer.id, - answer: answer.answer, - })), - })); + return questions + .slice() + .sort((firstQuestion, secondQuestion) => firstQuestion.id - secondQuestion.id) + .map((question) => ({ + id: question.id, + question: question.question, + questionEn: question.question_en ?? undefined, + points: question.points, + type: question.type, + answers: answers + .slice() + .sort((firstAnswer, secondAnswer) => firstAnswer.id - secondAnswer.id) + .filter((answer) => answer.questionid === question.id) + .map((answer) => ({ + id: answer.id, + answer: answer.answer, + answerEn: answer.answer_en ?? undefined, + })), + })); } catch (err) { console.error('Erreur lors de la rĂ©cupĂ©ration du questionnaire VSS:', err); throw new Error('Erreur de base de donnĂ©es'); diff --git a/backend/src/utils/responses.ts b/backend/src/shared/http/responses.ts similarity index 100% rename from backend/src/utils/responses.ts rename to backend/src/shared/http/responses.ts diff --git a/backend/src/utils/billetweb.ts b/backend/src/shared/integrations/billetweb.ts similarity index 96% rename from backend/src/utils/billetweb.ts rename to backend/src/shared/integrations/billetweb.ts index a5135d8..9d5d86f 100644 --- a/backend/src/utils/billetweb.ts +++ b/backend/src/shared/integrations/billetweb.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { api_billetweb_token, api_billetweb_url, api_billetweb_respondent_students_list_id } from './secret'; +import { api_billetweb_token, api_billetweb_url, api_billetweb_respondent_students_list_id } from '../secrets/secrets'; import type { BilletwebUser } from '../../types/billetweb'; const headers = { diff --git a/backend/src/utils/siep.ts b/backend/src/shared/integrations/siep.ts similarity index 78% rename from backend/src/utils/siep.ts rename to backend/src/shared/integrations/siep.ts index 1dd7100..2b5e941 100644 --- a/backend/src/utils/siep.ts +++ b/backend/src/shared/integrations/siep.ts @@ -1,22 +1,26 @@ import axios from 'axios'; -import { api_utt_admis_url_ismajor, api_utt_auth_url, api_utt_password, api_utt_username } from "./secret"; +import { api_utt_admis_url_ismajor, api_utt_auth_url, api_utt_password, api_utt_username } from '../secrets/secrets'; export const getTokenUTTAPI = async () => { try { - const response = await axios.post(api_utt_auth_url, { - login: api_utt_username, - password: api_utt_password, - }, { - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - } - }); + const response = await axios.post( + api_utt_auth_url, + { + login: api_utt_username, + password: api_utt_password, + }, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + ); return response.data.token; } catch (error) { console.error('Error during POST request:', error); } -} +}; export const getNewStudentsFromUTTAPI_PAGE = async (token: string, date: string) => { const allNewStudents: any[] = []; @@ -25,7 +29,7 @@ export const getNewStudentsFromUTTAPI_PAGE = async (token: string, date: string) try { while (hasNextPage) { - const response = await axios.get(api_utt_admis_url_ismajor + date + "?page=" + currentPage, { + const response = await axios.get(api_utt_admis_url_ismajor + date + '?page=' + currentPage, { headers: { Authorization: `Bearer ${token}`, }, @@ -55,7 +59,6 @@ export const getNewStudentsFromUTTAPI_NOPAGE = async (token: string, date: strin const allNewStudents: any[] = []; try { - const response = await axios.get(api_utt_admis_url_ismajor + date, { headers: { Authorization: `Bearer ${token}`, diff --git a/backend/src/utils/secret.ts b/backend/src/shared/secrets/secrets.ts similarity index 100% rename from backend/src/utils/secret.ts rename to backend/src/shared/secrets/secrets.ts diff --git a/backend/src/utils/uploadDocuments.ts b/backend/src/shared/storage/uploadDocuments.ts similarity index 100% rename from backend/src/utils/uploadDocuments.ts rename to backend/src/shared/storage/uploadDocuments.ts diff --git a/backend/src/utils/pLimit.ts b/backend/src/shared/utils/pLimit.ts similarity index 100% rename from backend/src/utils/pLimit.ts rename to backend/src/shared/utils/pLimit.ts diff --git a/backend/src/utils/token.ts b/backend/src/shared/utils/token.ts similarity index 72% rename from backend/src/utils/token.ts rename to backend/src/shared/utils/token.ts index f77f74b..c2ea62f 100644 --- a/backend/src/utils/token.ts +++ b/backend/src/shared/utils/token.ts @@ -1,6 +1,6 @@ import { verify } from 'jsonwebtoken'; -import type { AuthTokenPayload } from '../types/auth'; -import { jwtSecret } from '../utils/secret'; +import type { AuthTokenPayload } from '../../types/auth'; +import { jwtSecret } from '../secrets/secrets'; export const decodeToken = (token: string): AuthTokenPayload | null => { if (!token) { diff --git a/backend/src/types/auth.ts b/backend/src/types/auth.d.ts similarity index 100% rename from backend/src/types/auth.ts rename to backend/src/types/auth.d.ts diff --git a/backend/types/billetweb.d.ts b/backend/src/types/billetweb.d.ts similarity index 100% rename from backend/types/billetweb.d.ts rename to backend/src/types/billetweb.d.ts diff --git a/backend/types/email.d.ts b/backend/src/types/email.d.ts similarity index 78% rename from backend/types/email.d.ts rename to backend/src/types/email.d.ts index 4e6c2fe..6688387 100644 --- a/backend/types/email.d.ts +++ b/backend/src/types/email.d.ts @@ -32,3 +32,14 @@ export interface TeamAssignmentEmailData extends TemplateData { } export type TemplatesRecordByNumber = Record; + +export type EmailPayload = { + subject: string; + templateName: string; + recipientsGroups?: string[]; + sendTo?: string[]; + html?: string; + title?: string; + content?: string; + text?: string; +}; diff --git a/backend/src/types/http.ts b/backend/src/types/http.d.ts similarity index 100% rename from backend/src/types/http.ts rename to backend/src/types/http.d.ts diff --git a/backend/types/permanence.d.ts b/backend/src/types/permanence.d.ts similarity index 100% rename from backend/types/permanence.d.ts rename to backend/src/types/permanence.d.ts diff --git a/backend/src/types/team.d.ts b/backend/src/types/team.d.ts new file mode 100644 index 0000000..01b0d0b --- /dev/null +++ b/backend/src/types/team.d.ts @@ -0,0 +1,35 @@ +export type TeamMemberRow = { + userId: number; +}; + +export type StudentRow = { + userId: number; + email: string; + branch: string; + male?: boolean; +}; + +export type TeamAssignmentNotification = { + email: string; + teamId: number; +}; + +export type TeamRow = { + teamId: number; + name: string; + description: string; + type: string; + socialLink: string; + riCompatible: boolean; +}; + +export type TeamDistributionState = TeamRow & { + size: number; + girlsCount: number; +}; + +export type TeamSizeRow = { + teamId: number; + teamName: string; + size: number; +}; diff --git a/backend/src/types/user.d.ts b/backend/src/types/user.d.ts new file mode 100644 index 0000000..c4ad664 --- /dev/null +++ b/backend/src/types/user.d.ts @@ -0,0 +1,19 @@ +export type VssQuestionnaireAnswer = { + id: number; + answer: string; + answerEn?: string; +}; + +export type VssQuestionnaireQuestion = { + id: number; + question: string; + questionEn?: string; + points: number; + type: 'single_choice' | 'multiple_choice'; + answers: VssQuestionnaireAnswer[]; +}; + +export type VssSubmissionAnswer = { + questionId: number; + answerIds: number[]; +}; diff --git a/frontend/src/components/home/emergencyModal.tsx b/frontend/src/components/home/emergencyModal.tsx index a9465f1..5413d05 100644 --- a/frontend/src/components/home/emergencyModal.tsx +++ b/frontend/src/components/home/emergencyModal.tsx @@ -6,15 +6,61 @@ import { createUserContactInformation, getCurrentUserOnboardingStatus } from '.. import { Button } from '../ui/button'; import { Input } from '../ui/input'; import Modal from '../ui/modal'; -import VssModal from './vssModal'; +import VssModal, { type Language } from './vssModal'; type FlowStep = 'idle' | 'loading' | 'emergency' | 'vss'; +const copy = { + fr: { + title: 'Formulaire VSS et Urgence', + toggleLanguage: 'Show in English', + intro: [ + "Bienvenue sur le site de l'intĂ©gration !", + "Nous sommes ravis de t'accueillir parmi nous Ă  l'UTT.", + "Durant ta premiĂšre semaine Ă  l'UTT, tu pourras participer aux activitĂ©s d'intĂ©gration. Afin que celle-ci se dĂ©roule dans les meilleures conditions, nous avons besoin que tu rĂ©pondes Ă  deux formulaires.", + "Dans ce premier formulaire, nous te demandons simplement de renseigner un contact d'urgence, au cas oĂč le moindre problĂšme surviendrait durant cette semaine.", + 'Tu peux quitter ce formulaire Ă  tout moment et le complĂ©ter plus tard. Cependant, il est obligatoire pour participer Ă  certaines activitĂ©s.', + ], + placeholders: { + name: "Nom du contact d'urgence", + phone: "TĂ©lĂ©phone du contact d'urgence", + }, + buttons: { + cancel: 'Annuler', + submit: 'Soumettre', + }, + statusError: 'Impossible de rĂ©cupĂ©rer le statut du formulaire.', + submitError: "Impossible d'enregistrer les informations d'urgence.", + }, + en: { + title: 'VSS and Emergency Form', + toggleLanguage: 'Voir en français', + intro: [ + 'Welcome to the integration website!', + 'We are happy to have you with us at UTT.', + 'During your first week at UTT, you will be able to take part in the integration activities. To make sure everything goes as smoothly as possible, we need you to answer two forms.', + 'In this first form, we simply ask you to provide an emergency contact in case the slightest issue happens during this week.', + 'You can leave this form at any time and complete it later. However, it is mandatory to participate in certain activities.', + ], + placeholders: { + name: 'Emergency contact name', + phone: 'Emergency contact phone number', + }, + buttons: { + cancel: 'Cancel', + submit: 'Submit', + }, + statusError: 'Unable to fetch the form status.', + submitError: 'Unable to save the emergency contact information.', + }, +} as const; + function EmergencyModal() { const [searchParams, setSearchParams] = useSearchParams(); const [form, setForm] = useState({ emergency_contact_name: '', emergency_contact_phone: '' }); const [flowStep, setFlowStep] = useState('idle'); const [error, setError] = useState(null); + const [language, setLanguage] = useState('fr'); const token = getToken(); const decodedToken = useMemo(() => (token ? decodeToken(token) : null), [token]); const userPermission = decodedToken?.userPermission; @@ -31,6 +77,7 @@ function EmergencyModal() { setFlowStep('idle'); setForm({ emergency_contact_name: '', emergency_contact_phone: '' }); setError(null); + setLanguage('fr'); return; } @@ -62,7 +109,7 @@ function EmergencyModal() { } catch { if (!cancelled) { setFlowStep('emergency'); - setError('Impossible de rĂ©cupĂ©rer le statut du formulaire.'); + setError(copy[language].statusError); } } }; @@ -81,6 +128,7 @@ function EmergencyModal() { setFlowStep('idle'); setForm({ emergency_contact_name: '', emergency_contact_phone: '' }); setError(null); + setLanguage('fr'); }; const handleContactSubmit = async () => { @@ -91,53 +139,51 @@ function EmergencyModal() { window.dispatchEvent(new Event('user-onboarding-updated')); setFlowStep('vss'); } catch { - setError("Impossible d'enregistrer les informations d'urgence."); + setError(copy[language].submitError); } }; + const localizedCopy = copy[language]; + return ( <>
-

Bienvenue sur le site de l'intégration !

-

Nous sommes ravis de t'accueillir parmi nous Ă  l'UTT.

-

- Durant ta premiÚre semaine à l'UTT, tu pourras participer aux activités d'intégration. Afin que - celle-ci se déroule dans les meilleures conditions, nous avons besoin que tu répondes à deux - formulaires. -

-

- Dans ce premier formulaire, nous te demandons simplement de renseigner un contact d'urgence, au - cas oĂč le moindre problĂšme surviendrait durant cette semaine. -

-

- Tu peux quitter ce formulaire à tout moment et le compléter plus tard. Cependant, il est - obligatoire pour participer à certaines activités. -

+
+ +
+ + {localizedCopy.intro.map((paragraph) => ( +

{paragraph}

+ ))} {error && (

{error}

)} setForm({ ...form, emergency_contact_name: e.target.value })} /> setForm({ ...form, emergency_contact_phone: e.target.value })} />
- +
@@ -145,6 +191,8 @@ function EmergencyModal() { setLanguage((currentLanguage) => (currentLanguage === 'fr' ? 'en' : 'fr'))} onSubmitted={() => { window.dispatchEvent(new Event('user-onboarding-updated')); }} diff --git a/frontend/src/components/home/vssModal.tsx b/frontend/src/components/home/vssModal.tsx index e5d7642..d6f2277 100644 --- a/frontend/src/components/home/vssModal.tsx +++ b/frontend/src/components/home/vssModal.tsx @@ -5,9 +5,76 @@ import { getVssQuestionnaire, submitVssQuestionnaire } from '../../services/requ import { Button } from '../ui/button'; import Modal from '../ui/modal'; +export type Language = 'fr' | 'en'; + +const copy = { + fr: { + title: 'Questionnaire VSS', + toggleLanguage: 'Show in English', + intro: [ + 'Dans ce questionnaire, tu devras rĂ©pondre aux questions ci-dessous Ă  propos des Violences Sexistes et Sexuelles (VSS).', + "La note est sur 14 et tu disposes de deux essais pour obtenir au moins 7 points. Cette sensibilisation est trĂšs importante pour nous afin de nous assurer que l'intĂ©gration se dĂ©roule dans les meilleures conditions pour tout le monde.", + "Si tu n'arrives pas Ă  obtenir la moyenne aprĂšs deux tentatives, nous serons malheureusement contraints de te refuser l'accĂšs Ă  la SoirĂ©e et au Week-end d'intĂ©gration, car ce sont les moments oĂč la majoritĂ© des situations de VSS se produisent.", + 'Tu peux quitter ce questionnaire Ă  tout moment et le complĂ©ter plus tard. Cependant, il est obligatoire pour participer Ă  certaines activitĂ©s.', + ], + answerGuidance: { + single: 'Choisis une seule rĂ©ponse.', + multiple: 'Tu peux sĂ©lectionner plusieurs rĂ©ponses.', + }, + loadingError: 'Impossible de charger le questionnaire VSS.', + submitError: 'Impossible d’envoyer le questionnaire VSS.', + unansweredError: "RĂ©ponds Ă  toutes les questions avant d'envoyer le questionnaire.", + emptyState: 'Aucun questionnaire disponible pour le moment.', + progress: (answered: number, total: number) => `${answered}/${total} questions rĂ©pondues`, + points: (totalPoints: number) => `${totalPoints} points possibles`, + submit: 'Soumettre le questionnaire', + submitting: 'Envoi...', + cancel: 'Annuler', + close: 'Fermer', + status: { + validated: 'Questionnaire validĂ©. Tu peux fermer cette fenĂȘtre.', + toretry: 'Le rĂ©sultat nĂ©cessite une seconde tentative. Tu pourras retenter plus tard.', + rejected: 'Le nombre de tentatives autorisĂ©es est atteint.', + pending: '', + }, + }, + en: { + title: 'VSS Questionnaire', + toggleLanguage: 'Voir en français', + intro: [ + 'In this questionnaire, you will need to answer the questions below about sexist and sexual violence (VSS).', + 'The score is out of 14 and you have two tries to get at least 7 points. This awareness step is very important to us so that the integration runs in the best possible conditions for everyone.', + 'If you do not reach the passing score after two attempts, we will unfortunately have to deny you access to the Party and the Integration Weekend, because these are the moments where most VSS situations happen.', + 'You can leave this questionnaire at any time and complete it later. However, it is mandatory to participate in certain activities.', + ], + answerGuidance: { + single: 'Choose one answer only.', + multiple: 'You can select multiple answers.', + }, + loadingError: 'Unable to load the VSS questionnaire.', + submitError: 'Unable to submit the VSS questionnaire.', + unansweredError: 'Answer all the questions before submitting the questionnaire.', + emptyState: 'No questionnaire is available right now.', + progress: (answered: number, total: number) => `${answered}/${total} questions answered`, + points: (totalPoints: number) => `${totalPoints} possible points`, + submit: 'Submit questionnaire', + submitting: 'Submitting...', + cancel: 'Cancel', + close: 'Close', + status: { + validated: 'Questionnaire validated. You can close this window.', + toretry: 'The result requires a second attempt. You will be able to try again later.', + rejected: 'The maximum number of attempts has been reached.', + pending: '', + }, + }, +} as const; + interface VssModalProps { visible: boolean; onCancel: () => void; + language: Language; + onToggleLanguage: () => void; onSubmitted?: (result: VssSubmissionResponse) => void; } @@ -20,20 +87,24 @@ const getAnswerClassName = (selected: boolean) => const VssQuestionBlock = ({ question, + language, selectedAnswerIds, onSelect, }: { question: VssQuestionnaireQuestion; + language: Language; selectedAnswerIds: number[]; onSelect: (questionId: number, answerId: number, type: VssQuestionnaireQuestion['type']) => void; }) => { + const localizedQuestion = language === 'en' && question.questionEn ? question.questionEn : question.question; + return (

Question {question.id}

- {question.question} + {localizedQuestion}

@@ -44,6 +115,7 @@ const VssQuestionBlock = ({
{question.answers.map((answer) => { const isSelected = selectedAnswerIds.includes(answer.id); + const localizedAnswer = language === 'en' && answer.answerEn ? answer.answerEn : answer.answer; return ( ); })}

{question.type === 'single_choice' - ? 'Choisis une seule réponse.' - : 'Tu peux sélectionner plusieurs réponses.'} + ? copy[language].answerGuidance.single + : copy[language].answerGuidance.multiple}

); }; -function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { +function VssModal({ visible, onCancel, onSubmitted, language, onToggleLanguage }: VssModalProps) { const [questions, setQuestions] = useState([]); const [selectedAnswers, setSelectedAnswers] = useState>({}); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); + const localizedCopy = copy[language]; useEffect(() => { if (!visible) { @@ -99,7 +172,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { setSelectedAnswers({}); } catch { if (!cancelled) { - setError('Impossible de charger le questionnaire VSS.'); + setError(localizedCopy.loadingError); } } }; @@ -143,7 +216,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { }); if (unansweredQuestions.length > 0) { - setError("RĂ©ponds Ă  toutes les questions avant d'envoyer le questionnaire."); + setError(localizedCopy.unansweredError); return; } @@ -157,7 +230,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { setResult(result); onSubmitted?.(result); } catch { - setError('Impossible d’envoyer le questionnaire VSS.'); + setError(localizedCopy.submitError); } finally { setSubmitting(false); } @@ -166,41 +239,25 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { const answeredCount = questions.filter((question) => (selectedAnswers[question.id] ?? []).length > 0).length; const totalQuestions = questions.length; - const statusMessage = - result?.status === 'validated' - ? 'Questionnaire validĂ©. Tu peux fermer cette fenĂȘtre.' - : result?.status === 'toretry' - ? 'Le rĂ©sultat nĂ©cessite une seconde tentative. Tu pourras retenter plus tard.' - : result?.status === 'rejected' - ? 'Le nombre de tentatives autorisĂ©es est atteint.' - : null; + const statusMessage = result ? localizedCopy.status[result.status] : null; return (
-

- Dans ce questionnaire, tu devras répondre aux questions ci-dessous à propos des Violences Sexistes - et Sexuelles (VSS). -

-

- La note est sur 14 et tu disposes de deux essais pour obtenir au moins 7 points. Cette - sensibilisation est trÚs importante pour nous afin de nous assurer que l'intégration se déroule dans - les meilleures conditions pour tout le monde. -

-

- Si tu n'arrives pas Ă  obtenir la moyenne aprĂšs deux tentatives, nous serons malheureusement - contraints de te refuser l'accĂšs Ă  la SoirĂ©e et au Week-end d'intĂ©gration, car ce sont les moments - oĂč la majoritĂ© des situations de VSS se produisent. -

-

- Tu peux quitter ce questionnaire à tout moment et le compléter plus tard. Cependant, il est - obligatoire pour participer à certaines activités. -

+
+ +
+ + {localizedCopy.intro.map((paragraph) => ( +

{paragraph}

+ ))} {error && (
@@ -229,11 +286,11 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { {questions.length > 0 && (
+ {localizedCopy.progress(answeredCount, totalQuestions)} - {answeredCount}/{totalQuestions} questions répondues - - - {questions.reduce((total, question) => total + question.points, 0)} points possibles + {localizedCopy.points( + questions.reduce((total, question) => total + question.points, 0), + )}
@@ -241,6 +298,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { @@ -250,20 +308,20 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { {questions.length === 0 && !error && (
- Aucun questionnaire disponible pour le moment. + {localizedCopy.emptyState}
)}
{result ? ( - + ) : ( <> )} diff --git a/frontend/src/interfaces/user.interface.ts b/frontend/src/interfaces/user.interface.ts index b53abce..5234bc2 100644 --- a/frontend/src/interfaces/user.interface.ts +++ b/frontend/src/interfaces/user.interface.ts @@ -31,11 +31,13 @@ export interface UserOnboardingStatus { export interface VssQuestionnaireAnswer { id: number; answer: string; + answerEn?: string; } export interface VssQuestionnaireQuestion { id: number; question: string; + questionEn?: string; points: number; type: 'single_choice' | 'multiple_choice'; answers: VssQuestionnaireAnswer[];