Compare commits

..
Author SHA1 Message Date
sdraris a8ca887f3d [BACK] Ajout/MAJ des routes enfants (CRUD) 2025-09-29 12:13:28 +02:00
sdraris 7c06fc9c00 feat(enfants): ajout de controller et service (refs #01) 2025-09-25 10:51:59 +02:00
sdraris 1df6166649 generation de route dossiers 2025-09-24 15:27:12 +02:00
sdraris e46a16735c chore: remove old auth DTOs from user/dto after refactor 2025-09-24 10:33:31 +02:00
sdraris 489edf22e7 auth dto moved to auth route 2025-09-24 10:26:21 +02:00
sdraris d117e7b925 few comments added 2025-09-23 13:02:14 +02:00
sdraris 0a4f3d59f8 user service corrected 2025-09-23 11:59:27 +02:00
sdraris b581acc1fe users just removed useless space 2025-09-23 11:54:59 +02:00
sdraris 450627b091 dto finally corrected 2025-09-23 11:54:18 +02:00
sdraris 740b88eceb token refresh dto 2025-09-23 10:23:11 +02:00
sdraris 18945edb51 controller refresh token dto applied 2025-09-23 09:53:22 +02:00
Sofiane Draris 0982f81dba starting logout 2025-09-22 22:53:52 +02:00
Sofiane Draris 7d08391ce0 starting logout 2025-09-22 22:52:23 +02:00
sdraris ceaf615a24 log test login 2025-09-22 13:07:33 +02:00
sdraris 64ec020cc8 remove double hashing 2025-09-22 12:23:43 +02:00
sdraris 72df8e473d hashage corrected 2025-09-22 12:00:34 +02:00
sdraris ce6d713ec7 jwt + validation config corrected 2025-09-22 10:55:21 +02:00
sdraris 2ff4711bf6 authguard corrected 2025-09-22 10:54:29 +02:00
sdraris b93f935564 type corrected 2025-09-22 10:53:57 +02:00
sdraris 641d0926e9 profile dto added 2025-09-22 10:53:11 +02:00
sdraris 99e742f840 auth profile added 2025-09-22 10:52:20 +02:00
sdraris de725dffd7 profile dto added 2025-09-22 10:30:08 +02:00
sdraris f22da94ab4 test de push + hash comparing back 2025-09-22 09:59:08 +02:00
sdraris a1283bf210 Supprime l'ancien dossier gestionnaires de src/routes 2025-09-19 15:27:09 +02:00
sdraris c4b5de6ef2 moving gestionnaires 2025-09-19 15:19:15 +02:00
sdraris bceffda1e8 gestion des roles pour user service 2025-09-19 13:40:40 +02:00
sdraris 6b82af49ae user correction added if exists 2025-09-19 12:16:07 +02:00
sdraris 31b3893ff1 delete removed 2025-09-19 12:07:44 +02:00
sdraris 260397c340 delete removed 2025-09-19 12:05:39 +02:00
sdraris d030234c1f delete removed 2025-09-19 12:04:40 +02:00
sdraris 7d47fbd8de delete removed from services for global reasons 2025-09-19 11:57:40 +02:00
sdraris 4f4ca28682 parents dto creection 2025-09-19 11:36:15 +02:00
sdraris 53126dd7e4 create user dto correction 2025-09-19 11:21:51 +02:00
sdraris 6c39a54b3b situation familiale added 2025-09-19 10:59:18 +02:00
sdraris 279d027944 dossiers corrected 2025-09-19 10:45:11 +02:00
sdraris fd58681c1b remove workflow 2025-09-17 20:55:25 +02:00
sdraris cf4bce12eb am can also create acounts for themselves now 2025-09-17 16:45:38 +02:00
sdraris 0823ffc5cf only super admin can remove users now 2025-09-17 16:05:47 +02:00
sdraris c8635c4d09 user dto correction 2025-09-17 15:43:13 +02:00
sdraris 39878644e8 test de push suspenduser service 2025-09-17 15:08:14 +02:00
sdraris b8af145325 petit test de push 2025-09-17 10:07:14 +02:00
sdraris b030d29b11 added health check at deploy 2025-09-16 11:51:50 +02:00
sdraris 7842050ed1 Mise à jour backend + workflow 2025-09-16 10:38:50 +02:00
sdraris 7848f9d4b0 putain LE NOM DE LA FONCTION 2025-09-15 17:42:58 +02:00
sdraris 0419cc5b1a added validation entity to module 2025-09-15 17:28:00 +02:00
sdraris a328a0a933 deploy is now edited 2025-09-15 16:22:08 +02:00
sdraris bfbf80f312 deploy is now added 2025-09-15 16:05:51 +02:00
sdraris 1641d4a027 deploy added 2025-09-15 13:03:24 +02:00
sdraris dc0e62bac3 added comment to validation 2025-09-15 12:14:10 +02:00
sdraris 31fc17fd40 added status change 2025-09-15 11:54:51 +02:00
sdraris dd6d1d3060 gestionnaire route added Swagger 2025-09-15 10:41:17 +02:00
sdraris 51ae2eb984 gestionnaire route added Swagger 2025-09-15 10:36:58 +02:00
sdraris a9af96e403 edited gestionnaires service 2025-09-15 10:22:17 +02:00
sdraris 5c75ad93b5 nounou service corrected 2025-09-14 19:24:13 +02:00
sdraris ff3cc7bc5d nounou dto harmonise 2025-09-14 19:22:27 +02:00
sdraris 70ae550733 nounou dto correction 2025-09-14 13:49:59 +02:00
Sofiane Draris 240fe6580b no more typo in validations entity 2025-09-13 18:41:56 +02:00
Sofiane Draris 4b9acacaec no more typo in validations entity 2025-09-13 18:33:38 +02:00
Sofiane Draris ba0a2ea7df no more typo in validations entity 2025-09-13 18:33:00 +02:00
Sofiane Draris e3624adb92 Merge branch 'master' of https://git.ptits-pas.fr/Ynov/ptitspas-ynov-back 2025-09-13 18:32:15 +02:00
Sofiane Draris 7ff22febe4 no more typo in validations entity 2025-09-13 18:30:38 +02:00
sdraris 67b00b9d6f entity validations corrected 2025-09-13 18:29:03 +02:00
Sofiane Draris 804c9e92a4 corccetion validations entity 2025-09-13 18:27:04 +02:00
Sofiane Draris c3be8f9569 added enumname 2025-09-13 17:31:07 +02:00
Sofiane Draris cc8de8cba2 corrected contracts entity 2025-09-13 17:28:05 +02:00
Sofiane Draris b1925539a8 enfants entity corrected 2025-09-13 17:16:57 +02:00
Sofiane Draris 56bddbda7b IL Y A UN E EN TROP 2025-09-13 16:53:30 +02:00
Sofiane Draris 3d48f9f19b nounou entity correction 2025-09-13 16:50:46 +02:00
sdraris 8ef3deafb2 test login 2025-09-12 15:08:02 +02:00
sdraris d687b2344b role administrateur added 2025-09-12 14:58:19 +02:00
sdraris 3003002c9c min length commented for tests 2025-09-12 12:41:28 +02:00
sdraris 1fded57daa gestionnaire service corrected 2025-09-12 12:32:10 +02:00
sdraris 6c2ebe9cab dto test login 2025-09-12 12:09:54 +02:00
Hanim ae6ee552c8 correction spec.ts file 2025-09-12 12:03:41 +02:00
sdraris b0f3214a34 user service corrected 2025-09-12 12:00:29 +02:00
sdraris e038cab520 auth service corrected 2025-09-12 11:53:59 +02:00
sdraris a783c1a829 register dto correction 2025-09-12 11:50:26 +02:00
sdraris fa40962c09 user dto correction 2025-09-12 11:47:24 +02:00
sdraris ff1171950b prant_children entity correction 2025-09-12 11:29:32 +02:00
sdraris 5900b73556 parents entity correction 2025-09-12 10:46:58 +02:00
sdraris d462b2a2b0 user entity correction 2025-09-12 10:39:51 +02:00
sdraris 2afeef699b user entity corrected 2025-09-12 00:40:56 +02:00
49 changed files with 1019 additions and 349 deletions
+7
View File
@@ -55,3 +55,10 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.env
# Tests bdd
.vscode/
BDD.sql
migrations/
src/seed/
+5 -5
View File
@@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
@@ -34,7 +34,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
@@ -2322,9 +2322,9 @@
}
},
"node_modules/@nestjs/common": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz",
"integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==",
"version": "11.1.6",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz",
"integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==",
"license": "MIT",
"dependencies": {
"file-type": "21.0.0",
+2 -2
View File
@@ -22,7 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
@@ -47,7 +47,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
+5
View File
@@ -9,4 +9,9 @@ export class AppController {
getOverView() {
return this.appService.getOverView();
}
@Get('hello')
getHello(): string {
return this.appService.getHello();
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
return 'Hello Test!!!';
}
getOverView() {
+4 -9
View File
@@ -5,10 +5,6 @@ import { Request } from 'express';
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config";
interface AuthenticatedRequest extends Request {
user?: any;
}
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@@ -17,7 +13,6 @@ export class AuthGuard implements CanActivate {
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
@@ -34,13 +29,13 @@ export class AuthGuard implements CanActivate {
const token = authHeader.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token,
{ secret: this.configService.get<string>('jwt.secret') },
);
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.accessSecret'), // ✅ corrige ici
});
request.user = payload;
return true;
} catch (error) {
throw new UnauthorizedException('Token invalide ou expire');
throw new UnauthorizedException('Token invalide ou expiré');
}
}
}
+4 -2
View File
@@ -1,6 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRATION_TIME,
accessSecret: process.env.JWT_ACCESS_SECRET,
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES,
refreshSecret: process.env.JWT_REFRESH_SECRET,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES,
}));
+4 -2
View File
@@ -14,6 +14,8 @@ export const configValidationSchema = Joi.object({
POSTGRES_DB: Joi.string().required(),
// JWT
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
JWT_ACCESS_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRES: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_REFRESH_EXPIRES: Joi.string().required(),
});
+23 -14
View File
@@ -1,6 +1,4 @@
import {
Entity, PrimaryColumn, Column, OneToOne, JoinColumn
} from 'typeorm';
import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Users } from './users.entity';
@Entity('assistantes_maternelles')
@@ -9,22 +7,15 @@ export class AssistanteMaternelle {
@PrimaryColumn('uuid', { name: 'id_utilisateur' })
user_id: string;
@OneToOne(() => Users, user => user.assistanteMaternelle, { onDelete: 'CASCADE' })
@OneToOne(() => Users, (user) => user.assistanteMaternelle, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({ name: 'numero_agrement', length: 50, nullable: true })
approval_number?: string;
@Column({ name: 'date_naissance', type: 'date', nullable: true })
birthdate?: Date;
@Column({ name: 'ville_naissance', length: 100, nullable: true })
birthplace_city?: string;
@Column({ name: 'pays_naissance', length: 2, nullable: true })
birthplace_country?: string;
@Column({ name: 'nir_chiffre', length: 15, nullable: true })
nir?: string;
@@ -34,9 +25,27 @@ export class AssistanteMaternelle {
@Column({ name: 'biographie', type: 'text', nullable: true })
biography?: string;
@Column({ name: 'disponible', type: 'boolean', default: true, nullable: true })
@Column({
name: 'disponible',
type: 'boolean',
default: true,
nullable: true,
})
available?: boolean;
@Column({ name: 'ville_residence', length: 100, nullable: true })
residence_city?: string;
@Column( { name: 'date_agrement', type: 'date', nullable: true })
agreement_date?: Date;
@Column( { name: 'annee_experience', type: 'smallint', nullable: true })
years_experience?: number;
@Column( { name: 'specialite', length: 100, nullable: true })
specialty?: string;
@Column( { name: 'place_disponible', type: 'integer', nullable: true })
places_available?: number;
}
+4 -4
View File
@@ -47,21 +47,21 @@ export class Children {
gender?: GenreType;
@Column({ type: 'date', nullable: true, name: 'date_naissance' })
birthdate?: Date;
birth_date?: Date;
@Column({ type: 'date', nullable: true, name: 'date_prevue_naissance' })
due_date?: Date;
@Column({ nullable: true, name: 'photo_url' })
@Column({ nullable: true, name: 'photo_url', type: 'text' })
photo_url?: string;
@Column({ default: false, name: 'consentement_photo' })
@Column({ default: false, name: 'consentement_photo', type: 'boolean' })
consent_photo: boolean;
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
consent_photo_at?: Date;
@Column({ default: false, name: 'est_multiple' })
@Column({ default: false, name: 'est_multiple', type: 'boolean' })
is_multiple: boolean;
// Lien via table de jointure enfants_parents
+4
View File
@@ -28,9 +28,13 @@ export class Contrat {
@Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'indemnites_repas'})
meal_indemnity?: string;
@Column( { name: 'date_debut', type: 'date', nullable: true })
start_date?: Date;
@Column({
type: 'enum',
enum: StatutContratType,
enumName: 'statut_contrat_type',
default: StatutContratType.BROUILLON,
name: 'statut'
})
+1
View File
@@ -10,6 +10,7 @@ export enum StatutDossierType {
ENVOYE = 'envoye',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
CLOTURE = 'cloture',
}
@Entity('dossiers')
+1 -1
View File
@@ -6,7 +6,7 @@ import { Users } from './users.entity';
import { ParentsChildren } from './parents_children.entity';
import { Dossier } from './dossiers.entity';
@Entity('parents')
@Entity('parents', { schema: 'public' })
export class Parents {
// PK = FK vers utilisateurs.id
@PrimaryColumn('uuid', { name: 'id_utilisateur' })
+3 -3
View File
@@ -4,13 +4,13 @@ import {
import { Parents } from './parents.entity';
import { Children } from './children.entity';
@Entity('enfants_parents')
@Entity('enfants_parents', { schema: 'public' })
export class ParentsChildren {
@PrimaryColumn('uuid', { name: 'id_parent' })
id_parent: string;
parentId: string;
@PrimaryColumn('uuid', { name: 'id_enfant' })
id_enfant: string;
enfantId: string;
@ManyToOne(() => Parents, p => p.parentChildren, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
+48 -16
View File
@@ -13,6 +13,7 @@ export enum RoleType {
GESTIONNAIRE = 'gestionnaire',
SUPER_ADMIN = 'super_admin',
ASSISTANTE_MATERNELLE = 'assistante_maternelle',
ADMINISTRATEUR = 'administrateur',
}
//Enum pour definir le genre
@@ -29,23 +30,34 @@ export enum StatutUtilisateurType {
SUSPENDU = 'suspendu',
}
export enum SituationFamilialeType {
CELIBATAIRE = 'celibataire',
MARIE = 'marie',
DIVORCE = 'divorce',
VEUF = 'veuf',
PACSE = 'pacse',
SEPARE = 'separe',
PARENT_ISOLE = 'parent_isole',
CONCUBINAGE = 'concubinage',
}
//Declaration de l'entite utilisateur
@Entity('utilisateurs', { schema: 'public' })
export class Users {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true, name: 'courriel' })
@Column({ unique: true, name: 'email' })
email: string;
@Column({ name: 'mot_de_passe_hash' })
password_hash: string;
@Column({ name: 'password' })
password: string;
@Column({ name: 'prenom', nullable: true })
first_name?: string;
prenom?: string;
@Column({ name: 'nom', nullable: true })
last_name?: string;
nom?: string;
@Column({
type: 'enum',
@@ -54,7 +66,7 @@ export class Users {
nullable: true,
name: 'genre'
})
gender?: GenreType;
genre?: GenreType;
@Column({
type: 'enum',
@@ -71,37 +83,57 @@ export class Users {
default: StatutUtilisateurType.EN_ATTENTE,
name: 'statut'
})
status: StatutUtilisateurType;
statut: StatutUtilisateurType;
@Column({ type: 'enum',
enum: SituationFamilialeType,
enumName: 'situation_familiale_type',
nullable: true,
name: 'situation_familiale'
})
situation_familiale?: SituationFamilialeType;
@Column({ nullable: true, name: 'telephone' })
phone?: string;
telephone?: string;
@Column({ name: 'mobile', nullable: true })
mobile?: string;
@Column({ name: 'telephone_fixe', nullable: true })
telephone_fixe?: string;
@Column({ nullable: true, name: 'adresse' })
address?: string;
adresse?: string;
@Column({ nullable: true, name: 'photo_url' })
photo_url?: string;
@Column({ default: false, name: 'consentement_photo' })
consent_photo: boolean;
consentement_photo: boolean;
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
consent_photo_at?: Date;
date_consentement_photo?: Date;
@Column({ default: false, name: 'changement_mdp_obligatoire' })
must_change_password: boolean;
changement_mdp_obligatoire: boolean;
@Column({ nullable: true, name: 'ville' })
city?: string;
ville?: string;
@Column({ nullable: true, name: 'code_postal' })
postal_code?: string;
code_postal?: string;
@Column({ nullable: true, name: 'profession' })
profession?: string;
@Column({ name: 'date_naissance', type: 'date', nullable: true })
date_naissance?: Date;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
cree_le: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
modifie_le: Date;
// Relations
@OneToOne(() => AssistanteMaternelle, a => a.user)
+9 -1
View File
@@ -16,7 +16,7 @@ export class Validation {
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'varchar', length: 50, name: 'type' })
@Column({ type: 'varchar', length: 50, name: 'type', nullable: true })
type: string;
@Column({
@@ -28,9 +28,17 @@ export class Validation {
})
status: StatutValidationType;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'valide_par', referencedColumnName: 'id' })
validated_by?: Users;
@Column( { name: 'commentaire', type: 'text', nullable: true })
comment?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}
@@ -6,22 +6,28 @@ import {
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RoleType } from 'src/entities/users.entity';
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { AuthGuard } from 'src/common/guards/auth.guard';
@ApiTags("Assistantes Maternelles")
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, RolesGuard)
@Controller('assistantes-maternelles')
export class AssistantesMaternellesController {
constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) { }
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiResponse({ status: 201, description: 'Assistante maternelle créée avec succès' })
@ApiOperation({ summary: 'Créer nounou' })
@ApiResponse({ status: 201, description: 'Nounou créée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiBody({ type: CreateAssistanteDto })
@Post()
@@ -31,7 +37,8 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiResponse({ status: 200, description: 'Liste des assistantes maternelles' })
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
@ApiResponse({ status: 200, description: 'Liste des nounous' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getAll(): Promise<AssistanteMaternelle[]> {
return this.assistantesMaternellesService.findAll();
@@ -39,8 +46,10 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id')
@ApiResponse({ status: 200, description: 'Détails de l\'assistante maternelle' })
@ApiResponse({ status: 404, description: 'Assistante maternelle non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@ApiOperation({ summary: 'Récupérer une nounou par id' })
@ApiResponse({ status: 200, description: 'Détails de la nounou' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.findOne(user_id);
@@ -48,20 +57,13 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiBody({ type: UpdateAssistanteDto })
@ApiResponse({ status: 200, description: 'Assistante maternelle mise à jour avec succès' })
@ApiOperation({ summary: 'Mettre à jour une nounou' })
@ApiResponse({ status: 200, description: 'Nounou mise à jour avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiResponse({ status: 404, description: 'Assistante maternelle non trouvée' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.update(id, dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiResponse({ status: 200, description: 'Assistante maternelle supprimée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiResponse({ status: 404, description: 'Assistante maternelle non trouvée' })
@Delete(':id')
remove(@Param('id') id: string): Promise<void> {
return this.assistantesMaternellesService.remove(id);
}
}
@@ -34,15 +34,16 @@ export class AssistantesMaternellesService {
const entity = this.assistantesMaternelleRepository.create({
user_id: dto.user_id,
user: { ...user, role: RoleType.ASSISTANTE_MATERNELLE },
approval_number: dto.numero_agrement,
birthdate: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
birthplace_city: dto.ville_naissance,
birthplace_country: dto.pays_naissance,
nir: dto.nir_chiffre,
max_children: dto.nb_max_enfants,
biography: dto.biographie,
available: dto.disponible ?? true,
residence_city: dto.ville_residence,
approval_number: dto.approval_number,
nir: dto.nir,
max_children: dto.max_children,
biography: dto.biography,
available: dto.available ?? true,
residence_city: dto.residence_city,
agreement_date: dto.agreement_date ? new Date(dto.agreement_date) : undefined,
years_experience: dto.years_experience,
specialty: dto.specialty,
places_available: dto.places_available,
});
return this.assistantesMaternelleRepository.save(entity);
@@ -70,9 +71,4 @@ export class AssistantesMaternellesService {
await this.assistantesMaternelleRepository.update(id, dto);
return this.findOne(id);
}
// Suppression
async remove(id: string): Promise<void> {
await this.assistantesMaternelleRepository.delete(id);
}
}
+38 -14
View File
@@ -1,15 +1,23 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { LoginDto } from '../user/dto/login.dto';
import { Body, Controller, Get, Post, Req, UnauthorizedException, UseGuards } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from '../user/dto/register.dto';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { RegisterDto } from './dto/register.dto';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
import type { Request } from 'express';
import { UserService } from '../user/user.service';
import { ProfileResponseDto } from './dto/profile_response.dto';
import { RefreshTokenDto } from './dto/refresh_token.dto';
@ApiTags('Authentification')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) { }
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) { }
@Public()
@ApiOperation({ summary: 'Connexion' })
@@ -27,16 +35,32 @@ export class AuthController {
@Public()
@Post('refresh')
@ApiBearerAuth('refresh_token')
@ApiResponse({ status: 200, description: 'Nouveaux tokens générés avec succès.' })
@ApiResponse({ status: 401, description: 'Token de rafraîchissement invalide ou expiré.' })
@ApiOperation({ summary: 'Rafraichir les tokens' })
async refresh(@Body('refresh_token') refreshToken: string) {
return this.authService.refreshTokens(refreshToken);
async refresh(@Body() dto: RefreshTokenDto) {
return this.authService.refreshTokens(dto.refresh_token);
}
// @Get('me')
// @UseGuards(AuthGuard)
// @ApiBearerAuth('access-token')
// @ApiOperation({ summary: "Recuperer le profil de l'utilisateur connecte"})
// getProfile(@Request() req) {
// return req.user;
// }
@Get('me')
@UseGuards(AuthGuard)
@ApiBearerAuth('access-token')
@ApiOperation({ summary: "Récupérer le profil complet de l'utilisateur connecté" })
@ApiResponse({ status: 200, type: ProfileResponseDto })
async getProfile(@Req() req: Request): Promise<ProfileResponseDto> {
if (!req.user || !req.user.sub) {
throw new UnauthorizedException('Utilisateur non authentifié');
}
const user = await this.userService.findOne(req.user.sub);
return {
id: user.id,
email: user.email,
role: user.role,
prenom: user.prenom ?? '',
nom: user.nom ?? '',
statut: user.statut,
};
}
}
+30 -20
View File
@@ -6,10 +6,10 @@ import {
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { RegisterDto } from '../user/dto/register.dto';
import { RegisterDto } from './dto/register.dto';
import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { LoginDto } from '../user/dto/login.dto';
import { LoginDto } from './dto/login.dto';
import { DeepPartial } from 'typeorm';
@Injectable()
@@ -24,12 +24,14 @@ export class AuthService {
* Génère un access_token et un refresh_token
*/
async generateTokens(userId: string, email: string, role: RoleType) {
const secret = this.configService.get<string>('jwt.secret');
const expiresIn = this.configService.get<string>('jwt.expiresIn');
const accessSecret = this.configService.get<string>('jwt.accessSecret');
const accessExpiresIn = this.configService.get<string>('jwt.accessExpiresIn');
const refreshSecret = this.configService.get<string>('jwt.refreshSecret');
const refreshExpiresIn = this.configService.get<string>('jwt.refreshExpiresIn');
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync({ sub: userId, email, role }, { secret, expiresIn }),
this.jwtService.signAsync({ sub: userId }, { secret, expiresIn }),
this.jwtService.signAsync({ sub: userId, email, role }, { secret: accessSecret, expiresIn: accessExpiresIn }),
this.jwtService.signAsync({ sub: userId }, { secret: refreshSecret, expiresIn: refreshExpiresIn }),
]);
return {
@@ -46,13 +48,19 @@ export class AuthService {
const user = await this.usersService.findByEmailOrNull(dto.email);
if (!user) {
throw new UnauthorizedException('Identifiants invalides');
throw new UnauthorizedException('Email invalide');
}
console.log("Tentative login:", dto.email, JSON.stringify(dto.password));
console.log("Utilisateur trouvé:", user.email, user.password);
const isMatch = await bcrypt.compare(dto.password, user.password_hash);
const isMatch = await bcrypt.compare(dto.password, user.password);
console.log("Résultat bcrypt.compare:", isMatch);
if (!isMatch) {
throw new UnauthorizedException('Identifiants invalides');
throw new UnauthorizedException('Mot de passe invalide');
}
// if (user.password !== dto.password) {
// throw new UnauthorizedException('Mot de passe invalide');
// }
return this.generateTokens(user.id, user.email, user.role);
} catch (error) {
@@ -67,7 +75,7 @@ export class AuthService {
async refreshTokens(refreshToken: string) {
try {
const payload = await this.jwtService.verifyAsync(refreshToken, {
secret: this.configService.get<string>('jwt.refresh_token_secret'),
secret: this.configService.get<string>('jwt.refreshSecret'),
});
const user = await this.usersService.findOne(payload.sub);
@@ -95,19 +103,18 @@ export class AuthService {
registerDto.role = RoleType.PARENT;
}
registerDto.status = StatutUtilisateurType.EN_ATTENTE;
registerDto.statut = StatutUtilisateurType.EN_ATTENTE;
if (!registerDto.consent_photo) {
registerDto.consent_photo_at = null;
} else if (registerDto.consent_photo_at) {
const date = new Date(registerDto.consent_photo_at);
if (!registerDto.consentement_photo) {
registerDto.date_consentement_photo = null;
} else if (registerDto.date_consentement_photo) {
const date = new Date(registerDto.date_consentement_photo);
if (isNaN(date.getTime())) {
registerDto.consent_photo_at = null;
registerDto.date_consentement_photo = null;
}
}
const user = await this.usersService.createUser(registerDto);
const tokens = await this.generateTokens(user.id, user.email, user.role);
return {
@@ -116,11 +123,14 @@ export class AuthService {
id: user.id,
email: user.email,
role: user.role,
first_name: user.first_name,
last_name: user.last_name,
status: user.status,
prenom: user.prenom,
nom: user.nom,
statut: user.statut,
},
};
}
async logout(userId: string) {
// Pour une implémentation simple, on ne fait rien ici.
}
}
@@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsString, MaxLength, MinLength } from "class-validator";
export class LoginDto {
@ApiProperty({ example: 'my.user@example.com', description: "Adresse email de l'utililisateur" })
@ApiProperty({ example: 'mon.utilisateur@exemple.com', description: "Adresse email de l'utililisateur" })
@IsEmail()
email: string;
@@ -11,7 +11,7 @@ export class LoginDto {
description: "Mot de passe de l'utilisateur"
})
@IsString({ message: 'Le mot de passe doit etre une chaine de caracteres' })
@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caracteres' })
//@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caracteres' })
@MaxLength(50)
password: string;
}
@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
export class ProfileResponseDto {
@ApiProperty()
id: string;
@ApiProperty()
email: string;
@ApiProperty({ enum: RoleType })
role: RoleType;
@ApiProperty()
prenom?: string;
@ApiProperty()
nom?: string;
@ApiProperty({ enum: StatutUtilisateurType })
statut: StatutUtilisateurType;
}
+12
View File
@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";
export class RefreshTokenDto {
@ApiProperty({
description: 'Token de rafraîchissement',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
@IsString()
refresh_token: string;
}
@@ -1,14 +1,14 @@
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator';
import { CreateUserDto } from './create_user.dto';
import { CreateUserDto } from '../../user/dto/create_user.dto';
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
export class RegisterDto extends OmitType(CreateUserDto, ['must_change_password'] as const) {
export class RegisterDto extends OmitType(CreateUserDto, ['changement_mdp_obligatoire'] as const) {
@ApiProperty({ enum: [RoleType.ASSISTANTE_MATERNELLE, RoleType.PARENT], default: RoleType.PARENT })
@IsEnum(RoleType)
role: RoleType = RoleType.PARENT;
@IsEnum(StatutUtilisateurType)
@IsOptional()
status?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE;
statut?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE;
}
+4
View File
@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class DossiersModule {}
@@ -0,0 +1,63 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength, ValidateIf } from "class-validator";
import { GenreType, StatutEnfantType } from "src/entities/children.entity";
export class CreateEnfantsDto {
@ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.ACTIF })
@IsEnum(StatutEnfantType)
@IsNotEmpty()
status: StatutEnfantType;
@ApiProperty({ example: 'Georges', required: false })
@IsOptional()
@IsString()
@MaxLength(100)
first_name?: string;
@ApiProperty({ example: 'Lucas', required: false })
@IsOptional()
@IsString()
@MaxLength(100)
last_name?: string;
@ApiProperty({ enum: GenreType, required: false })
@IsOptional()
@IsEnum(GenreType)
gender?: GenreType;
@ApiProperty({ example: '2018-06-24', required: false })
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)
@IsOptional()
@IsDateString()
birth_date?: string;
@ApiProperty({ example: '2024-12-24', required: false })
@ValidateIf(o => o.status === StatutEnfantType.A_NAITRE)
@IsOptional()
@IsDateString()
due_date?: string;
@ApiProperty({ example: 'https://monimage.com/photo.jpg', required: false })
@IsOptional()
@IsString()
photo_url?: string;
@ApiProperty({ default: false })
@IsBoolean()
consent_photo: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString()
consent_photo_at?: string;
@ApiProperty({ default: false })
@IsBoolean()
is_multiple: boolean;
@ApiProperty({ example: 'UUID-parent' })
@IsUUID()
@IsNotEmpty()
id_parent: string;
}
@@ -0,0 +1,38 @@
import { ApiProperty } from "@nestjs/swagger";
import { GenreType, StatutEnfantType } from "src/entities/children.entity";
export class EnfantResponseDto {
@ApiProperty({ example: 'UUID-enfant' })
id: string;
@ApiProperty({ enum: StatutEnfantType })
status: StatutEnfantType;
@ApiProperty({ example: 'Georges', required: false })
first_name?: string;
@ApiProperty({ example: 'Dupont', required: false })
last_name?: string;
@ApiProperty({ enum: GenreType, required: false })
gender?: GenreType;
@ApiProperty({ example: '2018-06-24', required: false })
birth_date?: string;
@ApiProperty({ example: '2024-12-24', required: false })
due_date?: string;
@ApiProperty({ example: 'https://monimage.com/photo.jpg', required: false })
photo_url?: string;
@ApiProperty({ example: false })
consent_photo: boolean;
@ApiProperty({ example: false })
is_multiple: boolean;
@ApiProperty({ example: 'UUID-parent' })
parent_id: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateEnfantsDto } from "./create_enfants.dto";
export class UpdateEnfantsDto extends PartialType(CreateEnfantsDto) {}
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnfantsController } from './enfants.controller';
describe('EnfantsController', () => {
let controller: EnfantsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EnfantsController],
}).compile();
controller = module.get<EnfantsController>(EnfantsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+77
View File
@@ -0,0 +1,77 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { EnfantsService } from './enfants.service';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RoleType } from 'src/entities/users.entity';
import { Children } from 'src/entities/children.entity';
import { CreateEnfantsDto } from './dto/create_enfants.dto';
import { EnfantResponseDto } from './dto/enfants_response.dto';
import { mapEnfantsToResponseDto, mapEnfantToResponseDto } from './enfants.mapper';
@ApiBearerAuth()
@ApiTags('Enfants')
@UseGuards(RolesGuard)
@Controller('enfants')
export class EnfantsController {
constructor(private readonly enfantsService: EnfantsService) { }
// Inscrire un enfant
@Post()
@ApiOperation({ summary: 'Inscrire un enfant' })
@ApiResponse({ status: 201, type: EnfantResponseDto, description: 'L\'enfant a été inscrit avec succès.' })
@Roles(RoleType.PARENT, RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
async create(@Body() dto: CreateEnfantsDto): Promise<EnfantResponseDto> {
const enfant = await this.enfantsService.create(dto);
// Mapper l'entité enfant vers le DTO de réponse
return mapEnfantToResponseDto(enfant);
}
// Récupérer tous les enfants avec leurs liens parents
@ApiOperation({ summary: 'Récupérer tous les enfants avec leurs liens parents' })
@ApiResponse({ status: 200, type: [EnfantResponseDto], description: 'Liste de tous les enfants avec leurs liens parents.' })
@Get()
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
async findAll(): Promise<EnfantResponseDto[]> {
const enfants = await this.enfantsService.findAll();
// Mapper les entités enfants vers les DTO de réponse
return mapEnfantsToResponseDto(enfants);
}
// Récupérer un enfant par son ID
@Get(':id')
@ApiOperation({ summary: 'Récupérer un enfant par son ID' })
@ApiResponse({ status: 200, type: EnfantResponseDto, description: 'Détails de l\'enfant avec ses liens parents.' })
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN, RoleType.PARENT)
async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<EnfantResponseDto> {
const enfant = await this.enfantsService.findOne(id);
// Mapper l'entité enfant vers le DTO de réponse
return mapEnfantToResponseDto(enfant);
}
// Mettre à jour un enfant
@Patch(':id')
@ApiOperation({ summary: 'Mettre à jour un enfant' })
@ApiResponse({ status: 200, type: EnfantResponseDto, description: 'L\'enfant a été mis à jour avec succès.' })
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
async update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() dto: Partial<CreateEnfantsDto>,
): Promise<EnfantResponseDto> {
const enfant = await this.enfantsService.update(id, dto);
// Mapper l'entité enfant vers le DTO de réponse
return mapEnfantToResponseDto(enfant);
}
// Supprimer un enfant
@Delete(':id')
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Supprimer un enfant' })
@ApiResponse({ status: 204, description: 'L\'enfant a été supprimé avec succès.' })
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.enfantsService.remove(id);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Children } from "src/entities/children.entity";
import { EnfantResponseDto } from "./dto/enfants_response.dto";
// Fonction pour mapper une entité Children vers EnfantResponseDto
export function mapEnfantToResponseDto(enfant: Children): EnfantResponseDto {
return {
id: enfant.id,
status: enfant.status,
first_name: enfant.first_name,
last_name: enfant.last_name,
gender: enfant.gender,
birth_date: enfant.birth_date?.toISOString(),
due_date: enfant.due_date?.toISOString(),
photo_url: enfant.photo_url,
consent_photo: enfant.consent_photo,
is_multiple: enfant.is_multiple,
parent_id: enfant.parentLinks?.[0]?.parentId ?? null,
};
}
export function mapEnfantsToResponseDto(enfants: Children[]): EnfantResponseDto[] {
return enfants.map(mapEnfantToResponseDto);
};
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { EnfantsController } from './enfants.controller';
import { EnfantsService } from './enfants.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Children } from 'src/entities/children.entity';
@Module({
imports: [TypeOrmModule.forFeature([Children])],
controllers: [EnfantsController],
providers: [EnfantsService],
})
export class EnfantsModule {}
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnfantsService } from './enfants.service';
describe('EnfantsService', () => {
let service: EnfantsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EnfantsService],
}).compile();
service = module.get<EnfantsService>(EnfantsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+110
View File
@@ -0,0 +1,110 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Children, StatutEnfantType } from 'src/entities/children.entity';
import { Parents } from 'src/entities/parents.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { Repository } from 'typeorm';
import { CreateEnfantsDto } from './dto/create_enfants.dto';
@Injectable()
export class EnfantsService {
constructor(
@InjectRepository(Children)
private readonly childrenRepository: Repository<Children>,
@InjectRepository(Parents)
private readonly parentsRepository: Repository<Parents>,
@InjectRepository(ParentsChildren)
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
) { }
// Inscruire un enfant
async create(dto: CreateEnfantsDto): Promise<Children> {
// Vérifier que le parent existe
const parent = await this.parentsRepository.findOne({
where: { user_id: dto.id_parent },
});
// Si le parent n'existe pas, lever une exception
if (!parent) {
throw new NotFoundException(`Id de parent : ${dto.id_parent} introuvable`);
}
// Evolution future : rendre l'option photo obligatoire ou non configurable
const optionObligatoire = false;
// Si l'enfant est pas a naitre, vérifier qu'une photo est fournie
// Puis si l'option est obligatoire
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.photo_url && optionObligatoire) {
throw new BadRequestException(`Pour un enfant actif, une photo est obligatoire`);
}
// Créer l'enfant
const child = this.childrenRepository.create({
status: dto.status,
first_name: dto.first_name,
last_name: dto.last_name,
gender: dto.gender,
birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined,
due_date: dto.due_date ? new Date(dto.due_date) : undefined,
photo_url: dto.photo_url,
consent_photo: dto.consent_photo,
consent_photo_at: dto.consent_photo_at ? new Date(dto.consent_photo_at) : undefined,
is_multiple: dto.is_multiple,
});
await this.childrenRepository.save(child);
// Créer le lien entre le parent et l'enfant
const parentLink = this.parentsChildrenRepository.create({
parentId: parent.user_id,
enfantId: child.id,
});
await this.parentsChildrenRepository.save(parentLink);
return this.findOne(child.id);
}
// Récupérer tous les enfants avec leurs liens parents
async findAll(): Promise<Children[]> {
const all_children = await this.childrenRepository.find({
relations: [
'parentLinks',
'parentLinks.parent',
'parentLinks.parent.user',
],
order: { last_name: 'ASC', first_name: 'ASC' }
});
return all_children;
}
// Récupérer un enfant par son id avec ses liens parents
async findOne(id: string): Promise<Children> {
const child = await this.childrenRepository.findOne({
where: { id },
relations: [
'parentLinks',
'parentLinks.parent',
'parentLinks.parent.user',
],
});
if (!child) {
throw new NotFoundException(`Id d'enfant : ${id} introuvable`);
}
return child;
}
// Mettre à jour un enfant
async update(id: string, dto: Partial<CreateEnfantsDto>): Promise<Children> {
const child = await this.childrenRepository.findOne({ where: { id } });
if (!child) {
throw new NotFoundException(`Id d'enfant : ${id} introuvable`);
}
const { id_parent, ...childData } = dto;
await this.childrenRepository.update(id, childData);
return this.findOne(id);
}
// Supprimer un enfant
async remove(id: string): Promise<void> {
await this.childrenRepository.delete(id);
}
}
@@ -1,52 +0,0 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { GestionnairesService } from './gestionnaires.service';
import { RoleType, Users } from 'src/entities/users.entity';
import { Roles } from 'src/common/decorators/roles.decorator';
import { UpdateGestionnaireDto } from '../user/dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../user/dto/create_gestionnaire.dto';
@Controller('gestionnaires')
export class GestionnairesController {
constructor(private readonly gestionnairesService: GestionnairesService) { }
@Roles(RoleType.SUPER_ADMIN)
@Post()
create(@Body() dto: CreateGestionnaireDto): Promise<Users> {
return this.gestionnairesService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
getAll(): Promise<Users[]> {
return this.gestionnairesService.findAll();
}
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
@Get(':id')
findOne(@Param('id') id: string): Promise<Users> {
return this.gestionnairesService.findOne(id);
}
@Roles(RoleType.SUPER_ADMIN)
@Patch(':id')
update(
@Param('id') id: string,
@Body() dto: UpdateGestionnaireDto,
): Promise<Users | null> {
return this.gestionnairesService.update(id, dto);
}
@Roles(RoleType.SUPER_ADMIN)
@Delete(':id')
remove(@Param('id') id: string): Promise<void> {
return this.gestionnairesService.remove(id);
}
}
-9
View File
@@ -52,13 +52,4 @@ export class ParentsController {
update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
return this.parentsService.update(id, dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Delete(':id')
@ApiResponse({ status: 200, description: 'Parent supprimé avec succès' })
@ApiResponse({ status: 404, description: 'Parent introuvable' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
remove(@Param('id') id: string): Promise<void> {
return this.parentsService.remove(id);
}
}
-5
View File
@@ -71,9 +71,4 @@ export class ParentsService {
await this.parentsRepository.update(id, dto);
return this.findOne(id);
}
// Suppression
async remove(id: string): Promise<void> {
await this.parentsRepository.delete(id);
}
}
+34 -31
View File
@@ -1,53 +1,56 @@
import { OmitType } from "@nestjs/swagger";
import { IsBoolean, IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Length, Matches, Max, Min } from "class-validator";
import { CreateUserDto } from "./create_user.dto";
import { IsBoolean, IsDateString, IsInt, IsOptional, IsString, IsUUID, Length, Matches, Max, Min } from "class-validator";
export class CreateAssistanteDto extends OmitType(CreateUserDto, ['role'] as const) {
export class CreateAssistanteDto extends OmitType(CreateUserDto, ['role', 'photo_url', 'consentement_photo'] as const) {
@IsUUID()
@IsOptional()
@IsNotEmpty()
user_id?: string;
@IsString()
@IsOptional()
@IsNotEmpty()
@Length(1, 50)
numero_agrement?: string;
@IsDateString()
@IsOptional()
date_naissance?: string;
@IsString()
@IsOptional()
@Length(1, 100)
ville_naissance?: string;
@IsString()
@IsOptional()
@Length(2, 2)
pays_naissance?: string;
approval_number: string;
@Matches(/^\d{15}$/)
@IsOptional()
nir_chiffre?: string;
@IsNotEmpty()
nir: string;
@IsInt()
@Min(1)
@Max(10)
@IsOptional()
nb_max_enfants?: number;
@IsNotEmpty()
max_children: number;
@IsOptional()
@IsString()
biographie?: string;
@IsNotEmpty()
photo_url: string;
@IsOptional()
@IsBoolean()
disponible?: boolean;
@IsNotEmpty()
consentement_photo: boolean;
@IsDateString()
@IsNotEmpty()
agreement_date: string;
@IsString()
@IsNotEmpty()
@Length(1, 100)
residence_city: string;
@IsOptional()
@IsString()
@Length(1, 100)
ville_residence?: string;
biography?: string;
@IsOptional()
available?: boolean;
@IsOptional()
years_experience?: number;
@IsOptional()
specialty?: string;
@IsOptional()
places_available?: number;
}
+7 -4
View File
@@ -1,14 +1,17 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
import { IsNotEmpty, IsOptional, IsUUID } from "class-validator";
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
export class CreateParentDto extends OmitType(CreateUserDto, ['role'] as const) {
export class CreateParentDto extends OmitType(CreateUserDto, ['role', 'photo_url'] as const) {
@IsUUID()
@IsOptional()
@IsNotEmpty()
user_id?: string;
user_id: string;
@IsOptional()
@IsUUID()
co_parent_id?: string;
@IsString()
@IsNotEmpty()
photo_url: string;
}
+28 -18
View File
@@ -10,7 +10,7 @@ import {
MinLength,
MaxLength,
} from 'class-validator';
import { RoleType, GenreType, StatutUtilisateurType } from 'src/entities/users.entity';
import { RoleType, GenreType, StatutUtilisateurType, SituationFamilialeType } from 'src/entities/users.entity';
export class CreateUserDto {
@ApiProperty({ example: 'sosso.test@example.com' })
@@ -20,25 +20,26 @@ export class CreateUserDto {
@ApiProperty({ minLength: 6, example: 'Mon_motdepasse_fort_1234?' })
@IsString()
@IsNotEmpty()
@MinLength(6)
password: string;
@ApiProperty({ example: 'Julien' })
@IsString()
@IsOptional()
@IsNotEmpty()
@MaxLength(100)
first_name?: string;
prenom: string;
@ApiProperty({ example: 'Dupont' })
@IsString()
@IsOptional()
@IsNotEmpty()
@MaxLength(100)
last_name?: string;
nom: string;
@ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE })
@IsOptional()
@IsEnum(GenreType)
gender?: GenreType = GenreType.AUTRE;
genre?: GenreType = GenreType.AUTRE;
@ApiProperty({ enum: RoleType })
@IsEnum(RoleType)
@@ -47,31 +48,35 @@ export class CreateUserDto {
@ApiProperty({ enum: StatutUtilisateurType, required: false, default: StatutUtilisateurType.EN_ATTENTE })
@IsOptional()
@IsEnum(StatutUtilisateurType)
status?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE;
statut?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE;
@ApiProperty({ example: SituationFamilialeType.MARIE, required: false, enum: SituationFamilialeType, default: SituationFamilialeType.MARIE})
@IsOptional()
@IsEnum(SituationFamilialeType)
situation_familiale?: SituationFamilialeType;
@ApiProperty({ example: '+33123456789' })
@IsString()
@IsOptional()
@IsNotEmpty()
@MaxLength(20)
phone?: string;
telephone: string;
@ApiProperty({ example: 'Paris', required: false })
@IsOptional()
@IsString()
@MaxLength(150)
city?: string;
ville?: string;
@ApiProperty({ example: '75000', required: false })
@IsOptional()
@IsString()
@MaxLength(10)
postal_code?: string;
code_postal?: string;
@ApiProperty({ example: '10 rue de la paix, 75000 Paris' })
@IsString()
@IsOptional()
address?: string;
@IsNotEmpty()
adresse: string;
@ApiProperty({ example: 'https://example.com/photo.jpg', required: false })
@IsOptional()
@@ -81,15 +86,20 @@ export class CreateUserDto {
@ApiProperty({ default: false })
@IsOptional()
@IsBoolean()
consent_photo?: boolean = false;
consentement_photo?: boolean = false;
@ApiProperty({ required: false })
@IsOptional()
@IsDateString({}, { message: 'consent_photo_at doit être une date ISO valide' })
consent_photo_at?: string | null;
@IsDateString({}, { message: 'date_consentement_photo doit être une date ISO valide' })
date_consentement_photo?: string | null;
@ApiProperty({ default: false })
@IsOptional()
@IsBoolean()
must_change_password?: boolean = false;
changement_mdp_obligatoire?: boolean = false;
@ApiProperty({ example: true })
@IsBoolean()
@IsNotEmpty()
cguAccepted: boolean;
}
@@ -0,0 +1,73 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { GestionnairesService } from './gestionnaires.service';
import { RoleType, Users } from 'src/entities/users.entity';
import { Roles } from 'src/common/decorators/roles.decorator';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
@ApiTags('Gestionnaires')
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, RolesGuard)
@Controller('gestionnaires')
export class GestionnairesController {
constructor(private readonly gestionnairesService: GestionnairesService) { }
@Roles(RoleType.SUPER_ADMIN)
@ApiResponse({ status: 201, description: 'Le gestionnaire a été créé avec succès.', type: Users })
@ApiResponse({ status: 409, description: 'Conflit. L\'email est déjà utilisé.' })
@ApiOperation({ summary: 'Création d\'un gestionnaire' })
@ApiBody({ type: CreateGestionnaireDto })
@Post()
create(@Body() dto: CreateGestionnaireDto): Promise<Users> {
return this.gestionnairesService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Liste des gestionnaires' })
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
@Get()
getAll(): Promise<Users[]> {
return this.gestionnairesService.findAll();
}
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Récupérer un gestionnaire par ID' })
@ApiResponse({ status: 400, description: 'ID invalide' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 401, description: 'Non authentifié' })
@ApiParam({ name: 'id', description: 'ID du gestionnaire' })
@ApiResponse({ status: 200, description: 'Gestionnaire trouvé', type: Users })
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
@Get(':id')
findOne(@Param('id') id: string): Promise<Users> {
return this.gestionnairesService.findOne(id);
}
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Mettre à jour un gestionnaire' })
@ApiResponse({ status: 200, description: 'Le gestionnaire a été mis à jour avec succès.', type: Users })
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 401, description: 'Non authentifié' })
@ApiParam({ name: 'id', description: 'ID du gestionnaire' })
@Patch(':id')
update(
@Param('id') id: string,
@Body() dto: UpdateGestionnaireDto,
): Promise<Users | null> {
return this.gestionnairesService.update(id, dto);
}
}
@@ -1,14 +1,13 @@
import {
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RoleType, Users } from 'src/entities/users.entity';
import { CreateGestionnaireDto } from '../user/dto/create_gestionnaire.dto';
import { UpdateGestionnaireDto } from '../user/dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
@@ -24,24 +23,25 @@ export class GestionnairesService {
if (exist) throw new ConflictException('Email déjà utilisé');
const salt = await bcrypt.genSalt();
const password_hash = await bcrypt.hash(dto.password, salt);
const hashedPassword = await bcrypt.hash(dto.password, salt);
const entity = this.gestionnaireRepository.create({
email: dto.email,
password_hash,
first_name: dto.first_name,
last_name: dto.last_name,
gender: dto.gender,
status: dto.status,
phone: dto.phone,
address: dto.address,
password: hashedPassword,
prenom: dto.prenom,
nom: dto.nom,
genre: dto.genre,
statut: dto.statut,
telephone: dto.telephone,
adresse: dto.adresse,
photo_url: dto.photo_url,
consent_photo: dto.consent_photo ?? false,
consent_photo_at: dto.consent_photo_at ? new Date(dto.consent_photo_at) : undefined,
must_change_password: dto.must_change_password ?? false,
consentement_photo: dto.consentement_photo ?? false,
date_consentement_photo: dto.date_consentement_photo
? new Date(dto.date_consentement_photo)
: undefined,
changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false,
role: RoleType.GESTIONNAIRE,
});
return this.gestionnaireRepository.save(entity);
}
@@ -60,29 +60,28 @@ export class GestionnairesService {
}
// Mise à jour dun gestionnaire
async update(id: string, dto: UpdateGestionnaireDto): Promise<Users | null> {
async update(id: string, dto: UpdateGestionnaireDto): Promise<Users> {
const gestionnaire = await this.findOne(id);
if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable');
if (dto.password) {
const salt = await bcrypt.genSalt();
gestionnaire.password_hash = await bcrypt.hash(dto.password, salt);
gestionnaire.password = await bcrypt.hash(dto.password, salt);
}
if (dto.consent_photo_at !== undefined) {
gestionnaire.consent_photo_at = dto.consent_photo_at
? new Date(dto.consent_photo_at)
if (dto.date_consentement_photo !== undefined) {
gestionnaire.date_consentement_photo = dto.date_consentement_photo
? new Date(dto.date_consentement_photo)
: undefined;
}
Object.assign(gestionnaire, dto);
const { password, date_consentement_photo, ...rest } = dto;
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined) {
(gestionnaire as any)[key] = value;
}
});
return this.gestionnaireRepository.save(gestionnaire);
}
// Suppression dun gestionnaire
async remove(id: string): Promise<void> {
const gestionnaire = await this.findOne(id);
if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable');
await this.gestionnaireRepository.delete(id);
}
}
+32 -5
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { Roles } from 'src/common/decorators/roles.decorator';
import { User } from 'src/common/decorators/user.decorator';
@@ -56,12 +56,39 @@ export class UserController {
return this.userService.updateUser(id, dto, currentUser);
}
// Supprimer un utilisateur (super_admin et gestionnaire)
@Patch(':id/valider')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Valider un compte utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
@ApiResponse({ status: 400, description: 'ID invalide' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 200, description: 'Compte validé avec succès' })
validate(
@Param('id') id: string,
@User() currentUser: Users,
@Body('comment') comment?: string,
) {
return this.userService.validateUser(id, currentUser, comment);
}
@Patch(':id/suspendre')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Suspendre un compte utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
suspend(
@Param('id') id: string,
@User() currentUser: Users,
@Body('comment') comment?: string,
) {
return this.userService.suspendUser(id, currentUser, comment);
}
// Supprimer un utilisateur (super_admin uniquement)
@Delete(':id')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Supprimer un utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
remove(@Param('id') id: string) {
return this.userService.remove(id);
remove(@Param('id') id: string, @User() currentUser: Users) {
return this.userService.remove(id, currentUser);
}
}
+2 -1
View File
@@ -4,9 +4,10 @@ import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from 'src/entities/users.entity';
import { AuthModule } from '../auth/auth.module';
import { Validation } from 'src/entities/validations.entity';
@Module({
imports: [TypeOrmModule.forFeature([Users]),
imports: [TypeOrmModule.forFeature([Users, Validation]),
forwardRef(() => AuthModule),
],
controllers: [UserController],
+154 -37
View File
@@ -1,70 +1,119 @@
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { RoleType, Users } from "src/entities/users.entity";
import { Repository } from "typeorm";
import { RoleType, StatutUtilisateurType, Users } from "src/entities/users.entity";
import { In, Repository } from "typeorm";
import { CreateUserDto } from "./dto/create_user.dto";
import { UpdateUserDto } from "./dto/update_user.dto";
import * as bcrypt from 'bcrypt';
import { StatutValidationType, Validation } from "src/entities/validations.entity";
import { Parents } from "src/entities/parents.entity";
import { AssistanteMaternelle } from "src/entities/assistantes_maternelles.entity";
@Injectable()
export class UserService {
constructor(
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>
private readonly usersRepository: Repository<Users>,
@InjectRepository(Validation)
private readonly validationRepository: Repository<Validation>,
@InjectRepository(Parents)
private readonly parentsRepository: Repository<Parents>,
@InjectRepository(AssistanteMaternelle)
private readonly assistantesRepository: Repository<AssistanteMaternelle>
) { }
// Création utilisateur
async createUser(dto: CreateUserDto, currentUser?: Users): Promise<Users> {
// Sécuriser le rôle
if (!currentUser || currentUser.role !== RoleType.SUPER_ADMIN) {
dto.role = RoleType.PARENT;
if (!dto.cguAccepted) {
throw new BadRequestException(
'Vous devez accepter les CGU et la Politique de confidentialité pour créer un compte.',
);
}
// Nettoyage / validation consentement photo
const exist = await this.usersRepository.findOneBy({ email: dto.email });
if (exist) throw new BadRequestException('Email déjà utilisé');
const isSuperAdmin = currentUser?.role === RoleType.SUPER_ADMIN;
const isAdmin = currentUser?.role === RoleType.ADMINISTRATEUR;
let role: RoleType;
if (dto.role === RoleType.GESTIONNAIRE) {
if (!isAdmin && !isSuperAdmin) {
throw new ForbiddenException('Seuls les administrateurs peuvent créer un gestionnaire');
}
role = RoleType.GESTIONNAIRE;
} else if (dto.role === RoleType.ADMINISTRATEUR) {
if (!isAdmin && !isSuperAdmin) {
throw new ForbiddenException('Seuls les administrateurs peuvent créer un administrateur');
}
role = RoleType.ADMINISTRATEUR;
} else if (dto.role === RoleType.ASSISTANTE_MATERNELLE) {
role = RoleType.ASSISTANTE_MATERNELLE;
if (!dto.photo_url) {
throw new BadRequestException(
'La photo de profil est obligatoire pour les assistantes maternelles.',
);
}
} else {
role = RoleType.PARENT;
}
const statut = isSuperAdmin
? dto.statut ?? StatutUtilisateurType.EN_ATTENTE
: StatutUtilisateurType.EN_ATTENTE;
if (!dto.nom?.trim()) throw new BadRequestException('Nom est obligatoire.');
if (!dto.prenom?.trim()) throw new BadRequestException('Prénom est obligatoire.');
if (!dto.adresse?.trim()) throw new BadRequestException('Adresse est obligatoire.');
if (!dto.telephone?.trim()) throw new BadRequestException('Téléphone est obligatoire.');
let consentDate: Date | undefined;
if (dto.consent_photo && dto.consent_photo_at) {
const parsed = new Date(dto.consent_photo_at);
if (dto.consentement_photo && dto.date_consentement_photo) {
const parsed = new Date(dto.date_consentement_photo);
if (!isNaN(parsed.getTime())) {
consentDate = parsed;
}
}
// Hash mot de passe
const salt = await bcrypt.genSalt();
const password_hash = await bcrypt.hash(dto.password, salt);
const hashedPassword = await bcrypt.hash(dto.password, salt);
const entity = this.usersRepository.create({
email: dto.email,
password_hash,
first_name: dto.first_name,
last_name: dto.last_name,
role: dto.role,
status: dto.status,
gender: dto.gender,
phone: dto.phone,
address: dto.address,
password: hashedPassword,
prenom: dto.prenom,
nom: dto.nom,
role,
statut,
genre: dto.genre,
telephone: dto.telephone,
ville: dto.ville,
code_postal: dto.code_postal,
adresse: dto.adresse,
photo_url: dto.photo_url,
consent_photo: dto.consent_photo ?? false,
consent_photo_at: consentDate,
must_change_password: dto.must_change_password ?? false
consentement_photo: dto.consentement_photo ?? false,
date_consentement_photo: consentDate,
changement_mdp_obligatoire:
role === RoleType.ADMINISTRATEUR || role === RoleType.GESTIONNAIRE
? true
: dto.changement_mdp_obligatoire ?? false,
});
const saved = await this.usersRepository.save(entity);
return this.findOne(saved.id);
}
//Trouver tous les utilisateurs
async findAll(): Promise<Users[]> {
return this.usersRepository.find();
}
// Trouver utilisateur par condition
async findOneBy(where: Partial<Users>): Promise<Users | null> {
return this.usersRepository.findOne({ where });
}
// Trouver utilisateur par ID
async findOne(id: string): Promise<Users> {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
@@ -73,12 +122,10 @@ export class UserService {
return user;
}
// Trouver utilisateur par email
async findByEmailOrNull(email: string): Promise<Users | null> {
return this.usersRepository.findOne({ where: { email } });
}
// Mettre à jour un utilisateur
async updateUser(id: string, dto: UpdateUserDto, currentUser: Users): Promise<Users> {
const user = await this.findOne(id);
@@ -87,27 +134,97 @@ export class UserService {
throw new ForbiddenException('Accès réservé aux super admins');
}
// Empêcher de modifier le flag changement_mdp_obligatoire pour admin/gestionnaire
if (
(user.role === RoleType.ADMINISTRATEUR || user.role === RoleType.GESTIONNAIRE) &&
dto.changement_mdp_obligatoire === false
) {
throw new ForbiddenException(
'Impossible de désactiver lobligation de changement de mot de passe pour ce rôle',
);
}
// Gestion du mot de passe
if (dto.password) {
const salt = await bcrypt.genSalt();
user.password_hash = await bcrypt.hash(dto.password, salt);
user.password = await bcrypt.hash(dto.password, salt);
delete (dto as any).password;
// Une fois le mot de passe changé, on peut lever lobligation
user.changement_mdp_obligatoire = false;
}
// Conversion de la date de consentement
if (dto.consent_photo_at !== undefined) {
user.consent_photo_at = dto.consent_photo_at
? new Date(dto.consent_photo_at)
if (dto.date_consentement_photo !== undefined) {
user.date_consentement_photo = dto.date_consentement_photo
? new Date(dto.date_consentement_photo)
: undefined;
delete (dto as any).consent_photo_at;
delete (dto as any).date_consentement_photo;
}
Object.assign(user, dto);
return this.usersRepository.save(user);
}
// Supprimer un utilisateur
async remove(id: string): Promise<void> {
// Valider un compte utilisateur
async validateUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
}
const user = await this.usersRepository.findOne({ where: { id: user_id } });
if (!user) throw new NotFoundException('Utilisateur introuvable');
user.statut = StatutUtilisateurType.ACTIF;
const savedUser = await this.usersRepository.save(user);
if (user.role === RoleType.PARENT) {
const existParent = await this.parentsRepository.findOneBy({ user_id: user.id });
if (!existParent) {
const parentEntity = this.parentsRepository.create({ user_id: user.id, user });
await this.parentsRepository.save(parentEntity);
}
} else if (user.role === RoleType.ASSISTANTE_MATERNELLE) {
const existAssistante = await this.assistantesRepository.findOneBy({ user_id: user.id });
if (!existAssistante) {
const assistanteEntity = this.assistantesRepository.create({ user_id: user.id, user });
await this.assistantesRepository.save(assistanteEntity);
}
}
const validation = this.validationRepository.create({
user: savedUser,
type: 'validation_compte',
status: StatutValidationType.VALIDE,
validated_by: currentUser,
comment,
});
await this.validationRepository.save(validation);
return savedUser;
}
// Mettre un compte en statut suspendu
async suspendUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
}
const user = await this.usersRepository.findOne({ where: { id: user_id } });
if (!user) throw new NotFoundException('Utilisateur introuvable');
user.statut = StatutUtilisateurType.SUSPENDU;
const savedUser = await this.usersRepository.save(user);
const suspend = this.validationRepository.create({
user: savedUser,
type: 'suspension_compte',
status: StatutValidationType.VALIDE,
validated_by: currentUser,
comment,
})
await this.validationRepository.save(suspend);
return savedUser;
}
async remove(id: string, currentUser: Users): Promise<void> {
if (currentUser.role !== RoleType.SUPER_ADMIN) {
throw new ForbiddenException('Accès réservé aux super admins');
}
const result = await this.usersRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException('Utilisateur introuvable');
+5 -1
View File
@@ -2,6 +2,10 @@ import { Users } from 'src/entities/users.entity';
declare module 'express-serve-static-core' {
interface Request {
user?: Users | any;
user?: Users & {
sub?: string;
email?: string;
role?: string;
};
}
}