Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
671f17a
feat(front): contact informations in admin user page
Suboyyy Jul 6, 2026
7ad8db8
feat(back): add endpoint and service for retrieving user contact info…
Suboyyy Jul 6, 2026
3e152d3
fix(back): wrong table
Suboyyy Jul 6, 2026
4571f0b
feat(front): Add header when form isn't complete
Suboyyy Jul 6, 2026
4e53913
feat(front): add urgency modal (empty) and header button
Suboyyy Jul 11, 2026
5c34228
feat(back): add user contact information creation endpoints
Suboyyy Jul 12, 2026
4e3e1a3
feat: add form with answer sending
Suboyyy Jul 12, 2026
f791d7d
feat(back): billetweb requests
tuturd Jul 13, 2026
db81338
refactor(back): billetweb with API route created for us
tuturd Jul 13, 2026
cb214ae
fix: delete contact_CE in form
Suboyyy Jul 17, 2026
cc817fa
feat: add vssqcm schemas and seed
Suboyyy Jul 17, 2026
c086704
fix: database works
Suboyyy Jul 17, 2026
4523e4f
fix: made a lot of shit but seems good
Suboyyy Jul 17, 2026
fa6cadf
feat: implement vss form
Suboyyy Jul 18, 2026
8358ceb
feat: can't access to billetweb iframe if form not submissed or vss d…
Suboyyy Jul 18, 2026
8e9c9f2
refactor: introduction text in urgency and vss modal
Suboyyy Jul 18, 2026
3518f39
feat: add to billetweb if VSS accepted
Suboyyy Jul 18, 2026
282c6b4
feat: only student can now see form and be impacted
Suboyyy Jul 18, 2026
bee4ea7
fix: resolve conflict in database (hope don't break everything)
Suboyyy Jul 18, 2026
b3a7993
Merge branch 'dev' into feat/information-form
Suboyyy Jul 18, 2026
2ed250b
fix: pointing to right prev migration
Suboyyy Jul 18, 2026
810169c
fix(lint)
Suboyyy Jul 18, 2026
7cd3d23
refactor: update user controller methods to use AppRequestHandler type
Suboyyy Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ CAS_LOGIN_URL=
DATABASE_URL=postgresql://admin:password@localhost:5432/integration-dev
OUTSIDE_DATABASE_URL=postgresql://admin:password@localhost:5432/integration-dev

# Les variables Billetweb permettent d'autoriser l'achat de billets uniquement aux membres d'une liste d'adresses emails commandée par le site.
# En 2026, cette liste comprends automatiquement les Nouveaux et CE ayant remplis le formulaire "Contact de secours" et le questionnaire VSS.
#
# Le token se récupère sur la page https://www.billetweb.fr/bo/api.php#/, en étant préalablement connecté au compte billetweb de l'Intégration.
#
# L'ID de la liste d'accès se récupère sur la page Configuration > Offres Spréciales > Filtrage par liste > Cliquer sur la liste ; Il s'agit de largument "list_parent" dans l'URL.
# Exemple: https://www.billetweb.fr/bo/lists.php?list_parent=925 -> API_BILLETWEB_RESPONDENT_STUDENTS_LIST_ID=925

API_BILLETWEB_URL=https://www.billetweb.fr/api
API_BILLETWEB_TOKEN=Basic azerty
API_BILLETWEB_RESPONDENT_STUDENTS_LIST_ID=

JWT_SECRET=

POSTGRES_PASSWORD=password
Expand Down
16 changes: 8 additions & 8 deletions backend/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { defineConfig } from 'drizzle-kit'
import * as dotenv from "dotenv";
import { defineConfig } from 'drizzle-kit';
import * as dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
schema: "./src/schemas/*",
out: "./src/database/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.OUTSIDE_DATABASE_URL ?? ""
},
schema: './src/schemas/*',
out: './src/database/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.OUTSIDE_DATABASE_URL ?? '',
},
});
12 changes: 7 additions & 5 deletions backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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 { initQcmvss } from './src/database/initdb/initQcmvss';

dotenv.config();

Expand All @@ -46,6 +47,7 @@ async function startServer() {
await initRoles();
await initEvent();
await initChallenge();
await initQcmvss();
console.log('Base de données initialisée avec succès.');

// Utilisation des routes d'authentification
Expand All @@ -67,11 +69,11 @@ async function startServer() {
app.use('/api/discord', authenticateUser, discordRoutes);
app.use('/api/tent', authenticateUser, tentRoutes);
app.use('/api/bus', authenticateUser, busRoutes);
app.use("/api/uploads/news", express.static(path.join(__dirname, "/uploads/news")));
app.use("/api/uploads/notebooks", express.static(path.join(__dirname, "/uploads/notebooks")));
app.use("/api/uploads/foodmenu", express.static(path.join(__dirname, "/uploads/foodmenu")));
app.use("/api/uploads/plannings", express.static(path.join(__dirname, "/uploads/plannings")));
app.use("/api/exports/bus", express.static(path.join(__dirname, "/exports/bus")));
app.use('/api/uploads/news', express.static(path.join(__dirname, '/uploads/news')));
app.use('/api/uploads/notebooks', express.static(path.join(__dirname, '/uploads/notebooks')));
app.use('/api/uploads/foodmenu', express.static(path.join(__dirname, '/uploads/foodmenu')));
app.use('/api/uploads/plannings', express.static(path.join(__dirname, '/uploads/plannings')));
app.use('/api/exports/bus', express.static(path.join(__dirname, '/exports/bus')));

// Démarrage du serveur
app.listen(server_port, () => {
Expand Down
56 changes: 56 additions & 0 deletions backend/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AdminCreateUserDto, PermissionParams, ProfileBody, SyncBody, UserIdParams } from '../dto/user.dto';
import * as user_service from '../services/user.service';
import { Error, Ok } from '../utils/responses';
import { type UserContactInformation } from '../../types/user';
import type { AppRequestHandler } from '../types/http';

export const getUsersAdmin: AppRequestHandler = async (_req, res) => {
Expand Down Expand Up @@ -66,6 +67,61 @@ export const getCurrentUser: AppRequestHandler = async (req, res) => {
}
};

export const getUserContactInformation: AppRequestHandler = async (req, res) => {
const { userId } = req.params;

try {
const userContactInfo = await user_service.getUserContactInformation(parseInt(userId));
Ok(res, { data: userContactInfo });
} catch {
Error(res, { msg: "Erreur lors de la récupération des informations de contact de l'utilisateur." });
}
};

export const createUserContactInformation: AppRequestHandler<UserContactInformation> = async (req, res) => {
const userId = req.user?.userId;
const contact = req.body;

try {
const result = await user_service.createUserContactInformation(userId, contact);
Ok(res, { msg: 'Informations de contact créées', data: result });
} catch {
Error(res, { msg: 'Erreur lors de la création des informations de contact.' });
}
};

export const getCurrentUserOnboardingStatus: AppRequestHandler = async (req, res) => {
const userId = req.user?.userId;

try {
const status = await user_service.getCurrentUserOnboardingStatus(userId);
Ok(res, { data: status });
} catch {
Error(res, { msg: "Erreur lors de la récupération du statut d'onboarding." });
}
};

export const getVssQuestionnaire: AppRequestHandler = async (req, res) => {
try {
const questionnaire = await user_service.getVssQuestionnaire();
Ok(res, { data: questionnaire });
} catch {
Error(res, { msg: 'Erreur lors de la récupération du questionnaire VSS.' });
}
};

export const submitVssQuestionnaire: AppRequestHandler<user_service.VssSubmissionPayload> = async (req, res) => {
const userId = req.user?.userId;
const payload = req.body;

try {
const result = await user_service.submitVssQuestionnaire(userId, payload);
Ok(res, { data: result });
} catch {
Error(res, { msg: 'Erreur lors de la soumission du questionnaire VSS.' });
}
};

export const updateProfile: AppRequestHandler<ProfileBody> = async (req, res) => {
const userId = req.user?.userId;
const { branch, contact } = req.body;
Expand Down
215 changes: 215 additions & 0 deletions backend/src/database/initdb/initQcmvss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { db } from '../db';
import { vssqcmanswerSchema } from '../../schemas/Relational/vssqcmanswer.schema';
import { vssqcmquestionSchema } from '../../schemas/Basic/vssqcmquestion.schema';

type SeedQuestion = {
question: string;
points: number;
type: 'single_choice' | 'multiple_choice';
answers: {
answer: string;
is_correct: boolean;
}[];
};

const qcmQuestions: SeedQuestion[] = [
{
question: 'Oui = ',
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 },
],
},
{
question: 'Non = ',
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 },
],
},
{
question: 'En résumé, le consentement',
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: "spécifique, enthousiaste; valable quand la personne chancèle sous l'effet de l'alcool",
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 pendant le bang. B était complètement bourré. Il s'agit d'une agression sexuelle. La prise d'alcool est alors une condition :",
points: 1,
type: 'single_choice',
answers: [
{ answer: 'Aggravante', is_correct: true },
{ answer: 'Atténuante', is_correct: false },
],
},
{
question: 'Parmi les situations suivantes, lesquelles sont des agressions sexuelles :',
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 },
],
},
{
question: "Un.e de tes amis touche les fesses de B et l'enlace. B a un mouvement de recul. Que peux-tu faire ?",
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: "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.",
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 ?",
points: 1,
type: 'multiple_choice',
answers: [
{ answer: 'Dans la Safe Zone', is_correct: true },
{ answer: 'Aux personnes des confiance', is_correct: true },
{ answer: 'Au stand de prévention', is_correct: true },
{ answer: 'A la team prévention', is_correct: true },
{ answer: 'Aux super orgas', is_correct: true },
{ answer: "A tes chefs d'équipe", is_correct: true },
{ answer: 'A ta marraine / A ton parrain', is_correct: true },
{ answer: 'À un•e ami•e', is_correct: true },
],
},
{
question:
'En cas de VSS, quelles sont les peines maximales légalement encourue par une personne ayant commis une agression sexuelle ?',
points: 1,
type: 'single_choice',
answers: [
{ answer: "75 000 € d'amende et 5 ans d'emprisonnemen", is_correct: true },
{ answer: "10 000€ d'amende", is_correct: false },
{ answer: '15 ans de prison', is_correct: false },
],
},
{
question: 'Quelles sont les conséquences possibles pour la victime de VSS ?',
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 },
],
},
{
question: 'Que puis-je faire si je suis témoins de 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 },
],
},
{
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.",
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 },
],
},
{
question: "Qu'est-ce qui est considéré comme un acte de bizutage (et qui est donc interdit) ? ",
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: 'Organiser une chasse au trésor géante à travers toute la ville pour les nouveaux',
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 },
],
},
{
question: "A quelles sanctions s'expose l'auteur du bizutage ?",
points: 1,
type: 'multiple_choice',
answers: [
{ answer: "Une exclusion de l'intégration", is_correct: true },
{ answer: 'Rien du tout', is_correct: false },
{
answer: "Le bizutage est un délit. Il est puni de 6 mois d'emprisonnement et de 7 500 € d'amende.",
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 },
],
},
];

export const initQcmvss = async () => {
const existingQuestion = await db.select().from(vssqcmquestionSchema).limit(1);

if (existingQuestion.length > 0) {
return;
}

for (const seedQuestion of qcmQuestions) {
const [createdQuestion] = await db
.insert(vssqcmquestionSchema)
.values({
question: seedQuestion.question,
points: seedQuestion.points,
type: seedQuestion.type,
})
.returning({ id: vssqcmquestionSchema.id });

if (!createdQuestion) {
throw new Error(`Question not inserted: ${seedQuestion.question}`);
}

await db.insert(vssqcmanswerSchema).values(
seedQuestion.answers.map((seedAnswer) => ({
questionid: createdQuestion.id,
answer: seedAnswer.answer,
is_correct: seedAnswer.is_correct,
})),
);
}
};
8 changes: 8 additions & 0 deletions backend/src/database/migrations/0024_even_morg.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE "user_informations" (
"user_id" integer PRIMARY KEY NOT NULL,
"urgency_contact_name" text,
"urgency_contact_phone" text,
"contact_CE" text
);
--> statement-breakpoint
ALTER TABLE "user_informations" ADD CONSTRAINT "user_informations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "user_informations" DROP COLUMN "contact_CE";
18 changes: 18 additions & 0 deletions backend/src/database/migrations/0026_high_wind_dancer.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TYPE "public"."vss_form" AS ENUM('pending', 'validated', 'rejected');--> statement-breakpoint
CREATE TYPE "public"."question_type" AS ENUM('single_choice', 'multiple_choice');--> statement-breakpoint
CREATE TABLE "vssqcmquestion" (
"id" serial PRIMARY KEY NOT NULL,
"question" text NOT NULL,
"points" integer NOT NULL,
"type" "question_type" NOT NULL
);
--> statement-breakpoint
CREATE TABLE "vssqcmanswer" (
"id" serial PRIMARY KEY NOT NULL,
"questionid" integer NOT NULL,
"answer" text NOT NULL,
"is_correct" boolean NOT NULL
);
--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "vss_form" "vss_form" DEFAULT 'pending';--> statement-breakpoint
ALTER TABLE "vssqcmanswer" ADD CONSTRAINT "vssqcmanswer_questionid_vssqcmquestion_id_fk" FOREIGN KEY ("questionid") REFERENCES "public"."vssqcmquestion"("id") ON DELETE cascade ON UPDATE no action;
1 change: 1 addition & 0 deletions backend/src/database/migrations/0027_good_unus.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE "public"."vss_form" ADD VALUE 'toretry' BEFORE 'validated';
Loading
Loading