feat: add profiles session API

This commit is contained in:
Matthieu
2025-09-17 23:11:25 +02:00
parent 83251b532c
commit df5bbeecb7
78 changed files with 3000 additions and 836 deletions

View File

@@ -11,6 +11,8 @@ import { DocumentsModule } from './documents/documents.module';
import { TypesModule } from './types/types.module';
import { CustomFieldsModule } from './custom-fields/custom-fields.module';
import { ConstructeursModule } from './constructeurs/constructeurs.module';
import { ProfilesModule } from './profiles/profiles.module';
import { SessionModule } from './session/session.module';
@Module({
imports: [
@@ -26,6 +28,8 @@ import { ConstructeursModule } from './constructeurs/constructeurs.module';
TypesModule,
CustomFieldsModule,
ConstructeursModule,
ProfilesModule,
SessionModule,
],
controllers: [AppController],
providers: [AppService],

View File

@@ -1,10 +1,25 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import * as session from 'express-session';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
session({
secret: process.env.SESSION_SECRET || 'change-me',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: (process.env.SESSION_SAME_SITE as 'strict' | 'lax' | 'none') ?? 'lax',
secure: process.env.NODE_ENV === 'production',
maxAge: Number(process.env.SESSION_MAX_AGE ?? 1000 * 60 * 60 * 24 * 7),
},
}),
);
// Récupérer les origines CORS depuis la variable d'environnement (séparées par des virgules)
const allowedOrigins = process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(',').map(origin => origin.trim())

View File

@@ -0,0 +1,23 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'
import { ProfilesService } from './profiles.service'
import { CreateProfileDto } from '../shared/dto/profile.dto'
@Controller('profiles')
export class ProfilesController {
constructor(private readonly profilesService: ProfilesService) {}
@Get()
async findAll() {
return this.profilesService.findAllActive()
}
@Post()
async create(@Body() dto: CreateProfileDto) {
return this.profilesService.create(dto)
}
@Delete(':id')
async delete(@Param('id') id: string) {
return this.profilesService.deactivate(id)
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common'
import { ProfilesController } from './profiles.controller'
import { ProfilesService } from './profiles.service'
@Module({
controllers: [ProfilesController],
providers: [ProfilesService],
exports: [ProfilesService],
})
export class ProfilesModule {}

View File

@@ -0,0 +1,119 @@
import { Injectable, NotFoundException, BadRequestException, OnModuleInit } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { CreateProfileDto } from '../shared/dto/profile.dto'
@Injectable()
export class ProfilesService implements OnModuleInit {
constructor(private readonly prisma: PrismaService) {}
async onModuleInit() {
await this.ensureDefaultProfile()
}
async findAllActive() {
return this.prisma.profile.findMany({
where: { isActive: true },
orderBy: { createdAt: 'asc' },
select: {
id: true,
firstName: true,
lastName: true,
createdAt: true,
updatedAt: true,
},
})
}
async findActiveById(profileId: string) {
if (!profileId) return null
return this.prisma.profile.findFirst({
where: {
id: profileId,
isActive: true,
},
select: {
id: true,
firstName: true,
lastName: true,
createdAt: true,
updatedAt: true,
isActive: true,
},
})
}
async create(dto: CreateProfileDto) {
const firstName = dto.firstName.trim()
const lastName = dto.lastName.trim()
if (!firstName || !lastName) {
throw new BadRequestException('Le prénom et le nom sont obligatoires.')
}
return this.prisma.profile.create({
data: {
firstName,
lastName,
},
select: {
id: true,
firstName: true,
lastName: true,
isActive: true,
createdAt: true,
updatedAt: true,
},
})
}
async deactivate(profileId: string) {
const existing = await this.prisma.profile.findUnique({ where: { id: profileId } })
if (!existing) {
throw new NotFoundException('Profil introuvable')
}
if (!existing.isActive) {
return this.prisma.profile.findUnique({
where: { id: profileId },
select: {
id: true,
firstName: true,
lastName: true,
isActive: true,
createdAt: true,
updatedAt: true,
},
})
}
return this.prisma.profile.update({
where: { id: profileId },
data: { isActive: false },
select: {
id: true,
firstName: true,
lastName: true,
isActive: true,
createdAt: true,
updatedAt: true,
},
})
}
private async ensureDefaultProfile() {
const count = await this.prisma.profile.count({ where: { isActive: true } })
if (count > 0) return
const firstName = process.env.DEFAULT_PROFILE_FIRST_NAME?.trim() || 'Admin'
const lastName = process.env.DEFAULT_PROFILE_LAST_NAME?.trim() || 'Général'
await this.prisma.profile.create({
data: {
firstName,
lastName,
isActive: true,
},
})
}
}

View File

@@ -0,0 +1,64 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Post,
Req,
UnauthorizedException,
} from '@nestjs/common'
import { Request } from 'express'
import { ProfilesService } from '../profiles/profiles.service'
import { ActivateProfileDto } from '../shared/dto/profile.dto'
@Controller('session/profile')
export class ProfileSessionController {
constructor(private readonly profilesService: ProfilesService) {}
@Get()
async getActiveProfile(@Req() req: Request) {
if (!req.session?.profileId) {
throw new UnauthorizedException('Aucun profil actif.')
}
const profile = await this.profilesService.findActiveById(req.session.profileId)
if (!profile) {
req.session.profileId = undefined
throw new UnauthorizedException('Profil introuvable ou inactif.')
}
return profile
}
@Post()
async activateProfile(@Req() req: Request, @Body() dto: ActivateProfileDto) {
if (!dto.profileId) {
throw new BadRequestException('profileId est requis.')
}
const profile = await this.profilesService.findActiveById(dto.profileId)
if (!profile) {
throw new UnauthorizedException('Profil introuvable ou inactif.')
}
req.session.profileId = profile.id
return profile
}
@Delete()
async logout(@Req() req: Request) {
if (!req.session) {
return { success: true }
}
return new Promise((resolve, reject) => {
req.session.destroy((err) => {
if (err) {
return reject(new BadRequestException('Impossible de déconnecter la session.'))
}
resolve({ success: true })
})
})
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ProfileSessionController } from './session.controller'
import { ProfilesModule } from '../profiles/profiles.module'
@Module({
imports: [ProfilesModule],
controllers: [ProfileSessionController],
})
export class SessionModule {}

View File

@@ -0,0 +1,19 @@
import { IsNotEmpty, IsString, MaxLength } from 'class-validator'
export class CreateProfileDto {
@IsString()
@IsNotEmpty()
@MaxLength(100)
firstName!: string
@IsString()
@IsNotEmpty()
@MaxLength(100)
lastName!: string
}
export class ActivateProfileDto {
@IsString()
@IsNotEmpty()
profileId!: string
}

7
src/types/express-session.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import 'express-session'
declare module 'express-session' {
interface SessionData {
profileId?: string
}
}