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) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.env .env
# Tests bdd
.vscode/
BDD.sql
migrations/
src/seed/

10
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs/common": "^11.1.6", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0", "@nestjs/jwt": "^11.0.0",
@ -34,7 +34,7 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.10", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
@ -2322,9 +2322,9 @@
} }
}, },
"node_modules/@nestjs/common": { "node_modules/@nestjs/common": {
"version": "11.1.6", "version": "11.1.5",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz",
"integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"file-type": "21.0.0", "file-type": "21.0.0",

View File

@ -22,7 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.1.6", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0", "@nestjs/jwt": "^11.0.0",
@ -47,7 +47,7 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.10", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0", "@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 { AuthModule } from './routes/auth/auth.module';
import { SentryGlobalFilter } from '@sentry/nestjs/setup'; import { SentryGlobalFilter } from '@sentry/nestjs/setup';
import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
import { EnfantsModule } from './routes/enfants/enfants.module';
@Module({ @Module({
imports: [ imports: [
@ -47,7 +46,6 @@ import { EnfantsModule } from './routes/enfants/enfants.module';
}), }),
UserModule, UserModule,
ParentsModule, ParentsModule,
EnfantsModule,
AuthModule, AuthModule,
], ],
controllers: [AppController], controllers: [AppController],

View File

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

View File

@ -5,45 +5,42 @@ import { Request } from 'express';
import { IS_PUBLIC_KEY } from "../decorators/public.decorator"; import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
interface AuthenticatedRequest extends Request {
user?: any;
}
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor( constructor(
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly reflector: Reflector, private readonly reflector: Reflector,
private readonly configService: ConfigService, 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>(); async canActivate(context: ExecutionContext): Promise<boolean> {
if (request.path.startsWith('/api-docs')) { const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
return true; 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'; import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({ export default registerAs('jwt', () => ({
accessSecret: process.env.JWT_ACCESS_SECRET, secret: process.env.JWT_SECRET,
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES, expiresIn: process.env.JWT_EXPIRATION_TIME,
refreshSecret: process.env.JWT_REFRESH_SECRET,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES,
})); }));

View File

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

View File

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

View File

@ -30,17 +30,6 @@ export enum StatutUtilisateurType {
SUSPENDU = 'suspendu', 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 //Declaration de l'entite utilisateur
@Entity('utilisateurs', { schema: 'public' }) @Entity('utilisateurs', { schema: 'public' })
export class Users { export class Users {
@ -85,14 +74,6 @@ export class Users {
}) })
statut: 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' }) @Column({ nullable: true, name: 'telephone' })
telephone?: string; telephone?: string;
@ -126,6 +107,9 @@ export class Users {
@Column({ nullable: true, name: 'profession' }) @Column({ nullable: true, name: 'profession' })
profession?: string; profession?: string;
@Column({ name: 'situation_familiale', nullable: true })
situation_familiale?: string;
@Column({ name: 'date_naissance', type: 'date', nullable: true }) @Column({ name: 'date_naissance', type: 'date', nullable: true })
date_naissance?: Date; date_naissance?: Date;

View File

@ -41,11 +41,10 @@ async function bootstrap() {
}, },
'access-token', 'access-token',
) )
//.addServer('/api/v1')
.build(); .build();
const document = SwaggerModule.createDocument(app, config); 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, Patch,
Param, Param,
Delete, Delete,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service'; 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 { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { Roles } from 'src/common/decorators/roles.decorator'; import { Roles } from 'src/common/decorators/roles.decorator';
import { RoleType } from 'src/entities/users.entity'; import { RoleType } from 'src/entities/users.entity';
import { CreateAssistanteDto } from '../user/dto/create_assistante.dto'; import { CreateAssistanteDto } from '../user/dto/create_assistante.dto';
import { UpdateAssistanteDto } from '../user/dto/update_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") @ApiTags("Assistantes Maternelles")
@ApiBearerAuth('access-token')
@UseGuards(AuthGuard, RolesGuard)
@Controller('assistantes-maternelles') @Controller('assistantes-maternelles')
export class AssistantesMaternellesController { export class AssistantesMaternellesController {
constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) { } constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) {}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Créer nounou' }) @ApiResponse({ status: 201, description: 'Assistante maternelle créée avec succès' })
@ApiResponse({ status: 201, description: 'Nounou créée avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' }) @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiBody({ type: CreateAssistanteDto }) @ApiBody({ type: CreateAssistanteDto })
@Post() @Post()
@ -37,8 +31,7 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get() @Get()
@ApiOperation({ summary: 'Récupérer la liste des nounous' }) @ApiResponse({ status: 200, description: 'Liste des assistantes maternelles' })
@ApiResponse({ status: 200, description: 'Liste des nounous' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' }) @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getAll(): Promise<AssistanteMaternelle[]> { getAll(): Promise<AssistanteMaternelle[]> {
return this.assistantesMaternellesService.findAll(); return this.assistantesMaternellesService.findAll();
@ -46,10 +39,8 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id') @Get(':id')
@ApiParam({ name: 'id', description: "UUID de la nounou" }) @ApiResponse({ status: 200, description: 'Détails de l\'assistante maternelle' })
@ApiOperation({ summary: 'Récupérer une nounou par id' }) @ApiResponse({ status: 404, description: 'Assistante maternelle non trouvée' })
@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' }) @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> { getOne(@Param('id') user_id: string): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.findOne(user_id); return this.assistantesMaternellesService.findOne(user_id);
@ -57,25 +48,20 @@ export class AssistantesMaternellesController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiBody({ type: UpdateAssistanteDto }) @ApiBody({ type: UpdateAssistanteDto })
@ApiOperation({ summary: 'Mettre à jour une nounou' }) @ApiResponse({ status: 200, description: 'Assistante maternelle mise à jour avec succès' })
@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: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiResponse({ status: 404, description: 'Nounou non trouvée' }) @ApiResponse({ status: 404, description: 'Assistante maternelle non trouvée' })
@ApiParam({ name: 'id', description: "UUID de la nounou" })
@Patch(':id') @Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> { update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise<AssistanteMaternelle> {
return this.assistantesMaternellesService.update(id, dto); return this.assistantesMaternellesService.update(id, dto);
} }
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Supprimer une nounou' }) @ApiResponse({ status: 200, description: 'Assistante maternelle supprimée avec succès' })
@ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' }) @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' })
@ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins, gestionnaires et administrateurs' }) @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" })
@Delete(':id') @Delete(':id')
remove(@Param('id') id: string): Promise<{ message: string }> remove(@Param('id') id: string): Promise<void> {
{
return this.assistantesMaternellesService.remove(id); return this.assistantesMaternellesService.remove(id);
} }
} }

View File

@ -1,20 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AssistantesMaternellesService } from './assistantes_maternelles.service'; import { AssistantesMaternellesService } from './assistantes_maternelles.service';
import { AssistantesMaternellesController } from './assistantes_maternelles.controller'; 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({ @Module({
imports: [TypeOrmModule.forFeature([AssistanteMaternelle, Users]),
AuthModule
],
controllers: [AssistantesMaternellesController], controllers: [AssistantesMaternellesController],
providers: [AssistantesMaternellesService], providers: [AssistantesMaternellesService],
exports: [
AssistantesMaternellesService,
TypeOrmModule,
],
}) })
export class AssistantesMaternellesModule { } export class AssistantesMaternellesModule {}

View File

@ -72,9 +72,8 @@ export class AssistantesMaternellesService {
return this.findOne(id); return this.findOne(id);
} }
// Suppression dune assistante maternelle // Suppression
async remove(id: string): Promise<{ message: string }> { async remove(id: string): Promise<void> {
await this.assistantesMaternelleRepository.delete(id); 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 { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from '../user/dto/login.dto';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator'; import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from '../user/dto/register.dto';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard'; 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') @ApiTags('Authentification')
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor( constructor(private readonly authService: AuthService) { }
private readonly authService: AuthService,
private readonly userService: UserService,
) { }
@Public() @Public()
@ApiOperation({ summary: 'Connexion' }) @ApiOperation({ summary: 'Connexion' })
@ -37,39 +27,16 @@ export class AuthController {
@Public() @Public()
@Post('refresh') @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' }) @ApiOperation({ summary: 'Rafraichir les tokens' })
async refresh(@Body() dto: RefreshTokenDto) { async refresh(@Body('refresh_token') refreshToken: string) {
return this.authService.refreshTokens(dto.refresh_token); return this.authService.refreshTokens(refreshToken);
} }
@Get('me') // @Get('me')
@UseGuards(AuthGuard) // @UseGuards(AuthGuard)
@ApiBearerAuth('access-token') // @ApiBearerAuth('access-token')
@ApiOperation({ summary: "Récupérer le profil complet de l'utilisateur connecté" }) // @ApiOperation({ summary: "Recuperer le profil de l'utilisateur connecte"})
@ApiResponse({ status: 200, type: ProfileResponseDto }) // getProfile(@Request() req) {
async getProfile(@Req() req: Request): Promise<ProfileResponseDto> { // return req.user;
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);
}
} }

View File

@ -6,10 +6,11 @@ import {
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from '../user/dto/register.dto';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity'; 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() @Injectable()
export class AuthService { export class AuthService {
@ -23,14 +24,12 @@ export class AuthService {
* Génère un access_token et un refresh_token * Génère un access_token et un refresh_token
*/ */
async generateTokens(userId: string, email: string, role: RoleType) { async generateTokens(userId: string, email: string, role: RoleType) {
const accessSecret = this.configService.get<string>('jwt.accessSecret'); const secret = this.configService.get<string>('jwt.secret');
const accessExpiresIn = this.configService.get<string>('jwt.accessExpiresIn'); const expiresIn = this.configService.get<string>('jwt.expiresIn');
const refreshSecret = this.configService.get<string>('jwt.refreshSecret');
const refreshExpiresIn = this.configService.get<string>('jwt.refreshExpiresIn');
const [accessToken, refreshToken] = await Promise.all([ const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync({ sub: userId, email, role }, { secret: accessSecret, expiresIn: accessExpiresIn }), this.jwtService.signAsync({ sub: userId, email, role }, { secret, expiresIn }),
this.jwtService.signAsync({ sub: userId }, { secret: refreshSecret, expiresIn: refreshExpiresIn }), this.jwtService.signAsync({ sub: userId }, { secret, expiresIn }),
]); ]);
return { return {
@ -49,17 +48,14 @@ export class AuthService {
if (!user) { if (!user) {
throw new UnauthorizedException('Email invalide'); 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); // const isMatch = await bcrypt.compare(dto.password, user.password);
console.log("Résultat bcrypt.compare:", isMatch); // if (!isMatch) {
if (!isMatch) {
throw new UnauthorizedException('Mot de passe invalide');
}
// if (user.password !== dto.password) {
// throw new UnauthorizedException('Mot de passe invalide'); // 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); return this.generateTokens(user.id, user.email, user.role);
} catch (error) { } catch (error) {
@ -74,7 +70,7 @@ export class AuthService {
async refreshTokens(refreshToken: string) { async refreshTokens(refreshToken: string) {
try { try {
const payload = await this.jwtService.verifyAsync(refreshToken, { 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); const user = await this.usersService.findOne(payload.sub);
@ -114,6 +110,7 @@ export class AuthService {
} }
const user = await this.usersService.createUser(registerDto); const user = await this.usersService.createUser(registerDto);
const tokens = await this.generateTokens(user.id, user.email, user.role); const tokens = await this.generateTokens(user.id, user.email, user.role);
return { 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 { GestionnairesService } from './gestionnaires.service';
import { RoleType, Users } from 'src/entities/users.entity'; import { RoleType, Users } from 'src/entities/users.entity';
import { Roles } from 'src/common/decorators/roles.decorator'; import { Roles } from 'src/common/decorators/roles.decorator';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto'; import { UpdateGestionnaireDto } from '../user/dto/update_gestionnaire.dto';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto'; import { CreateGestionnaireDto } from '../user/dto/create_gestionnaire.dto';
import { RolesGuard } from 'src/common/guards/roles.guard'; import { RolesGuard } from 'src/common/guards/roles.guard';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard'; import { AuthGuard } from 'src/common/guards/auth.guard';
@ -70,4 +70,16 @@ export class GestionnairesController {
): Promise<Users | null> { ): Promise<Users | null> {
return this.gestionnairesService.update(id, dto); 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 { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { RoleType, Users } from 'src/entities/users.entity'; import { RoleType, Users } from 'src/entities/users.entity';
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto'; import { CreateGestionnaireDto } from '../user/dto/create_gestionnaire.dto';
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto'; import { UpdateGestionnaireDto } from '../user/dto/update_gestionnaire.dto';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
@Injectable() @Injectable()
@ -60,28 +60,31 @@ export class GestionnairesService {
} }
// Mise à jour dun gestionnaire // 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); const gestionnaire = await this.findOne(id);
if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable');
if (dto.password) { if (dto.password) {
const salt = await bcrypt.genSalt(); const salt = await bcrypt.genSalt();
gestionnaire.password = await bcrypt.hash(dto.password, salt); gestionnaire.password = await bcrypt.hash(dto.password, salt);
delete (dto as any).password;
} }
if (dto.date_consentement_photo !== undefined) { if (dto.date_consentement_photo !== undefined) {
gestionnaire.date_consentement_photo = dto.date_consentement_photo gestionnaire.date_consentement_photo = dto.date_consentement_photo
? new Date(dto.date_consentement_photo) ? new Date(dto.date_consentement_photo)
: undefined; : undefined;
delete (dto as any).date_consentement_photo;
} }
const { password, date_consentement_photo, ...rest } = dto; Object.assign(gestionnaire, dto);
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined) {
(gestionnaire as any)[key] = value;
}
});
return this.gestionnaireRepository.save(gestionnaire); 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) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get() @Get()
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' }) @ApiResponse({ status: 200, description: 'Liste des parents' })
@ApiResponse({ status: 403, description: 'Accès refusé !' }) @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins' })
getAll(): Promise<Parents[]> { getAll(): Promise<Parents[]> {
return this.parentsService.findAll(); return this.parentsService.findAll();
} }
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get(':id') @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: 404, description: 'Parent non trouvé' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
getOne(@Param('id') user_id: string): Promise<Parents> { getOne(@Param('id') user_id: string): Promise<Parents> {
return this.parentsService.findOne(user_id); return this.parentsService.findOne(user_id);
} }
@ -40,8 +39,7 @@ export class ParentsController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Post() @Post()
@ApiBody({ type: CreateParentDto }) @ApiBody({ type: CreateParentDto })
@ApiResponse({ status: 201, type: Parents, description: 'Parent créé avec succès' }) @ApiResponse({ status: 201, description: 'Parent créé avec succès' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
create(@Body() dto: CreateParentDto): Promise<Parents> { create(@Body() dto: CreateParentDto): Promise<Parents> {
return this.parentsService.create(dto); return this.parentsService.create(dto);
} }
@ -49,10 +47,18 @@ export class ParentsController {
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Patch(':id') @Patch(':id')
@ApiBody({ type: UpdateParentsDto }) @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: 404, description: 'Parent introuvable' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> { update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
return this.parentsService.update(id, dto); 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])], imports: [TypeOrmModule.forFeature([Parents, Users])],
controllers: [ParentsController], controllers: [ParentsController],
providers: [ParentsService], 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); await this.parentsRepository.update(id, dto);
return this.findOne(id); 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 { 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 { 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() @IsUUID()
@IsNotEmpty() @IsOptional()
user_id?: string; user_id?: string;
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
@Length(1, 50) @Length(1, 50)
approval_number: string; approval_number?: string;
@Matches(/^\d{15}$/) @Matches(/^\d{15}$/)
@IsNotEmpty() @IsOptional()
nir: string; nir?: string;
@IsInt() @IsInt()
@Min(1) @Min(1)
@Max(10) @Max(10)
@IsNotEmpty() @IsOptional()
max_children: number; 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() @IsOptional()
@IsString()
biography?: string; biography?: string;
@IsOptional() @IsOptional()
@IsBoolean()
available?: boolean; available?: boolean;
@IsOptional() @IsOptional()
@IsString()
@Length(1, 100)
residence_city?: string;
@IsOptional()
@IsDateString()
agreement_date?: string;
@IsOptional()
@IsInt()
@Min(0)
years_experience?: number; years_experience?: number;
@IsOptional() @IsOptional()
@IsString()
@Length(1, 100)
specialty?: string; specialty?: string;
@IsOptional() @IsOptional()
@IsInt()
@Min(0)
places_available?: number; places_available?: number;
} }

View File

@ -1,17 +1,14 @@
import { OmitType } from "@nestjs/swagger"; import { OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create_user.dto"; 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() @IsUUID()
@IsOptional()
@IsNotEmpty() @IsNotEmpty()
user_id: string; user_id?: string;
@IsOptional() @IsOptional()
@IsUUID() @IsUUID()
co_parent_id?: string; co_parent_id?: string;
@IsString()
@IsNotEmpty()
photo_url: string;
} }

View File

@ -10,7 +10,7 @@ import {
MinLength, MinLength,
MaxLength, MaxLength,
} from 'class-validator'; } 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 { export class CreateUserDto {
@ApiProperty({ example: 'sosso.test@example.com' }) @ApiProperty({ example: 'sosso.test@example.com' })
@ -20,21 +20,20 @@ export class CreateUserDto {
@ApiProperty({ minLength: 6, example: 'Mon_motdepasse_fort_1234?' }) @ApiProperty({ minLength: 6, example: 'Mon_motdepasse_fort_1234?' })
@IsString() @IsString()
@IsNotEmpty()
@MinLength(6) @MinLength(6)
password: string; password: string;
@ApiProperty({ example: 'Julien' }) @ApiProperty({ example: 'Julien' })
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
@MaxLength(100) @MaxLength(100)
prenom: string; prenom?: string;
@ApiProperty({ example: 'Dupont' }) @ApiProperty({ example: 'Dupont' })
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
@MaxLength(100) @MaxLength(100)
nom: string; nom?: string;
@ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE }) @ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE })
@IsOptional() @IsOptional()
@ -50,16 +49,11 @@ export class CreateUserDto {
@IsEnum(StatutUtilisateurType) @IsEnum(StatutUtilisateurType)
statut?: 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' }) @ApiProperty({ example: '+33123456789' })
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
@MaxLength(20) @MaxLength(20)
telephone: string; telephone?: string;
@ApiProperty({ example: 'Paris', required: false }) @ApiProperty({ example: 'Paris', required: false })
@IsOptional() @IsOptional()
@ -75,8 +69,8 @@ export class CreateUserDto {
@ApiProperty({ example: '10 rue de la paix, 75000 Paris' }) @ApiProperty({ example: '10 rue de la paix, 75000 Paris' })
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
adresse: string; adresse?: string;
@ApiProperty({ example: 'https://example.com/photo.jpg', required: false }) @ApiProperty({ example: 'https://example.com/photo.jpg', required: false })
@IsOptional() @IsOptional()
@ -97,9 +91,4 @@ export class CreateUserDto {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
changement_mdp_obligatoire?: boolean = false; 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 { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator'; 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'; import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
export class RegisterDto extends OmitType(CreateUserDto, ['changement_mdp_obligatoire'] as const) { 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: 400, description: 'ID invalide' })
@ApiResponse({ status: 403, description: 'Accès refusé' }) @ApiResponse({ status: 403, description: 'Accès refusé' })
@ApiResponse({ status: 200, description: 'Compte validé avec succès' }) @ApiResponse({ status: 200, description: 'Compte validé avec succès' })
validate( validerUtilisateur(
@Param('id') id: string, @Param('id') id: string,
@User() currentUser: Users, @User() currentUser: Users,
@Body('comment') comment?: string, @Body('comment') comment?: string,
@ -71,24 +71,13 @@ export class UserController {
return this.userService.validateUser(id, currentUser, comment); 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') @Delete(':id')
@Roles(RoleType.SUPER_ADMIN) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Supprimer un utilisateur' }) @ApiOperation({ summary: 'Supprimer un utilisateur' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) @ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
remove(@Param('id') id: string, @User() currentUser: Users) { remove(@Param('id') id: string) {
return this.userService.remove(id, currentUser); return this.userService.remove(id);
} }
} }

View File

@ -4,22 +4,10 @@ import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from 'src/entities/users.entity'; import { Users } from 'src/entities/users.entity';
import { AuthModule } from '../auth/auth.module'; 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({ @Module({
imports: [TypeOrmModule.forFeature( imports: [TypeOrmModule.forFeature([Users]),
[ forwardRef(() => AuthModule),
Users,
Validation,
Parents,
AssistanteMaternelle,
]), forwardRef(() => AuthModule),
ParentsModule,
AssistantesMaternellesModule,
], ],
controllers: [UserController], controllers: [UserController],
providers: [UserService], 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 { InjectRepository } from "@nestjs/typeorm";
import { RoleType, StatutUtilisateurType, Users } from "src/entities/users.entity"; 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 { CreateUserDto } from "./dto/create_user.dto";
import { UpdateUserDto } from "./dto/update_user.dto"; import { UpdateUserDto } from "./dto/update_user.dto";
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { StatutValidationType, Validation } from "src/entities/validations.entity"; import { StatutValidationType, Validation } from "src/entities/validations.entity";
import { Parents } from "src/entities/parents.entity";
import { AssistanteMaternelle } from "src/entities/assistantes_maternelles.entity";
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -16,60 +14,17 @@ export class UserService {
private readonly usersRepository: Repository<Users>, private readonly usersRepository: Repository<Users>,
@InjectRepository(Validation) @InjectRepository(Validation)
private readonly validationRepository: Repository<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> { async createUser(dto: CreateUserDto, currentUser?: Users): Promise<Users> {
if (!dto.cguAccepted) { // Sécuriser le rôle
throw new BadRequestException( if (!currentUser || currentUser.role !== RoleType.SUPER_ADMIN) {
'Vous devez accepter les CGU et la Politique de confidentialité pour créer un compte.', dto.role = RoleType.PARENT;
);
} }
const exist = await this.usersRepository.findOneBy({ email: dto.email }); // Nettoyage / validation consentement photo
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; let consentDate: Date | undefined;
if (dto.consentement_photo && dto.date_consentement_photo) { if (dto.consentement_photo && dto.date_consentement_photo) {
const parsed = new Date(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 salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(dto.password, salt); const hashedPassword = await bcrypt.hash(dto.password, salt);
@ -86,8 +42,8 @@ export class UserService {
password: hashedPassword, password: hashedPassword,
prenom: dto.prenom, prenom: dto.prenom,
nom: dto.nom, nom: dto.nom,
role, role: dto.role,
statut, statut: dto.statut,
genre: dto.genre, genre: dto.genre,
telephone: dto.telephone, telephone: dto.telephone,
ville: dto.ville, ville: dto.ville,
@ -96,10 +52,7 @@ export class UserService {
photo_url: dto.photo_url, photo_url: dto.photo_url,
consentement_photo: dto.consentement_photo ?? false, consentement_photo: dto.consentement_photo ?? false,
date_consentement_photo: consentDate, date_consentement_photo: consentDate,
changement_mdp_obligatoire: changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false
role === RoleType.ADMINISTRATEUR || role === RoleType.GESTIONNAIRE
? true
: dto.changement_mdp_obligatoire ?? false,
}); });
const saved = await this.usersRepository.save(entity); const saved = await this.usersRepository.save(entity);
@ -134,23 +87,11 @@ export class UserService {
throw new ForbiddenException('Accès réservé aux super admins'); 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 // Gestion du mot de passe
if (dto.password) { if (dto.password) {
const salt = await bcrypt.genSalt(); const salt = await bcrypt.genSalt();
user.password = await bcrypt.hash(dto.password, salt); user.password = await bcrypt.hash(dto.password, salt);
delete (dto as any).password; 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 // Conversion de la date de consentement
@ -165,7 +106,6 @@ export class UserService {
return this.usersRepository.save(user); return this.usersRepository.save(user);
} }
// Valider un compte utilisateur
async validateUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> { async validateUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) { if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires'); throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
@ -173,61 +113,25 @@ export class UserService {
const user = await this.usersRepository.findOne({ where: { id: user_id } }); const user = await this.usersRepository.findOne({ where: { id: user_id } });
if (!user) throw new NotFoundException('Utilisateur introuvable'); if (!user) throw new NotFoundException('Utilisateur introuvable');
user.statut = StatutUtilisateurType.ACTIF; user.statut = StatutUtilisateurType.ACTIF;
const savedUser = await this.usersRepository.save(user); 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({ const validation = this.validationRepository.create({
user: savedUser, user: savedUser,
type: 'validation_compte', type: 'validation_compte',
status: StatutValidationType.VALIDE, status: StatutValidationType.VALIDE,
validated_by: currentUser, validated_by: currentUser,
comment, comment
}); });
await this.validationRepository.save(validation); await this.validationRepository.save(validation);
return savedUser; return savedUser;
} }
async remove(id: string): Promise<void> {
// 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); const result = await this.usersRepository.delete(id);
if (result.affected === 0) { if (result.affected === 0) {
throw new NotFoundException('Utilisateur introuvable'); throw new NotFoundException('Utilisateur introuvable');
} }
} }
} }

View File

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