diff --git a/.gitignore b/.gitignore index 47046e0..7948990 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,12 @@ backend/uploads/ backend/exports/ backend/dist +# Autres dossiers à ignorer +.vscode/ + # Autres fichiers à ignorer .DS_Store *.log *.env +*.pem google_credentials.json diff --git a/backend/server.ts b/backend/server.ts index 02128c2..0847051 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -3,7 +3,7 @@ import cors from 'cors'; import express from 'express'; import dotenv from 'dotenv'; -import path from "path"; +import path from 'path'; import { initChallenge } from './src/database/initdb/initChallenge'; import { initUser } from './src/database/initdb/initUser'; @@ -27,6 +27,7 @@ import roleRoutes from './src/routes/role.routes'; 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'; dotenv.config(); @@ -35,7 +36,7 @@ async function startServer() { const app = express(); // Configuration des middlewares - app.use(cors({ origin: "*" })); + app.use(cors({ origin: '*' })); app.use(bodyParser.json()); app.use(express.urlencoded({ extended: true })); @@ -48,16 +49,17 @@ async function startServer() { console.log('Base de données initialisée avec succès.'); // Utilisation des routes d'authentification - app.use('/api', defaultRoute) + app.use('/api', defaultRoute); app.use('/api/auth', authRoutes); app.use('/api/automation', authenticateAutomation, automationRoutes); app.use('/api/authadmin', authenticateUser, authRoutes); + app.use('/api/banned', authenticateUser, bannedRoutes); app.use('/api/role', authenticateUser, roleRoutes); app.use('/api/user', authenticateUser, userRoutes); app.use('/api/team', authenticateUser, teamRoutes); app.use('/api/event', authenticateUser, eventRoutes); app.use('/api/faction', authenticateUser, factionRoutes); - app.use('/api/imexport', authenticateUser, imexportRouter) + app.use('/api/imexport', authenticateUser, imexportRouter); app.use('/api/permanence', authenticateUser, permanenceRoutes); app.use('/api/challenge', authenticateUser, challengeRoutes); app.use('/api/email', authenticateUser, emailRoutes); @@ -76,8 +78,8 @@ async function startServer() { console.log(`Server running on port ${server_port}`); }); } catch (err) { - console.error('Erreur lors de l\'initialisation de la base de données :', err); - process.exit(1); // Arrêter le serveur si l'initialisation échoue + console.error("Erreur lors de l'initialisation de la base de données :", err); + process.exit(1); // Arrêter le serveur si l'initialisation échoue } } diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 6e5cf8a..f82748b 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -6,6 +6,7 @@ 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'; +import * as banned_service from '../services/banned.service'; import * as email_service from '../services/email.service'; import * as registration_service from '../services/registration.service'; import * as role_service from '../services/role.service'; @@ -52,6 +53,12 @@ export const handlecasticket = async (req: Request, res: Response) => { // Assurez-vous que user.email est un string let user = await user_service.getUserByEmail(CASuser.email.toLowerCase()); if (!user) { + const isBanned = await banned_service.getBannedByEmail(CASuser.email); + + if (isBanned) { + Unauthorized(res, { msg: 'Unauthorized: user email prohibited' }); + } + const password = bigInt.randBetween(bigInt(2).pow(255), bigInt(2).pow(256).minus(1)).toString(); await user_service.createUser( CASuser.givenName, diff --git a/backend/src/controllers/banned.controller.ts b/backend/src/controllers/banned.controller.ts new file mode 100644 index 0000000..60a45b1 --- /dev/null +++ b/backend/src/controllers/banned.controller.ts @@ -0,0 +1,41 @@ +import { type Request, type Response } from 'express'; +import * as banned_service from '../services/banned.service'; +import { Error, Ok } from '../utils/responses'; + +export const addBanned = async (req: Request, res: Response) => { + const { email } = req.body; + + try { + const banned = await banned_service.addBanned(email); + Ok(res, { data: banned }); + } catch (err) { + console.error(err); + Error(res, { msg: "Erreur lors de l'ajout de l'addresses bannie." }); + } +}; + +export const removeBanned = async (req: Request, res: Response) => { + const id = Number(req.params.id); + + if (isNaN(id)) { + return Error(res, { msg: 'ID invalide.' }); + } + + try { + const banned = await banned_service.removeBanned(id); + Ok(res, { data: banned }); + } catch (err) { + console.error(err); + Error(res, { msg: "Erreur lors de la suppression de l'address bannie." }); + } +}; + +export const getAllBanned = async (req: Request, res: Response) => { + try { + const banned = await banned_service.getAllBanned(); + Ok(res, { data: banned }); + } catch (err) { + console.error(err); + Error(res, { msg: 'Erreur lors de la récupération des addresses bannies.' }); + } +}; diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index d7008dd..e4f8d61 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -1,11 +1,7 @@ -import bcrypt from 'bcryptjs'; import { type Request, type Response } from 'express'; -import * as randomstring from 'randomstring'; -import * as auth_service from '../services/auth.service'; +import type { AdminCreateUserDto } from '../dto/user.dto'; import * as user_service from '../services/user.service'; -import { noSyncEmails } from '../utils/no_sync_list'; import { Error, Ok } from '../utils/responses'; -import * as SIEP_Utils from '../utils/siep'; export const getUsersAdmin = async (req: Request, res: Response) => { try { @@ -49,28 +45,11 @@ export const syncNewstudent = async (req: Request, res: Response) => { const { date } = req.body; try { - const token = await SIEP_Utils.getTokenUTTAPI(); - const newStudents = await SIEP_Utils.getNewStudentsFromUTTAPI_NOPAGE(token, date); - const newStudentfiltered = newStudents.filter((student: any) => !noSyncEmails.includes(student.email)); //Nouveau à ne pas sync (Démissionnaires, etc) - - for (const element of newStudentfiltered) { - const userInDb = await user_service.getUserByEmail(element.email.toLowerCase()); - if (userInDb === undefined) { - const tmpPassword = await bcrypt.hash(randomstring.generate(48), 10); - const newUser = await user_service.createUser( - element.prenom, - element.nom, - element.email.toLowerCase(), - element.Majeur, - 'Nouveau', - element.diplome === 'MA' ? 'Master' : element.specialite, - tmpPassword, - ); - - await auth_service.createRegistrationToken(newUser.id); - } - } - Ok(res, { msg: 'All NewStudent created and synced' }); + await user_service.syncNewStudents(date); + + Ok(res, { + msg: 'All NewStudent created and synced', + }); } catch (error) { Error(res, { error }); } @@ -111,6 +90,19 @@ export const adminUpdateUser = async (req: Request, res: Response) => { } }; +export const adminCreateUser = async (req: Request, res: Response) => { + try { + const user = await user_service.adminCreateUser(req.body); + + Ok(res, { + msg: 'Utilisateur créé', + data: user, + }); + } catch (err) { + Error(res, { msg: err.message }); + } +}; + export const adminDeleteUser = async (req: Request, res: Response) => { const { userId } = req.params; diff --git a/backend/src/database/migrations/0023_zippy_colonel_america.sql b/backend/src/database/migrations/0023_zippy_colonel_america.sql new file mode 100644 index 0000000..f4254d5 --- /dev/null +++ b/backend/src/database/migrations/0023_zippy_colonel_america.sql @@ -0,0 +1,5 @@ +CREATE TABLE "banned_addresses" ( + "id" serial PRIMARY KEY NOT NULL, + "email" text, + CONSTRAINT "banned_addresses_email_unique" UNIQUE("email") +); diff --git a/backend/src/database/migrations/meta/0023_snapshot.json b/backend/src/database/migrations/meta/0023_snapshot.json new file mode 100644 index 0000000..54df657 --- /dev/null +++ b/backend/src/database/migrations/meta/0023_snapshot.json @@ -0,0 +1,1317 @@ +{ + "id": "128e2d49-22e7-477d-a202-ae518c2e21ed", + "prevId": "93925edf-9622-4ce2-80bc-26050abbce0b", + "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 + } + }, + "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 + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "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.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 + } + }, + "enums": {}, + "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 6924d8e..7c107c1 100644 --- a/backend/src/database/migrations/meta/_journal.json +++ b/backend/src/database/migrations/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1757002717384, "tag": "0022_light_omega_red", "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1782943789540, + "tag": "0023_zippy_colonel_america", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/dto/user.dto.ts b/backend/src/dto/user.dto.ts new file mode 100644 index 0000000..fd1a841 --- /dev/null +++ b/backend/src/dto/user.dto.ts @@ -0,0 +1,8 @@ +export interface AdminCreateUserDto { + firstName: string; + lastName: string; + email: string; + major: boolean; + branch: string; + withNotification: boolean; +} diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 560a5f4..96b6893 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -6,16 +6,16 @@ const authRouter = express.Router(); // Route d'inscription authRouter.post('/register', authController.register); -authRouter.post("/completeregistration", authController.completeRegistration); -authRouter.get('/handlecasticket', authController.handlecasticket) +authRouter.post('/completeregistration', authController.completeRegistration); +authRouter.get('/handlecasticket', authController.handlecasticket); // Route de connexion authRouter.post('/login', authController.login); -authRouter.get("/istokenvalid", authController.isTokenValid); -authRouter.post('/resetpassworduser', authController.resetPasswordUser) -authRouter.post('/requestpassworduser', authController.requestPasswordUser) +authRouter.get('/istokenvalid', authController.isTokenValid); +authRouter.post('/resetpassworduser', authController.resetPasswordUser); +authRouter.post('/requestpassworduser', authController.requestPasswordUser); //Admin reset token -authRouter.post('/admin/renewtoken', checkRole("Admin", []), authController.renewToken); +authRouter.post('/admin/renewtoken', checkRole('Admin', []), authController.renewToken); export default authRouter; diff --git a/backend/src/routes/banned.routes.ts b/backend/src/routes/banned.routes.ts new file mode 100644 index 0000000..2d9ca9c --- /dev/null +++ b/backend/src/routes/banned.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import * as bannedController from '../controllers/banned.controller'; +import { checkRole } from '../middlewares/user.middleware'; + +const bannedRouter = Router(); + +bannedRouter.post('/admin', checkRole('Admin', []), bannedController.addBanned); +bannedRouter.delete('/admin/:id', checkRole('Admin', []), bannedController.removeBanned); +bannedRouter.get('/admin/all', checkRole('Admin', []), bannedController.getAllBanned); + +export default bannedRouter; diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts index ae2f9d2..fe9f230 100644 --- a/backend/src/routes/user.routes.ts +++ b/backend/src/routes/user.routes.ts @@ -5,11 +5,12 @@ import { checkRole } from '../middlewares/user.middleware'; const userRouter = express.Router(); // Admin routes -userRouter.get('/admin/getusersbypermission', checkRole("Admin", []), userController.getUsersByPermission); -userRouter.patch('/admin/user/:userId', checkRole("Admin", []), userController.adminUpdateUser); -userRouter.delete('/admin/user/:userId', checkRole("Admin", []), userController.adminDeleteUser); -userRouter.get('/admin/getusers', checkRole("Admin", ["Respo CE"]), userController.getUsersAdmin); -userRouter.post('/admin/syncnewstudent', checkRole("Admin", []), userController.syncNewstudent); +userRouter.get('/admin/getusersbypermission', checkRole('Admin', []), userController.getUsersByPermission); +userRouter.post('/admin/user', checkRole('Admin', []), userController.adminCreateUser); +userRouter.patch('/admin/user/:userId', checkRole('Admin', []), userController.adminUpdateUser); +userRouter.delete('/admin/user/:userId', checkRole('Admin', []), userController.adminDeleteUser); +userRouter.get('/admin/getusers', checkRole('Admin', ['Respo CE']), userController.getUsersAdmin); +userRouter.post('/admin/syncnewstudent', checkRole('Admin', []), userController.syncNewstudent); // User routes userRouter.patch('/user/me', userController.updateProfile); diff --git a/backend/src/schemas/Basic/banned-addresses.ts b/backend/src/schemas/Basic/banned-addresses.ts new file mode 100644 index 0000000..3bc7c7e --- /dev/null +++ b/backend/src/schemas/Basic/banned-addresses.ts @@ -0,0 +1,8 @@ +import { pgTable, serial, text } from 'drizzle-orm/pg-core'; + +export const bannedAddressesSchema = pgTable('banned_addresses', { + id: serial('id').primaryKey(), + email: text('email').unique(), +}); + +export type BannedAddress = typeof bannedAddressesSchema.$inferSelect; diff --git a/backend/src/services/banned.service.ts b/backend/src/services/banned.service.ts new file mode 100644 index 0000000..d7ced07 --- /dev/null +++ b/backend/src/services/banned.service.ts @@ -0,0 +1,51 @@ +import { db } from '../database/db'; +import { bannedAddressesSchema } from '../schemas/Basic/banned-addresses'; +import { userSchema } from '../schemas/Basic/user.schema'; +import { busAttributionSchema } from '../schemas/Relational/busattribution.schema'; +import { challengeValidationSchema } from '../schemas/Relational/challengevalidation.schema'; +import { registrationSchema } from '../schemas/Relational/registration.schema'; +import { respoPermanenceSchema } from '../schemas/Relational/userpermanences.schema'; +import { userPermanenceSchema } from '../schemas/Relational/userpermanences.schema'; +import { userPreferencesSchema, userRolesSchema } from '../schemas/Relational/userroles.schema'; +import { userTeamsSchema } from '../schemas/Relational/userteams.schema'; +import { userTentSchema } from '../schemas/Relational/usertent.schema'; +import { eq } from 'drizzle-orm'; + +export const addBanned = async (email: string) => { + const user = (await db.select().from(userSchema).where(eq(userSchema.email, email)))[0]; + + if (user) { + // TODO: à simplifier une fois que les éléments en db seront en CASCADE + await db.delete(busAttributionSchema).where(eq(busAttributionSchema.user_id, user.id)); + await db.delete(challengeValidationSchema).where(eq(challengeValidationSchema.target_user_id, user.id)); + await db.delete(registrationSchema).where(eq(registrationSchema.user_id, user.id)); + await db.delete(respoPermanenceSchema).where(eq(respoPermanenceSchema.user_id, user.id)); + await db.delete(userPermanenceSchema).where(eq(userPermanenceSchema.user_id, user.id)); + await db.delete(userPreferencesSchema).where(eq(userPreferencesSchema.userId, user.id)); + await db.delete(userRolesSchema).where(eq(userRolesSchema.user_id, user.id)); + await db.delete(userTeamsSchema).where(eq(userTeamsSchema.user_id, user.id)); + await db.delete(userTentSchema).where(eq(userTentSchema.user_id_1, user.id)); + await db.delete(userTentSchema).where(eq(userTentSchema.user_id_2, user.id)); + await db.delete(userSchema).where(eq(userSchema.id, user.id)); + } + + const result = await db.insert(bannedAddressesSchema).values({ email }).onConflictDoNothing().returning(); + + return Array.isArray(result) && result.length > 0 ? result[0] : null; +}; + +export const removeBanned = async (id: number) => { + const result = await db.delete(bannedAddressesSchema).where(eq(bannedAddressesSchema.id, id)).returning(); + + return Array.isArray(result) && result.length > 0 ? result[0] : null; +}; + +export const getAllBanned = async () => { + const result = await db.select().from(bannedAddressesSchema); + return result; +}; + +export const getBannedByEmail = async (email: string) => { + const result = (await db.select().from(bannedAddressesSchema).where(eq(bannedAddressesSchema.email, email)))[0]; + return result; +}; diff --git a/backend/src/services/user.service.ts b/backend/src/services/user.service.ts index c457be9..1f98a04 100644 --- a/backend/src/services/user.service.ts +++ b/backend/src/services/user.service.ts @@ -1,11 +1,19 @@ 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 } from '../dto/user.dto'; import { type User, userSchema } from '../schemas/Basic/user.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 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 { generateEmailHtml, sendEmail } from './email.service'; +import { email_from } from '../utils/secret'; // Fonction pour récupérer un utilisateur par email export const getUserByEmail = async (email: string) => { @@ -13,15 +21,15 @@ export const getUserByEmail = async (email: string) => { const users = await db.select().from(userSchema).where(eq(userSchema.email, email)); return users[0]; } catch (err) { - console.error('Erreur lors de la récupération de l\'utilisateur par email:', err); + console.error("Erreur lors de la récupération de l'utilisateur par email:", err); throw new Error('Erreur de base de données'); } }; export const getUserById = async (userId: number) => { try { - const user = await db.select( - { + const user = await db + .select({ userId: userSchema.id, firstName: userSchema.first_name, lastName: userSchema.last_name, @@ -30,16 +38,51 @@ export const getUserById = async (userId: number) => { branch: userSchema.branch, contact: userSchema.contact, permission: userSchema.permission, - discord_id: userSchema.discord_id - } - ).from(userSchema).where(eq(userSchema.id, userId)); + discord_id: userSchema.discord_id, + }) + .from(userSchema) + .where(eq(userSchema.id, userId)); return user[0]; } catch (err) { - console.error('Erreur lors de la récupération de l\'utilisateur par email:', err); + console.error("Erreur lors de la récupération de l'utilisateur par email:", err); throw new Error('Erreur de base de données'); } }; +export const syncNewStudents = async (data: string) => { + const token = await SIEP_Utils.getTokenUTTAPI(); + + const newStudents = await SIEP_Utils.getNewStudentsFromUTTAPI_NOPAGE(token, data); + + const noSyncEmails = await Banned_Service.getAllBanned().then((bannedList) => + bannedList.map((banned) => banned.email), + ); + + const filteredStudents = newStudents.filter((student: any) => !noSyncEmails.includes(student.email)); + + for (const student of filteredStudents) { + const userInDb = await getUserByEmail(student.email.toLowerCase()); + + if (!userInDb) { + const tmpPassword = randomstring.generate(48); + + const newUser = await createUser( + student.prenom, + student.nom, + student.email.toLowerCase(), + student.Majeur, + 'Nouveau', + student.diplome === 'MA' ? 'Master' : student.specialite, + tmpPassword, + ); + + await auth_service.createRegistrationToken(newUser.id); + } + } + + return filteredStudents.length; +}; + // Fonction pour enregistrer un nouvel utilisateur export const createUser = async ( firstName: string, @@ -48,7 +91,8 @@ export const createUser = async ( majeur: boolean, permission: string, branch: string, - password: string) => { + password: string, +) => { try { // Hacher le mot de passe const hashedPassword = await bcrypt.hash(password, 10); @@ -57,17 +101,17 @@ export const createUser = async ( first_name: firstName, last_name: lastName, email: email, - branch: branch === "CV_ING" ? "RI" : branch, + branch: branch === 'CV_ING' ? 'RI' : branch, majeur: majeur, password: hashedPassword, - permission: permission + permission: permission, }; // Insérer un nouvel utilisateur dans la base de données - const result = await db.insert(userSchema).values(newUser).returning() + const result = await db.insert(userSchema).values(newUser).returning(); return result[0]; } catch (err) { - console.error('Erreur lors de la création de l\'utilisateur:', err); + console.error("Erreur lors de la création de l'utilisateur:", err); throw new Error('Erreur de base de données'); } }; @@ -79,24 +123,75 @@ export const comparePassword = async (enteredPassword: string, storedPassword: s export const updateUserStudent = async (firstName: string, lastName: string, email: string) => { try { - const result = await db.update(userSchema) + const result = await db + .update(userSchema) .set({ first_name: firstName, - last_name: lastName + last_name: lastName, }) .where(eq(userSchema.email, email)); return result.rows[0]; } catch (err) { - console.error('Erreur lors de la récupération et de l\'update de l\'utilisateur par email:', err); + console.error("Erreur lors de la récupération et de l'update de l'utilisateur par email:", err); throw new Error('Erreur de base de données'); } -} +}; + +export const adminCreateUser = async (data: AdminCreateUserDto) => { + const email = data.email.toLowerCase(); + + const userInDb = await getUserByEmail(email); + + if (userInDb) { + throw new Error('Utilisateur déjà existant'); + } + + const isBanned = await Banned_Service.getBannedByEmail(email); + + if (isBanned) { + throw new Error('Adresse email bannie. Rendez vous dans la page Admin/Bannis.'); + } + + const tmpPassword = randomstring.generate(48); + + const newUser = await createUser( + data.firstName, + data.lastName, + email, + data.major, + 'Nouveau', + data.branch === 'MA' ? 'Master' : data.branch, + tmpPassword, + ); + + const registrationToken = await createRegistrationToken(newUser.id); + + if (data.withNotification) { + const htmlEmail = generateEmailHtml('templateWelcome', { + token: registrationToken, + }); + + const emailOptions = { + from: email_from, + to: [email], + cc: [], + bcc: [], + subject: `[EN BELOW] Bienvenue à l'UTT !`, + text: ``, + html: htmlEmail || '', + }; + + await sendEmail(emailOptions); + } + + return newUser; +}; export const getUsersAdmin = async () => { try { - const users = await db.select( - { + const users = await db + .select({ userId: userSchema.id, firstName: userSchema.first_name, lastName: userSchema.last_name, @@ -105,9 +200,9 @@ export const getUsersAdmin = async () => { branch: userSchema.branch, contact: userSchema.contact, permission: userSchema.permission, - discord_id: userSchema.discord_id - } - ).from(userSchema); + discord_id: userSchema.discord_id, + }) + .from(userSchema); return users; } catch (err) { console.error('Erreur lors de la récupération des utilisateurs ', err); @@ -117,15 +212,15 @@ export const getUsersAdmin = async () => { export const getUsers = async () => { try { - const users = await db.select( - { + const users = await db + .select({ userId: userSchema.id, firstName: userSchema.first_name, lastName: userSchema.last_name, permission: userSchema.permission, - email: userSchema.email - } - ).from(userSchema); + email: userSchema.email, + }) + .from(userSchema); return users; } catch (err) { console.error('Erreur lors de la récupération des utilisateurs ', err); @@ -164,10 +259,9 @@ export const getUsersAll = async () => { factionName, roles, }; - }) + }), ); - return userWithTeam; } catch (err) { console.error('Erreur lors de la récupération des utilisateurs ', err); @@ -177,15 +271,16 @@ export const getUsersAll = async () => { export const getUsersbyPermission = async (permission: string) => { try { - const users = await db.select( - { + const users = await db + .select({ userId: userSchema.id, firstName: userSchema.first_name, lastName: userSchema.last_name, email: userSchema.email, - branch: userSchema.branch - } - ).from(userSchema).where(eq(userSchema.permission, permission)); + branch: userSchema.branch, + }) + .from(userSchema) + .where(eq(userSchema.permission, permission)); return users; } catch (err) { console.error('Erreur lors de la récupération des utilisateurs ', err); @@ -195,29 +290,27 @@ export const getUsersbyPermission = async (permission: string) => { export const updateUserPassword = async (userId: number, password: string) => { try { - const result = await db.update(userSchema) + const result = await db + .update(userSchema) .set({ - password: password + password: password, }) .where(eq(userSchema.id, userId)); return result.rows[0]; } catch (err) { - console.error('Erreur lors de la récupération et de l\'update de l\'utilisateur par email:', err); + console.error("Erreur lors de la récupération et de l'update de l'utilisateur par email:", err); throw new Error('Erreur de base de données'); } -} +}; -export const updateUserInfoByUserId = async ( - userId: number, - branch?: string, - contact?: string -) => { +export const updateUserInfoByUserId = async (userId: number, branch?: string, contact?: string) => { try { - const result = await db.update(userSchema) + const result = await db + .update(userSchema) .set({ branch: branch, - contact: contact + contact: contact, }) .where(eq(userSchema.id, userId)); @@ -228,33 +321,27 @@ export const updateUserInfoByUserId = async ( } }; -export const updateUserByAdmin = async ( - userId: number, - updates: Partial -) => { +export const updateUserByAdmin = async (userId: number, updates: Partial) => { try { - if (Object.keys(updates).length === 0) { throw new Error('Aucune donnée à mettre à jour'); } - const result = await db.update(userSchema) - .set( - updates - ) - .where(eq(userSchema.id, userId)); + const result = await db.update(userSchema).set(updates).where(eq(userSchema.id, userId)); return result; } catch (err) { - console.error('Erreur lors de la mise à jour par l\'admin:', err); + console.error("Erreur lors de la mise à jour par l'admin:", err); throw new Error('Erreur de base de données'); } }; export const deleteUserById = async (userId: number) => { try { - - const user_registration_token = await db.select({ user_id: registrationSchema.user_id }).from(registrationSchema).where(eq(registrationSchema.user_id, userId)); + const user_registration_token = await db + .select({ user_id: registrationSchema.user_id }) + .from(registrationSchema) + .where(eq(registrationSchema.user_id, userId)); if (user_registration_token.length > 0) { await db.delete(registrationSchema).where(eq(registrationSchema.user_id, userId)); @@ -263,7 +350,7 @@ export const deleteUserById = async (userId: number) => { const result = await db.delete(userSchema).where(eq(userSchema.id, userId)); return result; } catch (err) { - console.error('Erreur lors de la suppression de l\'utilisateur:', err); + console.error("Erreur lors de la suppression de l'utilisateur:", err); throw new Error('Erreur de base de données'); } }; diff --git a/backend/src/utils/no_sync_list.ts b/backend/src/utils/no_sync_list.ts deleted file mode 100644 index dcfe90e..0000000 --- a/backend/src/utils/no_sync_list.ts +++ /dev/null @@ -1,4 +0,0 @@ -//This list is to ignore user from UTT API who already have an account on the website but not with the same email address -// OR People we don't want to sync like "BIO REF" or demissionary -export const noSyncEmails = [ -]; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bf8f1ec..9e0a619 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import AdminRoute from './components/utils/adminroute'; import PrivateRoute from './components/utils/privateroute'; import ProtectedRoute from './components/utils/protectedroute'; import { + AdminBanned, AdminPageBus, AdminPageChall, AdminPageEmail, @@ -310,6 +311,14 @@ const App: React.FC = () => { } /> + + + + } + /> {/* Fallback */} } /> diff --git a/frontend/src/components/Admin/adminBanned.tsx b/frontend/src/components/Admin/adminBanned.tsx new file mode 100644 index 0000000..81c5007 --- /dev/null +++ b/frontend/src/components/Admin/adminBanned.tsx @@ -0,0 +1,201 @@ +import { useState } from 'react'; +import { useEffect } from 'react'; +import Swal from 'sweetalert2'; + +import type { AdminBannedProps, Banned } from '../../interfaces/banned.interface'; +import { addBanned, getAllBanned, removeBanned } from '../../services/requests/banned.service'; +import { Button } from '../ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { Input } from '../ui/input'; + +export const AdminBannedList = ({ ...props }: AdminBannedProps) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchShotgunAttempts = async () => { + try { + const data = await getAllBanned(); + props.setBannedList(data); + } catch { + setError('Impossible de récupérer les addresses bannies.'); + } finally { + setLoading(false); + } + }; + + void fetchShotgunAttempts(); + }, []); + + const handleDelete = async (banned: Banned) => { + const confirm = await Swal.fire({ + title: `Supprimer l'adresse ${banned.email} ?`, + text: 'Il sera à nouveau possible de créer un compte avec cette adresse email.', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#2563eb', + cancelButtonColor: '#d33', + confirmButtonText: 'Oui', + cancelButtonText: 'Annuler', + }); + + if (!confirm.isConfirmed) return; + + try { + const response = await removeBanned(banned.id); + + props.setBannedList(props.bannedList.filter((item) => item.id !== response.data.id)); + + Swal.fire({ + icon: 'success', + title: 'Adresse retirée', + text: response.message, + confirmButtonColor: '#16a34a', + }); + } catch (error: any) { + Swal.fire({ + title: 'Erreur ❌', + text: error?.response?.data?.msg || 'Une erreur est survenue.', + icon: 'error', + }); + } + }; + + return ( + + + + Adresses emails bannies + + + + {loading &&

Chargement...

} + {!loading && error &&

{error}

} + + {!loading && !error && ( +
+ + + + + + + + + {props.bannedList + .sort((a, b) => a.email.localeCompare(b.email)) + .map((banned) => ( + + + + + ))} + {props.bannedList.length === 0 && ( + + + + )} + +
AddresseSupprimer
{banned.email} + +
+ Aucune addresse enregistrée. +
+
+ )} +
+
+ ); +}; + +export const AdminBannedAddEmail = ({ ...props }: AdminBannedProps) => { + const [newAddress, setNewAddress] = useState(''); + + const handleSave = async () => { + if (props.bannedList.map((banned) => banned.email).includes(newAddress)) { + Swal.fire({ + title: 'Addresse déjà présente', + icon: 'error', + }); + return; + } + + const confirm = await Swal.fire({ + title: `Supprimer le compte de ${newAddress} ?`, + text: "Le compte de l'utilisateur sera supprimé (s'il existe), et il ne pourra plus se créer de compte.", + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#2563eb', + cancelButtonColor: '#d33', + confirmButtonText: 'Oui', + cancelButtonText: 'Annuler', + }); + + if (confirm.isConfirmed) { + try { + const response = await addBanned(newAddress); + + props.setBannedList([response.data, ...props.bannedList]); + + setNewAddress(''); + + Swal.fire({ + icon: 'success', + title: 'Adresse bannie', + text: response.message, + confirmButtonColor: '#16a34a', + }); + } catch (error: any) { + Swal.fire({ + title: 'Erreur ❌', + text: error?.response?.data?.msg || 'Une erreur est survenue.', + icon: 'error', + }); + } + } + }; + + return ( + + + + ➕ Nouvelle adresse bannie + + + + +
{ + e.preventDefault(); + handleSave(); + }}> + setNewAddress(e.target.value)} + placeholder="Email" + /> + +
+ +
+
+ Explication: +

+ Les adresses emails ajoutées à la liste de ban ne seront plus synchronisées, la création de + leur compte étant impossible, même lors d'un envoi depuis l'API de l'UTT. Il s'agit de la + technique idéale pour les personnes démissionnaires ou interdites d'intégration. +
+ L'ajout d'une adresse à cette liste supprimera immédiatement le compte affilié à cette + adresse email s'il existe. +

+
+
+
+
+ ); +}; diff --git a/frontend/src/components/Admin/adminUser.tsx b/frontend/src/components/Admin/adminUser.tsx index e1a7a74..bab1bdc 100644 --- a/frontend/src/components/Admin/adminUser.tsx +++ b/frontend/src/components/Admin/adminUser.tsx @@ -1,10 +1,12 @@ import { useEffect, useState } from 'react'; import Select from 'react-select'; +import { type SingleValue } from 'react-select'; import Swal from 'sweetalert2'; import { type User } from '../../interfaces/user.interface'; import { renewTokenUser, requestPasswordUser } from '../../services/requests/auth.service'; import { + createUserByAdmin, deleteUserByAdmin, getUsersAdmin, syncnewStudent, @@ -42,6 +44,10 @@ const majeurOptions = [ { value: false, label: 'Mineur' }, ]; +type BranchOption = (typeof branchOptions)[number]; + +type MajorOption = (typeof majeurOptions)[number]; + export const AdminUser = () => { const [users, setUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); @@ -330,3 +336,134 @@ export const AdminSyncNewStudent = () => { ); }; + +export const AdminRegisterNewStudent = () => { + const [newFirstName, setNewFirstName] = useState(''); + const [newLastName, setNewLastName] = useState(''); + const [newEmail, setNewEmail] = useState(''); + const [newMajorState, setNewMajorState] = useState(null); + const [newBranch, setNewBranch] = useState(null); + + const handleSave = async ({ withNotification = true } = {}) => { + if (!newFirstName || !newLastName || !newEmail || !newMajorState || !newBranch) { + Swal.fire({ + title: 'Champs vides', + text: "L'ensemble des champs sont nécessaires.", + icon: 'error', + }); + return; + } + + try { + const response = await createUserByAdmin({ + firstName: newFirstName, + lastName: newLastName, + email: newEmail.toLowerCase(), + major: newMajorState.value, + branch: newBranch.value, + withNotification, + }); + + Swal.fire({ + icon: 'success', + title: 'Utilisateur mis à jour', + text: response.message, + confirmButtonColor: '#16a34a', + }); + } catch (error: any) { + Swal.fire({ + title: 'Erreur ❌', + text: error?.response?.data?.message || 'Une erreur est survenue.', + icon: 'error', + }); + } + }; + + return ( + + + + ➕ Nouvel Utilisateur + + + + +
+

+ Ce formulaire permet de créer uniquement un compte Nouveau. +
+ Pour les CE, Organisateurs, Admins, il suffit à la personne de se connecter via le CAS pour + créer son compte. +

+ + setNewFirstName(e.target.value)} + placeholder="Prénom" + /> + setNewLastName(e.target.value)} + placeholder="Nom" + /> + setNewEmail(e.target.value)} + placeholder="Email" + /> + +

+ Attention : Renseigner l'état de majorité le jour de la SDI. +

+ + ) => setNewBranch(value)} + /> + +
+ + +
+
+ Détail des options: +
    +
  • + Le bouton Enregistrer et envoyer mail Welcome permet d'enregistrer l'utilisateur + sur le site, puis envoi immédiatement un mail présentant l'intégration et permettant de + créer son mot de passe pour accéder au site. +
  • +
  • + Le bouton Enregistrer silencieusement permet d'enregistrer l'utilisateur sur le + site, mais n'envoi aucun mail. +
  • +
+
+
+
+
+ ); +}; diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index f0975ba..c22cba5 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -1,10 +1,10 @@ -import { Bars4Icon, CogIcon, HomeIcon, UsersIcon } from "@heroicons/react/24/outline"; -import { XMarkIcon, } from "@heroicons/react/24/solid"; -import { AnimatePresence, motion } from "framer-motion"; -import { Fragment, useEffect, useState } from "react"; -import { NavLink, useLocation } from "react-router-dom"; +import { Bars4Icon, CogIcon, HomeIcon, UsersIcon } from '@heroicons/react/24/outline'; +import { XMarkIcon } from '@heroicons/react/24/solid'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Fragment, useEffect, useState } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; -import { decodeToken, getToken } from "../services/requests/auth.service"; +import { decodeToken, getToken } from '../services/requests/auth.service'; interface NavItem { label: string; @@ -12,10 +12,10 @@ interface NavItem { icon?: React.ComponentType>; rolesAllowed?: string[]; // ["Admin", "Respo CE", ...] public?: boolean; - showWhen?: "always" | "auth" | "guest"; - kind?: "link" | "action"; + showWhen?: 'always' | 'auth' | 'guest'; + kind?: 'link' | 'action'; onClick?: () => void; - children?: NavItem[]; // pour dropdown + children?: NavItem[]; // pour dropdown } export const Navbar = () => { @@ -23,12 +23,14 @@ export const Navbar = () => { const token = getToken(); const [menuOpen, setMenuOpen] = useState(false); const isAuthenticated = Boolean(token); - const { userPermission, userRoles = [] } = token ? decodeToken(token) : { userPermission: undefined, userRoles: [] }; - const roles = [userPermission, ...userRoles.map(r => r.roleName)].filter(Boolean) as string[]; + const { userPermission, userRoles = [] } = token + ? decodeToken(token) + : { userPermission: undefined, userRoles: [] }; + const roles = [userPermission, ...userRoles.map((r) => r.roleName)].filter(Boolean) as string[]; const handleLogout = () => { - localStorage.removeItem("authToken"); - window.location.href = "/"; + localStorage.removeItem('authToken'); + window.location.href = '/'; }; useEffect(() => { @@ -36,90 +38,89 @@ export const Navbar = () => { }, [pathname]); const navItems: NavItem[] = [ - { label: "Home", to: "/home", icon: HomeIcon }, - { label: "Plannings", to: "/plannings" }, - { label: "Parrainage", to: "/parrainage" }, - { label: "Challenges", to: "/challenges" }, - { label: "Mes Actus", to: "/news" }, + { label: 'Home', to: '/home', icon: HomeIcon }, + { label: 'Plannings', to: '/plannings' }, + { label: 'Parrainage', to: '/parrainage' }, + { label: 'Challenges', to: '/challenges' }, + { label: 'Mes Actus', to: '/news' }, { - label: "Permanences", - to: "#", + label: 'Permanences', + to: '#', children: [ - { label: "Listes des permanences", to: "/permanenceslist", rolesAllowed: ["Admin", "Student"] }, - { label: "Mes permanences", to: "/mypermanences", rolesAllowed: ["Admin", "Student"] }, - { label: "Faire l'appel", to: "/permanencesappeal", rolesAllowed: ["Admin", "Student"] }, + { label: 'Listes des permanences', to: '/permanenceslist', rolesAllowed: ['Admin', 'Student'] }, + { label: 'Mes permanences', to: '/mypermanences', rolesAllowed: ['Admin', 'Student'] }, + { label: "Faire l'appel", to: '/permanencesappeal', rolesAllowed: ['Admin', 'Student'] }, ], }, { - label: "Events", - to: "#", + label: 'Events', + to: '#', children: [ - { label: "Shotgun", to: "/shotgun", rolesAllowed: ["Admin", "Student"] }, - { label: "WEI", to: "/wei" }, - { label: "SDI", to: "/sdi" }, - { label: "Repas", to: "/food" }, - { label: "Defis Commissions", to: "/games", rolesAllowed: ["Admin", "Student"] }, + { label: 'Shotgun', to: '/shotgun', rolesAllowed: ['Admin', 'Student'] }, + { label: 'WEI', to: '/wei' }, + { label: 'SDI', to: '/sdi' }, + { label: 'Repas', to: '/food' }, + { label: 'Defis Commissions', to: '/games', rolesAllowed: ['Admin', 'Student'] }, ], }, - { label: "Mon compte", to: "/profil", icon: UsersIcon }, + { label: 'Mon compte', to: '/profil', icon: UsersIcon }, { - label: "Admin", - to: "#", + label: 'Admin', + to: '#', icon: CogIcon, children: [ - { label: "Bus", to: "/admin/bus", rolesAllowed: ["Admin"] }, - { label: "Challenge", to: "/admin/challenge", rolesAllowed: ["Admin", "Arbitre"] }, - { label: "Email", to: "/admin/email", rolesAllowed: ["Admin"] }, - { label: "Events", to: "/admin/events", rolesAllowed: ["Admin"] }, - { label: "Export / Import", to: "/admin/export-import", rolesAllowed: ["Admin"] }, - { label: "Factions", to: "/admin/factions", rolesAllowed: ["Admin", "Respo CE"] }, - { label: "Games", to: "/admin/games", rolesAllowed: ["Admin"] }, - { label: "News", to: "/admin/news", rolesAllowed: ["Admin", "Communication"] }, - { label: "Permanences", to: "/admin/permanences", rolesAllowed: ["Admin", "Respo CE"] }, - { label: "Roles", to: "/admin/roles", rolesAllowed: ["Admin"] }, - { label: "Shotgun", to: "/admin/shotgun", rolesAllowed: ["Admin", "Respo CE"] }, - { label: "Teams", to: "/admin/teams", rolesAllowed: ["Admin", "Respo CE"] }, - { label: "Tentes", to: "/admin/tent", rolesAllowed: ["Admin"] }, - { label: "Users", to: "/admin/users", rolesAllowed: ["Admin"] }, + { label: 'Bannis', to: '/admin/banned', rolesAllowed: ['Admin'] }, + { label: 'Bus', to: '/admin/bus', rolesAllowed: ['Admin'] }, + { label: 'Challenge', to: '/admin/challenge', rolesAllowed: ['Admin', 'Arbitre'] }, + { label: 'Email', to: '/admin/email', rolesAllowed: ['Admin'] }, + { label: 'Events', to: '/admin/events', rolesAllowed: ['Admin'] }, + { label: 'Export / Import', to: '/admin/export-import', rolesAllowed: ['Admin'] }, + { label: 'Factions', to: '/admin/factions', rolesAllowed: ['Admin', 'Respo CE'] }, + { label: 'Games', to: '/admin/games', rolesAllowed: ['Admin'] }, + { label: 'News', to: '/admin/news', rolesAllowed: ['Admin', 'Communication'] }, + { label: 'Permanences', to: '/admin/permanences', rolesAllowed: ['Admin', 'Respo CE'] }, + { label: 'Roles', to: '/admin/roles', rolesAllowed: ['Admin'] }, + { label: 'Shotgun', to: '/admin/shotgun', rolesAllowed: ['Admin', 'Respo CE'] }, + { label: 'Teams', to: '/admin/teams', rolesAllowed: ['Admin', 'Respo CE'] }, + { label: 'Tentes', to: '/admin/tent', rolesAllowed: ['Admin'] }, + { label: 'Users', to: '/admin/users', rolesAllowed: ['Admin'] }, ], }, { - label: "Déconnexion", - to: "/", - kind: "action", - showWhen: "auth", + label: 'Déconnexion', + to: '/', + kind: 'action', + showWhen: 'auth', onClick: handleLogout, }, { - label: "Se connecter", - to: "/", + label: 'Se connecter', + to: '/', public: true, - showWhen: "guest", + showWhen: 'guest', }, - ]; - const canShowItem = (item: NavItem): boolean => { - if (item.showWhen === "auth") return isAuthenticated; - if (item.showWhen === "guest") return !isAuthenticated; + if (item.showWhen === 'auth') return isAuthenticated; + if (item.showWhen === 'guest') return !isAuthenticated; if (!isAuthenticated) { if (item.public) return true; if (item.children && item.children.length > 0) { - return item.children.some(child => canShowItem(child)); + return item.children.some((child) => canShowItem(child)); } return false; } if (item.rolesAllowed) { - return item.rolesAllowed.some(r => roles.includes(r)); + return item.rolesAllowed.some((r) => roles.includes(r)); } if (item.children && item.children.length > 0) { - return item.children.some(child => canShowItem(child)); + return item.children.some((child) => canShowItem(child)); } return true; @@ -137,29 +138,24 @@ export const Navbar = () => { {/* Menu desktop */}
    - {navItems.map(item => + {navItems.map((item) => canShowItem(item) ? (
  • {item.children ? ( - ) : item.kind === "action" ? ( + ) : item.kind === 'action' ? ( ) : ( )}
  • - ) : null + ) : null, )}
@@ -169,15 +165,14 @@ export const Navbar = () => { {menuOpen && ( - {navItems.map(item => + className="lg:hidden bg-blue-700 overflow-hidden"> + {navItems.map((item) => canShowItem(item) ? ( {!item.children ? ( - item.kind === "action" ? ( + item.kind === 'action' ? ( ) : ( @@ -186,7 +181,7 @@ export const Navbar = () => { )} - ) : null + ) : null, )} )} @@ -195,51 +190,24 @@ export const Navbar = () => { ); }; -const NavActionItem = ({ - item, - mobile = false, -}: { - item: NavItem; - mobile?: boolean; -}) => { - const base = mobile ? "block py-2 px-4 text-left w-full" : "inline-flex items-center py-2"; +const NavActionItem = ({ item, mobile = false }: { item: NavItem; mobile?: boolean }) => { + const base = mobile ? 'block py-2 px-4 text-left w-full' : 'inline-flex items-center py-2'; return ( - ); }; // Composant MenuItem -const MenuItem = ({ - item, - active = false, - mobile = false, -}: { - item: NavItem; - active?: boolean; - mobile?: boolean; -}) => { - const base = mobile ? "block py-2 px-4" : "inline-flex items-center py-2"; - const activeClass = active - ? "text-yellow-300 font-semibold border-b-2 border-yellow-300" - : "hover:text-yellow-200"; +const MenuItem = ({ item, active = false, mobile = false }: { item: NavItem; active?: boolean; mobile?: boolean }) => { + const base = mobile ? 'block py-2 px-4' : 'inline-flex items-center py-2'; + const activeClass = active ? 'text-yellow-300 font-semibold border-b-2 border-yellow-300' : 'hover:text-yellow-200'; return ( - - {item.icon && ( - - )} + + {item.icon && } {item.label} ); @@ -256,7 +224,7 @@ const Dropdown = ({ canShowItem: (item: NavItem) => boolean; }) => { const [open, setOpen] = useState(false); - const trigger = mobile ? "p-4" : "py-2 cursor-pointer"; + const trigger = mobile ? 'p-4' : 'py-2 cursor-pointer'; return (
@@ -266,8 +234,7 @@ const Dropdown = ({ className={`${trigger} flex items-center justify-between`} aria-expanded={open} aria-controls={`submenu-${item.label}`} - aria-haspopup="menu" - > + aria-haspopup="menu"> {item.icon && } {item.label} @@ -278,19 +245,18 @@ const Dropdown = ({ initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} - className={`absolute bg-white text-black rounded shadow-md z-50 ${mobile ? "static mt-2" : "top-full left-0 mt-1" - }`} - role="menu" - > - {item.children! - .filter(child => canShowItem(child)) - .map(child => ( + className={`absolute bg-white text-black rounded shadow-md z-50 ${ + mobile ? 'static mt-2' : 'top-full left-0 mt-1' + }`} + role="menu"> + {item + .children!.filter((child) => canShowItem(child)) + .map((child) => (
  • + role="menuitem"> {child.label}
  • diff --git a/frontend/src/interfaces/banned.interface.ts b/frontend/src/interfaces/banned.interface.ts new file mode 100644 index 0000000..cbe8850 --- /dev/null +++ b/frontend/src/interfaces/banned.interface.ts @@ -0,0 +1,11 @@ +import type { Dispatch, SetStateAction } from 'react'; + +export interface Banned { + id: number; + email: string; +} + +export interface AdminBannedProps { + bannedList: Banned[]; + setBannedList: Dispatch>; +} diff --git a/frontend/src/interfaces/user.interface.ts b/frontend/src/interfaces/user.interface.ts index 680dd49..75c6a7f 100644 --- a/frontend/src/interfaces/user.interface.ts +++ b/frontend/src/interfaces/user.interface.ts @@ -9,3 +9,12 @@ export interface User { contact: string; discord_id: string; } + +export interface NewUser { + firstName: string; + lastName: string; + email: string; + major: boolean; + branch: string; + withNotification: boolean; +} diff --git a/frontend/src/pages/admin.tsx b/frontend/src/pages/admin.tsx index edd8af9..433ab0f 100644 --- a/frontend/src/pages/admin.tsx +++ b/frontend/src/pages/admin.tsx @@ -1,42 +1,49 @@ //--------------Challenge Import--------------// -import { useEffect, useRef, useState } from "react"; - -import { AdminBusTools } from "../components/Admin/adminBus"; -import AdminChallengeList from "../components/Admin/AdminChallenge/adminChalengeList"; -import { AdminChallengeAddPointsForm } from "../components/Admin/AdminChallenge/adminChallengeAddPointsForm"; -import ChallengeEditor from "../components/Admin/AdminChallenge/adminChallengeEditor"; -import { AdminValidatedChallengesList } from "../components/Admin/AdminChallenge/adminChallengeValidatedList"; -import { AdminEmail } from "../components/Admin/adminEmail"; -import { AdminEvents } from "../components/Admin/adminEvent"; -import { AdminExportConnect, AdminImportFoodMenu, AdminImportNotebooks, AdminImportPlannings } from "../components/Admin/adminExportImport"; -import { AdminFactionManagement } from "../components/Admin/adminFaction"; -import { AdminRolePointsManager } from "../components/Admin/adminGames"; -import { AdminLayout } from "../components/Admin/adminLayout"; -import { AdminNews } from "../components/Admin/adminNews"; +import { useEffect, useRef, useState } from 'react'; + +import { AdminBannedAddEmail, AdminBannedList } from '../components/Admin/adminBanned'; +import { AdminBusTools } from '../components/Admin/adminBus'; +import AdminChallengeList from '../components/Admin/AdminChallenge/adminChalengeList'; +import { AdminChallengeAddPointsForm } from '../components/Admin/AdminChallenge/adminChallengeAddPointsForm'; +import ChallengeEditor from '../components/Admin/AdminChallenge/adminChallengeEditor'; +import { AdminValidatedChallengesList } from '../components/Admin/AdminChallenge/adminChallengeValidatedList'; +import { AdminEmail } from '../components/Admin/adminEmail'; +import { AdminEvents } from '../components/Admin/adminEvent'; +import { + AdminExportConnect, + AdminImportFoodMenu, + AdminImportNotebooks, + AdminImportPlannings, +} from '../components/Admin/adminExportImport'; +import { AdminFactionManagement } from '../components/Admin/adminFaction'; +import { AdminRolePointsManager } from '../components/Admin/adminGames'; +import { AdminLayout } from '../components/Admin/adminLayout'; +import { AdminNews } from '../components/Admin/adminNews'; //--------------Perm Import--------------// -import PermanenceActions from "../components/Admin/AdminPerm/adminPermAction"; -import PermanenceForm from "../components/Admin/AdminPerm/adminPermForm"; -import { ImportPermCSV } from "../components/Admin/AdminPerm/adminPermImport"; -import PermanenceList from "../components/Admin/AdminPerm/adminPermList"; -import { AdminRoleManagement, AdminRolePreferences } from "../components/Admin/adminRole"; -import { AdminShotgunRanking } from "../components/Admin/adminShotgun"; -import { AdminTeamManagement, DistributeTeam } from "../components/Admin/adminTeam"; -import { TentAdmin } from "../components/Admin/adminTent"; -import { AdminSyncNewStudent, AdminUser } from "../components/Admin/adminUser"; -import { RevealSection } from "../components/ui/revealSection"; -import { type Challenge, type ValidatedChallenge } from "../interfaces/challenge.interface"; -import { type Faction } from "../interfaces/faction.interface"; -import { type Permanence } from "../interfaces/permanence.interface"; -import { type Team } from "../interfaces/team.interface"; -import { type User } from "../interfaces/user.interface"; -import { getAllChallenges, getAllChallengesValidates } from "../services/requests/challenge.service"; -import { getAllFactionsUser } from "../services/requests/faction.service"; -import { getAllPermanences } from "../services/requests/permanence.service"; -import { getAllTeams } from "../services/requests/team.service"; -import { getUsers, getUsersAdmin } from "../services/requests/user.service"; +import PermanenceActions from '../components/Admin/AdminPerm/adminPermAction'; +import PermanenceForm from '../components/Admin/AdminPerm/adminPermForm'; +import { ImportPermCSV } from '../components/Admin/AdminPerm/adminPermImport'; +import PermanenceList from '../components/Admin/AdminPerm/adminPermList'; +import { AdminRoleManagement, AdminRolePreferences } from '../components/Admin/adminRole'; +import { AdminShotgunRanking } from '../components/Admin/adminShotgun'; +import { AdminTeamManagement, DistributeTeam } from '../components/Admin/adminTeam'; +import { TentAdmin } from '../components/Admin/adminTent'; +import { AdminRegisterNewStudent, AdminSyncNewStudent, AdminUser } from '../components/Admin/adminUser'; +import { RevealSection } from '../components/ui/revealSection'; +import { type Banned } from '../interfaces/banned.interface'; +import { type Challenge, type ValidatedChallenge } from '../interfaces/challenge.interface'; +import { type Faction } from '../interfaces/faction.interface'; +import { type Permanence } from '../interfaces/permanence.interface'; +import { type Team } from '../interfaces/team.interface'; +import { type User } from '../interfaces/user.interface'; +import { getAllChallenges, getAllChallengesValidates } from '../services/requests/challenge.service'; +import { getAllFactionsUser } from '../services/requests/faction.service'; +import { getAllPermanences } from '../services/requests/permanence.service'; +import { getAllTeams } from '../services/requests/team.service'; +import { getUsers, getUsersAdmin } from '../services/requests/user.service'; export const AdminPageTeam: React.FC = () => ( - +
    @@ -50,7 +57,7 @@ export const AdminPageTeam: React.FC = () => ( ); export const AdminPageFaction: React.FC = () => ( - +
    @@ -60,7 +67,7 @@ export const AdminPageFaction: React.FC = () => ( ); export const AdminPageRole: React.FC = () => ( - +
    @@ -74,7 +81,7 @@ export const AdminPageRole: React.FC = () => ( ); export const AdminPageEvents: React.FC = () => ( - +
    @@ -84,7 +91,7 @@ export const AdminPageEvents: React.FC = () => ( ); export const AdminPageShotgun: React.FC = () => ( - +
    @@ -94,7 +101,7 @@ export const AdminPageShotgun: React.FC = () => ( ); export const AdminPageExport: React.FC = () => ( - +
    @@ -139,9 +146,8 @@ export const AdminPagePerm: React.FC = () => { }; return ( - +
    - {/* Formulaire (créer/éditer) */} { setEditMode(true); setEditPermanence(perm); setTimeout(() => { - editorRef.current?.scrollIntoView({ behavior: "smooth" }); + editorRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 100); }} /> @@ -173,10 +179,7 @@ export const AdminPagePerm: React.FC = () => { {/* Actions globales */} - + {/* Import CSV (si dispo) */} @@ -199,20 +202,18 @@ export const AdminPageChall: React.FC = () => { const fetchChallengesUsersTeamsFactions = async () => { try { - const challsRes = await getAllChallenges(); const usersRes = await getUsers(); const teamsRes = await getAllTeams(); const factionsRes = await getAllFactionsUser(); - const challsResFiltered = challsRes.filter((c: Challenge) => c.category != "Free") + const challsResFiltered = challsRes.filter((c: Challenge) => c.category != 'Free'); setChallenges(challsResFiltered); setUsers(usersRes); setTeams(teamsRes); setFactions(factionsRes); - } catch (err) { - console.error("Erreur chargement challenges", err); + console.error('Erreur chargement challenges', err); } }; @@ -221,7 +222,7 @@ export const AdminPageChall: React.FC = () => { const res = await getAllChallengesValidates(); setValidatedChallenges(res); } catch (err) { - console.error("Erreur chargement challenges validés", err); + console.error('Erreur chargement challenges validés', err); } }; @@ -232,11 +233,11 @@ export const AdminPageChall: React.FC = () => { const handleEdit = (challenge: Challenge) => { setEditingChallenge(challenge); - editorRef.current?.scrollIntoView({ behavior: "smooth" }); + editorRef.current?.scrollIntoView({ behavior: 'smooth' }); }; return ( - +
    {/* Formulaire création / édition */} @@ -267,7 +268,6 @@ export const AdminPageChall: React.FC = () => { - {/* Liste des challenges validés */} { }; export const AdminPageEmail: React.FC = () => ( - +
    @@ -291,7 +291,7 @@ export const AdminPageEmail: React.FC = () => ( ); export const AdminPageUser: React.FC = () => ( - +
    @@ -300,12 +300,16 @@ export const AdminPageUser: React.FC = () => ( + + + +
    ); export const AdminPageNews: React.FC = () => ( - +
    @@ -315,7 +319,7 @@ export const AdminPageNews: React.FC = () => ( ); export const AdminPageTent: React.FC = () => ( - +
    @@ -325,7 +329,7 @@ export const AdminPageTent: React.FC = () => ( ); export const AdminPageBus: React.FC = () => ( - +
    @@ -335,7 +339,7 @@ export const AdminPageBus: React.FC = () => ( ); export const AdminPageGames: React.FC = () => ( - +
    @@ -343,3 +347,21 @@ export const AdminPageGames: React.FC = () => (
    ); + +export const AdminBanned: React.FC = () => { + const [bannedList, setBannedList] = useState([]); + + return ( + +
    + + + + + + + +
    +
    + ); +}; diff --git a/frontend/src/services/requests/banned.service.ts b/frontend/src/services/requests/banned.service.ts new file mode 100644 index 0000000..44a5862 --- /dev/null +++ b/frontend/src/services/requests/banned.service.ts @@ -0,0 +1,16 @@ +import api from '../api'; + +export const getAllBanned = async () => { + const res = await api.get('/banned/admin/all/'); + return res.data.data; +}; + +export const addBanned = async (email: string) => { + const res = await api.post('/banned/admin/', { email }); + return res.data; +}; + +export const removeBanned = async (id: number) => { + const res = await api.delete(`/banned/admin/${id}/`); + return res.data; +}; diff --git a/frontend/src/services/requests/user.service.ts b/frontend/src/services/requests/user.service.ts index 80ac24c..c154779 100644 --- a/frontend/src/services/requests/user.service.ts +++ b/frontend/src/services/requests/user.service.ts @@ -1,4 +1,4 @@ -import { type User } from '../../interfaces/user.interface'; +import type { NewUser, User } from '../../interfaces/user.interface'; import api from '../api'; export const getPermission = (): string | null => { @@ -24,49 +24,54 @@ export const isConnected = (): boolean => { }; export const getUsers = async () => { - const response = await api.get("/user/user/getusers"); + const response = await api.get('/user/user/getusers'); const users = response.data.data; return users; -} +}; export const getUsersAdmin = async () => { - const response = await api.get("/user/admin/getusers"); + const response = await api.get('/user/admin/getusers'); const users = response.data.data; return users; -} +}; export const getUsersByPermission = async () => { - const response = await api.get("/user/admin/getusersbypermission"); + const response = await api.get('/user/admin/getusersbypermission'); const users = response.data.data; return users; -} +}; export const getCurrentUser = async () => { - const res = await api.get("/user/user/me"); + const res = await api.get('/user/user/me'); return res.data.data; }; export const updateCurrentUser = async (data: Partial) => { - const response = await api.patch("/user/user/me", data); - return response.data + const response = await api.patch('/user/user/me', data); + return response.data; }; export const updateUserByAdmin = async (id: number, data: Partial) => { const response = await api.patch(`/user/admin/user/${id}`, data); - return response.data + return response.data; +}; + +export const createUserByAdmin = async (data: NewUser) => { + const response = await api.post(`/user/admin/user`, data); + return response.data; }; export const deleteUserByAdmin = async (id: number) => { const response = await api.delete(`/user/admin/user/${id}`); - return response.data + return response.data; }; export const syncnewStudent = async (date: string) => { const response = await api.post(`/user/admin/syncnewstudent/`, { date }); - return response.data + return response.data; }; export const syncDiscordUser = async (code: string) => { const response = await api.post(`/discord/user/callback/`, { code }); - return response.data -} + return response.data; +}; diff --git a/integration.utt.fr-key.pem b/integration.utt.fr-key.pem deleted file mode 100644 index 9715f79..0000000 --- a/integration.utt.fr-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCtif9VpeK8oNba -IfHlIyElLPsCYDLdfcYMNQUyGIyZGNa053rJuf7O5zyClVZb2WzLUXXOGROM4pZi -qE5D5CN0hbGLW2oPB4tuLhdl5CTse1iOqT0QYT4Uh+0760gug97lvDGaZlWVclEc -YpXsaz0qEFOORkGzm0E/7Q83dTkBEjI4yT0RpgM444DOGhaqpCM0hItUPaaIgHj9 -jwi8Z4leOQWW5edmPL2WoJmCs8z89iKn156e8V3Rudvax5kB8JZNrfKy0t+Dxq8D -fR/aZwFh2Zwm5VMbvsyiPD9EAJe85dVcmgDJ2msqzA2KXrCpuybXaBQNDvL1F7JQ -sPnEFj5FAgMBAAECggEAdZRugCVpPszrRdiCAPKQXpPfbninfhLdwR+baZngiUqf -DutX8p4m2yEpioCMhqcGY6vJ2d57tJVBt465mJ5Wt3huFzHU5mICQqDQEaVGr0PT -eLWKPjLk0RxXzKVZvspyl82u/iIgEqXl9wmE8y6lvn5ZXqiTk5G54ApKqRSvjt0b -lpk5qDPcgDeEKHmy/5GY6Xw5fbnohyeU2pROd/5iANRxxhh3zZehV+BE8IvX6m+Y -rKgQnj+1c3gL9idOUXI4ouHGLQXpUmF7FzHHZxDXjZpIwgnCV5wQHY15Mjraucrl -10gs7f0BUgh3bPGVMJOLoZP2yy7swPZJmMPwZKhDAQKBgQDM/KhXlyZ5GfWEbWt5 -4z1E3JaAlkqrrnwh6pvJASU8usSSXHp9kdPg9/UvWIgXTox1LS0gIiRg7g3v3nmv -UXCM65vpkvZ6dSoLmxG7y1Y4A9bFPBC95BaMiRRCa5XwIuVmlIMmZ6ruqLS2eTnL -gGHUSmDMpsRNriQeWpjmi+9gVQKBgQDYudowoENqXYpEms7gtZJ/RLDAc4joz9LI -MuIKFwOEDv9JCPbCjngKxIbEHvVl/hF5eaWk2P8lsGAT2GcR1ZQ/xxBMytWlvdAl -5/b8sdQfAGtE5K6BysumjIVqObAEQcLQ3ta+B3PR7n2Vh/LRRT+bt4g72dccRcIB -UmvfPIeWMQKBgChDpKl31iXJdJFrkMXjXeCN174wR0CLyHQ9Chakc/UG1p/NLH5H -y6+P5QhEwo6ZbjuCATAjpLOpbvFj6NEIFSyJBxoNNP7+zqBy+DvECA5+qowZbUxv -ZgJ61pDpYw1FPXw1xcEgcdHpL338N98CO7UgWv038K01fIC92PTIHd/1AoGBALXZ -DBktACQppLD37IpkEC41ptF0n/YpG0XcXAn3UX3nT5EqslKBVHxEdoftKh+QVX1F -8xUk9sHbAmLke2ddfG0fTLACqc3OPO7xei6Bz+jLYzaFY1+Il+SBmBiPmv+XZi34 -LNt6SVZm9H0Ze0bZAgxYrTj7CiGw7p5JWDYSBfqhAoGAXInJFpLndIQPRylTya7w -fJRZQ6vuLCN/BHdCu7DNPMt7kGGym4pJ+88DXrUkBmvoJ/ivzpuHY8MJXhp7cE5L -tsU43N6mfqBgeYP/utdMNKw6istk0d+4xSLiyF8fuBVIinDHny1oxV9Ww5TScTPe -Dg+Kdon5jypMQzPFEpB9xdY= ------END PRIVATE KEY----- diff --git a/integration.utt.fr.pem b/integration.utt.fr.pem deleted file mode 100644 index 53c55da..0000000 --- a/integration.utt.fr.pem +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEFjCCAn6gAwIBAgIRALShiUHfdwNDV0zIXo5dFUAwDQYJKoZIhvcNAQELBQAw -XzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMRowGAYDVQQLDBFkb2Rp -bkBBcnRodXJEb2RpbjEhMB8GA1UEAwwYbWtjZXJ0IGRvZGluQEFydGh1ckRvZGlu -MB4XDTI2MDQyOTEzMzkzNloXDTI4MDcyOTEzMzkzNlowRTEnMCUGA1UEChMebWtj -ZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMRowGAYDVQQLDBFkb2RpbkBBcnRo -dXJEb2RpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2J/1Wl4ryg -1toh8eUjISUs+wJgMt19xgw1BTIYjJkY1rTnesm5/s7nPIKVVlvZbMtRdc4ZE4zi -lmKoTkPkI3SFsYtbag8Hi24uF2XkJOx7WI6pPRBhPhSH7TvrSC6D3uW8MZpmVZVy -URxilexrPSoQU45GQbObQT/tDzd1OQESMjjJPRGmAzjjgM4aFqqkIzSEi1Q9poiA -eP2PCLxniV45BZbl52Y8vZagmYKzzPz2IqfXnp7xXdG529rHmQHwlk2t8rLS34PG -rwN9H9pnAWHZnCblUxu+zKI8P0QAl7zl1VyaAMnaayrMDYpesKm7JtdoFA0O8vUX -slCw+cQWPkUCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsG -AQUFBwMBMB8GA1UdIwQYMBaAFCiT+Oto6lg8pQlhwjiu0sL8L0xgMB0GA1UdEQQW -MBSCEmludGVncmF0aW9uLnV0dC5mcjANBgkqhkiG9w0BAQsFAAOCAYEAlip+Dy7G -UuHwqHE3MkkJZbhmc7Vv1zTa4Lr/HULKGF16egdqU3BDAYD8KrWjQwJgOd/gSATT -Z0jSRv7AZqKVcl99hTcvDyEWFoHZDPyyjbHr0pwfr3WBtEu5FOaE/ntTqVAX93tQ -Qy/pWFhJ9Grj6SC1dKc7DV8hgzYdxLt+QwM0t01niPdOsrC63DDcJ2Avxdw/SrCR -pjFDy2ilP1htmX96KnzO5LDGtl6T4mKEogOIz8Q5xenxWVosgrxVq6xa3tNmcBDh -if3AtNdWPNDtJdU4+LIVtlHps0t6MEqj+oqPSPpaMTS1iNmjtR5O63+NN8yE44/K -/oMX0tVx1X3kjRDq9t9vlY2Bq0TBqlfHD2Aw7rzFD9rURQdyBqY8hzfDvoaMUrSo -rcwubTgJky3d8s6HHSsK4ix0hTbTPO1FrHSfAl4FMg5M/LStfEvtgyUmK6MZDWyY -yjkxLaVseJsG8D0IFME3yOosaN7eVPMx71Qc+bPOQhgXjNsabC94dcjO ------END CERTIFICATE-----