Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

45 changed files with 240 additions and 805 deletions

22
.github/workflow/deploy.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Deploy Backend
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Déployer sur le serveur
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /home/ynov/project
git pull origin main
docker compose build backend
docker compose up -d backend

7
.gitignore vendored
View File

@ -55,10 +55,3 @@ 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/

10
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^11.1.6",
"@nestjs/common": "^11.0.1",
"@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.10",
"@nestjs/cli": "^11.0.0",
"@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.6",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz",
"integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==",
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz",
"integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==",
"license": "MIT",
"dependencies": {
"file-type": "21.0.0",

View File

@ -22,7 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.1.6",
"@nestjs/common": "^11.0.1",
"@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.10",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",

View File

@ -13,7 +13,6 @@ import { ParentsModule } from './routes/parents/parents.module';
import { AuthModule } from './routes/auth/auth.module';
import { SentryGlobalFilter } from '@sentry/nestjs/setup';
import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
import { EnfantsModule } from './routes/enfants/enfants.module';
@Module({
imports: [
@ -47,7 +46,6 @@ import { EnfantsModule } from './routes/enfants/enfants.module';
}),
UserModule,
ParentsModule,
EnfantsModule,
AuthModule,
],
controllers: [AppController],

View File

@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello Test!!!';
return 'Hello World!';
}
getOverView() {

View File

@ -5,45 +5,42 @@ 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(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {}
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest<Request>();
if (request.path.startsWith('/api-docs')) {
return true;
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.headers['authorization'] as string | undefined;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token manquant ou invalide');
}
const token = authHeader.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token,
{ secret: this.configService.get<string>('jwt.secret') },
);
request.user = payload;
return true;
} catch (error) {
throw new UnauthorizedException('Token invalide ou expire');
}
}
const authHeader = request.headers['authorization'] as string | undefined;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token manquant ou invalide');
}
const token = authHeader.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.accessSecret'),
});
request.user = {
...payload,
id: payload.sub,
};
return true;
} catch (error) {
throw new UnauthorizedException('Token invalide ou expiré');
}
}
}

View File

@ -1,8 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
accessSecret: process.env.JWT_ACCESS_SECRET,
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES,
refreshSecret: process.env.JWT_REFRESH_SECRET,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES,
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRATION_TIME,
}));

View File

@ -14,8 +14,6 @@ export const configValidationSchema = Joi.object({
POSTGRES_DB: Joi.string().required(),
// JWT
JWT_ACCESS_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRES: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_REFRESH_EXPIRES: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
});

View File

@ -10,7 +10,6 @@ export enum StatutDossierType {
ENVOYE = 'envoye',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
CLOTURE = 'cloture',
}
@Entity('dossiers')

View File

@ -30,17 +30,6 @@ 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 {
@ -85,14 +74,6 @@ export class Users {
})
statut: StatutUtilisateurType;
@Column({ type: 'enum',
enum: SituationFamilialeType,
enumName: 'situation_familiale_type',
nullable: true,
name: 'situation_familiale'
})
situation_familiale?: SituationFamilialeType;
@Column({ nullable: true, name: 'telephone' })
telephone?: string;
@ -126,6 +107,9 @@ export class Users {
@Column({ nullable: true, name: 'profession' })
profession?: string;
@Column({ name: 'situation_familiale', nullable: true })
situation_familiale?: string;
@Column({ name: 'date_naissance', type: 'date', nullable: true })
date_naissance?: Date;

View File

@ -41,11 +41,10 @@ async function bootstrap() {
},
'access-token',
)
//.addServer('/api/v1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/v1/swagger', app, document);
SwaggerModule.setup('api-docs', app, document);

View File

@ -6,28 +6,22 @@ import {
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiBody, 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) { }
constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) {}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Créer nounou' })
@ApiResponse({ status: 201, description: 'Nounou créée avec succès' })
@ApiResponse({ status: 201, description: 'Assistante maternelle créée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiBody({ type: CreateAssistanteDto })
@Post()
@ -37,8 +31,7 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
@ApiResponse({ status: 200, description: 'Liste des nounous' })
@ApiResponse({ status: 200, description: 'Liste des assistantes maternelles' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getAll(): Promise<AssistanteMaternelle[]> {
return this.assistantesMaternellesService.findAll();
@ -46,10 +39,8 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id')
@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: 200, description: 'Détails de l\'assistante maternelle' })
@ApiResponse({ status: 404, description: 'Assistante maternelle 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);
@ -57,25 +48,20 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiBody({ type: UpdateAssistanteDto })
@ApiOperation({ summary: 'Mettre à jour une nounou' })
@ApiResponse({ status: 200, description: 'Nounou mise à jour avec succès' })
@ApiResponse({ status: 200, description: 'Assistante maternelle mise à jour avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@ApiResponse({ status: 404, description: 'Assistante maternelle non trouvée' })
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.update(id, dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Supprimer une nounou' })
@ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins, gestionnaires et administrateurs' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@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<{ message: string }>
{
remove(@Param('id') id: string): Promise<void> {
return this.assistantesMaternellesService.remove(id);
}
}

View File

@ -1,20 +1,9 @@
import { Module } from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service';
import { AssistantesMaternellesController } from './assistantes_maternelles.controller';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from 'src/entities/users.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, Users]),
AuthModule
],
controllers: [AssistantesMaternellesController],
providers: [AssistantesMaternellesService],
exports: [
AssistantesMaternellesService,
TypeOrmModule,
],
})
export class AssistantesMaternellesModule { }
export class AssistantesMaternellesModule {}

View File

@ -72,9 +72,8 @@ export class AssistantesMaternellesService {
return this.findOne(id);
}
// Suppression dune assistante maternelle
async remove(id: string): Promise<{ message: string }> {
// Suppression
async remove(id: string): Promise<void> {
await this.assistantesMaternelleRepository.delete(id);
return { message: 'Assistante maternelle supprimée' };
}
}

View File

@ -1,25 +1,15 @@
import { Body, Controller, Get, Post, Req, UnauthorizedException, UseGuards } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { LoginDto } from '../user/dto/login.dto';
import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { RegisterDto } from '../user/dto/register.dto';
import { ApiBearerAuth, ApiOperation, 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';
import { User } from 'src/common/decorators/user.decorator';
import { Users } from 'src/entities/users.entity';
@ApiTags('Authentification')
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) { }
constructor(private readonly authService: AuthService) { }
@Public()
@ApiOperation({ summary: 'Connexion' })
@ -37,39 +27,16 @@ 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() dto: RefreshTokenDto) {
return this.authService.refreshTokens(dto.refresh_token);
async refresh(@Body('refresh_token') refreshToken: string) {
return this.authService.refreshTokens(refreshToken);
}
@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,
};
}
@UseGuards(AuthGuard)
@ApiBearerAuth('access-token')
@Post('logout')
logout(@User() currentUser: Users) {
return this.authService.logout(currentUser.id);
}
// @Get('me')
// @UseGuards(AuthGuard)
// @ApiBearerAuth('access-token')
// @ApiOperation({ summary: "Recuperer le profil de l'utilisateur connecte"})
// getProfile(@Request() req) {
// return req.user;
// }
}

View File

@ -6,10 +6,11 @@ import {
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { RegisterDto } from './dto/register.dto';
import { RegisterDto } from '../user/dto/register.dto';
import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { LoginDto } from './dto/login.dto';
import { LoginDto } from '../user/dto/login.dto';
import { DeepPartial } from 'typeorm';
@Injectable()
export class AuthService {
@ -23,14 +24,12 @@ export class AuthService {
* Génère un access_token et un refresh_token
*/
async generateTokens(userId: string, email: string, role: RoleType) {
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 secret = this.configService.get<string>('jwt.secret');
const expiresIn = this.configService.get<string>('jwt.expiresIn');
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync({ sub: userId, email, role }, { secret: accessSecret, expiresIn: accessExpiresIn }),
this.jwtService.signAsync({ sub: userId }, { secret: refreshSecret, expiresIn: refreshExpiresIn }),
this.jwtService.signAsync({ sub: userId, email, role }, { secret, expiresIn }),
this.jwtService.signAsync({ sub: userId }, { secret, expiresIn }),
]);
return {
@ -49,17 +48,14 @@ export class AuthService {
if (!user) {
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);
console.log("Résultat bcrypt.compare:", isMatch);
if (!isMatch) {
throw new UnauthorizedException('Mot de passe invalide');
}
// if (user.password !== dto.password) {
// const isMatch = await bcrypt.compare(dto.password, user.password);
// if (!isMatch) {
// 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) {
@ -74,7 +70,7 @@ export class AuthService {
async refreshTokens(refreshToken: string) {
try {
const payload = await this.jwtService.verifyAsync(refreshToken, {
secret: this.configService.get<string>('jwt.refreshSecret'),
secret: this.configService.get<string>('jwt.refresh_token_secret'),
});
const user = await this.usersService.findOne(payload.sub);
@ -114,6 +110,7 @@ export class AuthService {
}
const user = await this.usersService.createUser(registerDto);
const tokens = await this.generateTokens(user.id, user.email, user.role);
return {
@ -128,10 +125,4 @@ export class AuthService {
},
};
}
async logout(userId: string) {
// Pour le moment envoyer un message clair
return { success: true, message: 'Deconnexion'}
}
}

View File

@ -1,22 +0,0 @@
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;
}

View File

@ -1,12 +0,0 @@
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;
}

View File

@ -1,4 +0,0 @@
import { Module } from '@nestjs/common';
@Module({})
export class DossiersModule {}

View File

@ -1,66 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsDateString,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
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: 'Dupont', 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: '2025-12-15', 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;
}

View File

@ -1,37 +0,0 @@
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: '2025-12-15', 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;
}

View File

@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/swagger';
import { CreateEnfantsDto } from './create_enfants.dto';
export class UpdateEnfantsDto extends PartialType(CreateEnfantsDto) {}

View File

@ -1,18 +0,0 @@
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();
});
});

View File

@ -1,70 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { EnfantsService } from './enfants.service';
import { CreateEnfantsDto } from './dto/create_enfants.dto';
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
import { RoleType, Users } from 'src/entities/users.entity';
import { User } from 'src/common/decorators/user.decorator';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { Roles } from 'src/common/decorators/roles.decorator';
import { RolesGuard } from 'src/common/guards/roles.guard';
@ApiBearerAuth('access-token')
@ApiTags('Enfants')
@UseGuards(AuthGuard, RolesGuard)
@Controller('enfants')
export class EnfantsController {
constructor(private readonly enfantsService: EnfantsService) { }
@Roles(RoleType.PARENT)
@Post()
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) {
return this.enfantsService.create(dto, currentUser);
}
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
@Get()
findAll() {
return this.enfantsService.findAll();
}
@Roles(
RoleType.PARENT,
RoleType.ADMINISTRATEUR,
RoleType.SUPER_ADMIN,
RoleType.GESTIONNAIRE
)
@Get(':id')
findOne(
@Param('id', new ParseUUIDPipe()) id: string,
@User() currentUser: Users
) {
return this.enfantsService.findOne(id, currentUser);
}
@Roles(RoleType.ADMINISTRATEUR, RoleType.SUPER_ADMIN, RoleType.PARENT)
@Patch(':id')
update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() dto: UpdateEnfantsDto,
@User() currentUser: Users,
) {
return this.enfantsService.update(id, dto, currentUser);
}
@Roles(RoleType.SUPER_ADMIN)
@Delete(':id')
remove(@Param('id', new ParseUUIDPipe()) id: string) {
return this.enfantsService.remove(id);
}
}

View File

@ -1,18 +0,0 @@
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';
import { Parents } from 'src/entities/parents.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [TypeOrmModule.forFeature([Children, Parents, ParentsChildren]),
AuthModule
],
controllers: [EnfantsController],
providers: [EnfantsService]
})
export class EnfantsModule { }

View File

@ -1,18 +0,0 @@
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();
});
});

View File

@ -1,113 +0,0 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from '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 { RoleType, Users } from 'src/entities/users.entity';
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>,
) { }
// Création dun enfant
async create(dto: CreateEnfantsDto, currentUser: Users): Promise<Children> {
const parent = await this.parentsRepository.findOne({
where: { user_id: currentUser.id },
});
if (!parent) throw new NotFoundException('Parent introuvable');
// Vérif métier simple
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
throw new BadRequestException('Un enfant actif doit avoir une date de naissance');
}
// Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent)
const exist = await this.childrenRepository.findOne({
where: {
first_name: dto.first_name,
last_name: dto.last_name,
birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined,
},
});
if (exist) throw new ConflictException('Cet enfant existe déjà');
// Création
const child = this.childrenRepository.create(dto);
await this.childrenRepository.save(child);
// Lien parent-enfant
const parentLink = this.parentsChildrenRepository.create({
parentId: parent.user_id,
enfantId: child.id,
});
await this.parentsChildrenRepository.save(parentLink);
return this.findOne(child.id, currentUser);
}
// Liste des enfants
async findAll(): Promise<Children[]> {
return this.childrenRepository.find({
relations: ['parentLinks'],
order: { last_name: 'ASC', first_name: 'ASC' },
});
}
// Récupérer un enfant par id
async findOne(id: string, currentUser: Users): Promise<Children> {
const child = await this.childrenRepository.findOne({
where: { id },
relations: ['parentLinks'],
});
if (!child) throw new NotFoundException('Enfant introuvable');
switch (currentUser.role) {
case RoleType.PARENT:
if (!child.parentLinks.some(link => link.parentId === currentUser.id)) {
throw new ForbiddenException('Cet enfant ne vous appartient pas');
}
break;
case RoleType.ADMINISTRATEUR:
case RoleType.SUPER_ADMIN:
case RoleType.GESTIONNAIRE:
// accès complet
break;
default:
throw new ForbiddenException('Accès interdit');
}
return child;
}
// Mise à jour
async update(id: string, dto: Partial<CreateEnfantsDto>, currentUser: Users): Promise<Children> {
const child = await this.childrenRepository.findOne({ where: { id } });
if (!child) throw new NotFoundException('Enfant introuvable');
await this.childrenRepository.update(id, dto);
return this.findOne(id, currentUser);
}
// Suppression
async remove(id: string): Promise<void> {
await this.childrenRepository.delete(id);
}
}

View File

@ -11,8 +11,8 @@ import {
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 { UpdateGestionnaireDto } from '../user/dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../user/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';
@ -70,4 +70,16 @@ export class GestionnairesController {
): Promise<Users | null> {
return this.gestionnairesService.update(id, dto);
}
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Supprimer un gestionnaire' })
@ApiResponse({ status: 200, description: 'Le gestionnaire a été supprimé avec succès.' })
@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' })
@Delete(':id')
remove(@Param('id') id: string): Promise<void> {
return this.gestionnairesService.remove(id);
}
}

View File

@ -6,8 +6,8 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RoleType, Users } from 'src/entities/users.entity';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../user/dto/create_gestionnaire.dto';
import { UpdateGestionnaireDto } from '../user/dto/update_gestionnaire.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
@ -60,28 +60,31 @@ export class GestionnairesService {
}
// Mise à jour dun gestionnaire
async update(id: string, dto: UpdateGestionnaireDto): Promise<Users> {
async update(id: string, dto: UpdateGestionnaireDto): Promise<Users | null> {
const gestionnaire = await this.findOne(id);
if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable');
if (dto.password) {
const salt = await bcrypt.genSalt();
gestionnaire.password = await bcrypt.hash(dto.password, salt);
delete (dto as any).password;
}
if (dto.date_consentement_photo !== undefined) {
gestionnaire.date_consentement_photo = dto.date_consentement_photo
? new Date(dto.date_consentement_photo)
: undefined;
delete (dto as any).date_consentement_photo;
}
const { password, date_consentement_photo, ...rest } = dto;
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined) {
(gestionnaire as any)[key] = value;
}
});
Object.assign(gestionnaire, dto);
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);
}
}

View File

@ -22,17 +22,16 @@ export class ParentsController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
@ApiResponse({ status: 200, description: 'Liste des parents' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins' })
getAll(): Promise<Parents[]> {
return this.parentsService.findAll();
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id')
@ApiResponse({ status: 200, type: Parents, description: 'Détails du parent par ID utilisateur' })
@ApiResponse({ status: 200, description: 'Détails du parent par ID utilisateur' })
@ApiResponse({ status: 404, description: 'Parent non trouvé' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
getOne(@Param('id') user_id: string): Promise<Parents> {
return this.parentsService.findOne(user_id);
}
@ -40,8 +39,7 @@ export class ParentsController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Post()
@ApiBody({ type: CreateParentDto })
@ApiResponse({ status: 201, type: Parents, description: 'Parent créé avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
@ApiResponse({ status: 201, description: 'Parent créé avec succès' })
create(@Body() dto: CreateParentDto): Promise<Parents> {
return this.parentsService.create(dto);
}
@ -49,10 +47,18 @@ export class ParentsController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Patch(':id')
@ApiBody({ type: UpdateParentsDto })
@ApiResponse({ status: 200, type: Parents, description: 'Parent mis à jour avec succès' })
@ApiResponse({ status: 200, description: 'Parent mis à jour avec succès' })
@ApiResponse({ status: 404, description: 'Parent introuvable' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
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);
}
}

View File

@ -9,8 +9,5 @@ import { Users } from 'src/entities/users.entity';
imports: [TypeOrmModule.forFeature([Parents, Users])],
controllers: [ParentsController],
providers: [ParentsService],
exports: [ParentsService,
TypeOrmModule,
],
})
export class ParentsModule { }
export class ParentsModule {}

View File

@ -71,4 +71,9 @@ 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);
}
}

View File

@ -1,56 +1,67 @@
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', 'photo_url', 'consentement_photo'] as const) {
export class CreateAssistanteDto extends OmitType(CreateUserDto, ['role'] as const) {
@IsUUID()
@IsNotEmpty()
@IsOptional()
user_id?: string;
@IsString()
@IsNotEmpty()
@IsOptional()
@Length(1, 50)
approval_number: string;
approval_number?: string;
@Matches(/^\d{15}$/)
@IsNotEmpty()
nir: string;
@IsOptional()
nir?: string;
@IsInt()
@Min(1)
@Max(10)
@IsNotEmpty()
max_children: number;
@IsString()
@IsNotEmpty()
photo_url: string;
@IsBoolean()
@IsNotEmpty()
consentement_photo: boolean;
@IsDateString()
@IsNotEmpty()
agreement_date: string;
@IsString()
@IsNotEmpty()
@Length(1, 100)
residence_city: string;
@IsOptional()
max_children?: number;
@IsOptional()
@IsString()
biography?: string;
@IsOptional()
@IsBoolean()
available?: boolean;
@IsOptional()
@IsString()
@Length(1, 100)
residence_city?: string;
@IsOptional()
@IsDateString()
agreement_date?: string;
@IsOptional()
@IsInt()
@Min(0)
years_experience?: number;
@IsOptional()
@IsString()
@Length(1, 100)
specialty?: string;
@IsOptional()
@IsInt()
@Min(0)
places_available?: number;
}

View File

@ -1,17 +1,14 @@
import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto";
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
import { IsNotEmpty, IsOptional, IsUUID } from "class-validator";
export class CreateParentDto extends OmitType(CreateUserDto, ['role', 'photo_url'] as const) {
export class CreateParentDto extends OmitType(CreateUserDto, ['role'] as const) {
@IsUUID()
@IsOptional()
@IsNotEmpty()
user_id: string;
user_id?: string;
@IsOptional()
@IsUUID()
co_parent_id?: string;
@IsString()
@IsNotEmpty()
photo_url: string;
}

View File

@ -10,7 +10,7 @@ import {
MinLength,
MaxLength,
} from 'class-validator';
import { RoleType, GenreType, StatutUtilisateurType, SituationFamilialeType } from 'src/entities/users.entity';
import { RoleType, GenreType, StatutUtilisateurType } from 'src/entities/users.entity';
export class CreateUserDto {
@ApiProperty({ example: 'sosso.test@example.com' })
@ -20,21 +20,20 @@ export class CreateUserDto {
@ApiProperty({ minLength: 6, example: 'Mon_motdepasse_fort_1234?' })
@IsString()
@IsNotEmpty()
@MinLength(6)
password: string;
@ApiProperty({ example: 'Julien' })
@IsString()
@IsNotEmpty()
@IsOptional()
@MaxLength(100)
prenom: string;
prenom?: string;
@ApiProperty({ example: 'Dupont' })
@IsString()
@IsNotEmpty()
@IsOptional()
@MaxLength(100)
nom: string;
nom?: string;
@ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE })
@IsOptional()
@ -50,16 +49,11 @@ export class CreateUserDto {
@IsEnum(StatutUtilisateurType)
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()
@IsNotEmpty()
@IsOptional()
@MaxLength(20)
telephone: string;
telephone?: string;
@ApiProperty({ example: 'Paris', required: false })
@IsOptional()
@ -75,8 +69,8 @@ export class CreateUserDto {
@ApiProperty({ example: '10 rue de la paix, 75000 Paris' })
@IsString()
@IsNotEmpty()
adresse: string;
@IsOptional()
adresse?: string;
@ApiProperty({ example: 'https://example.com/photo.jpg', required: false })
@IsOptional()
@ -97,9 +91,4 @@ export class CreateUserDto {
@IsOptional()
@IsBoolean()
changement_mdp_obligatoire?: boolean = false;
@ApiProperty({ example: true })
@IsBoolean()
@IsNotEmpty()
cguAccepted: boolean;
}

View File

@ -1,6 +1,6 @@
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator';
import { CreateUserDto } from '../../user/dto/create_user.dto';
import { CreateUserDto } from './create_user.dto';
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
export class RegisterDto extends OmitType(CreateUserDto, ['changement_mdp_obligatoire'] as const) {

View File

@ -63,7 +63,7 @@ export class UserController {
@ApiResponse({ status: 400, description: 'ID invalide' })
@ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 200, description: 'Compte validé avec succès' })
validate(
validerUtilisateur(
@Param('id') id: string,
@User() currentUser: Users,
@Body('comment') comment?: string,
@ -71,24 +71,13 @@ export class UserController {
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)
// Supprimer un utilisateur (super_admin et gestionnaire)
@Delete(':id')
@Roles(RoleType.SUPER_ADMIN)
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Supprimer un utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
remove(@Param('id') id: string, @User() currentUser: Users) {
return this.userService.remove(id, currentUser);
remove(@Param('id') id: string) {
return this.userService.remove(id);
}
}

View File

@ -4,22 +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';
import { ParentsModule } from '../parents/parents.module';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
import { Parents } from 'src/entities/parents.entity';
@Module({
imports: [TypeOrmModule.forFeature(
[
Users,
Validation,
Parents,
AssistanteMaternelle,
]), forwardRef(() => AuthModule),
ParentsModule,
AssistantesMaternellesModule,
imports: [TypeOrmModule.forFeature([Users]),
forwardRef(() => AuthModule),
],
controllers: [UserController],
providers: [UserService],

View File

@ -1,13 +1,11 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { RoleType, StatutUtilisateurType, Users } from "src/entities/users.entity";
import { In, Repository } from "typeorm";
import { 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 {
@ -16,60 +14,17 @@ export class UserService {
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>
private readonly validationRepository: Repository<Validation>
) { }
// Création utilisateur
async createUser(dto: CreateUserDto, currentUser?: Users): Promise<Users> {
if (!dto.cguAccepted) {
throw new BadRequestException(
'Vous devez accepter les CGU et la Politique de confidentialité pour créer un compte.',
);
// Sécuriser le rôle
if (!currentUser || currentUser.role !== RoleType.SUPER_ADMIN) {
dto.role = RoleType.PARENT;
}
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.');
// Nettoyage / validation consentement photo
let consentDate: Date | undefined;
if (dto.consentement_photo && dto.date_consentement_photo) {
const parsed = new Date(dto.date_consentement_photo);
@ -78,6 +33,7 @@ export class UserService {
}
}
// Hash mot de passe
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(dto.password, salt);
@ -86,8 +42,8 @@ export class UserService {
password: hashedPassword,
prenom: dto.prenom,
nom: dto.nom,
role,
statut,
role: dto.role,
statut: dto.statut,
genre: dto.genre,
telephone: dto.telephone,
ville: dto.ville,
@ -96,10 +52,7 @@ export class UserService {
photo_url: dto.photo_url,
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,
changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false
});
const saved = await this.usersRepository.save(entity);
@ -134,23 +87,11 @@ 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 = 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
@ -165,7 +106,6 @@ export class UserService {
return this.usersRepository.save(user);
}
// 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');
@ -173,58 +113,22 @@ export class UserService {
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,
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');
}
async remove(id: string): Promise<void> {
const result = await this.usersRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException('Utilisateur introuvable');

View File

@ -2,10 +2,6 @@ import { Users } from 'src/entities/users.entity';
declare module 'express-serve-static-core' {
interface Request {
user?: Users & {
sub?: string;
email?: string;
role?: string;
};
user?: Users | any;
}
}