Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8104e7e4 | ||
|
|
c4d93ee458 | ||
|
|
1fca0cf132 | ||
|
|
b16dd4b55c | ||
|
|
8682421453 | ||
|
|
31bd8c3175 | ||
|
|
c94f2cf0d5 | ||
|
|
dfe7daed14 | ||
|
|
111935e451 | ||
|
|
ae3292a7fc | ||
|
|
8e8c6d79b1 | ||
|
|
6752dc97b4 | ||
|
|
31857ec891 | ||
|
|
ca7ef862da | ||
|
|
11aa66feff | ||
|
|
358eefdab3 | ||
|
|
d23f3c9f4f | ||
|
|
1834eb8c79 | ||
|
|
0386785f81 | ||
|
|
c43f55bed6 | ||
|
|
0c48a5c06f | ||
|
|
be8b1f23ed | ||
|
|
18b270eaa3 | ||
|
|
68e4f54814 | ||
|
|
6794190916 | ||
|
|
790761d576 | ||
|
|
930097f87d | ||
|
|
18af5c9034 | ||
|
|
10bf2553e7 | ||
|
|
678f4219b5 | ||
|
|
5295e8ec72 | ||
|
|
480f4a9396 | ||
|
|
6bf0932da8 | ||
|
|
2f1740b35f | ||
|
|
fd97e68dd9 | ||
|
|
813fdb8449 | ||
|
|
155c6ca4d5 | ||
|
|
030ef81038 | ||
|
|
0d88597bb6 | ||
|
|
9b007fe490 | ||
|
|
7ecb99963c | ||
|
|
39814c76b1 | ||
|
|
b956f94ad2 | ||
|
|
b18d5c8a9e | ||
|
|
6ad88cbbc6 | ||
|
|
dfe91ed772 | ||
|
|
08612c455d | ||
|
|
6452706680 | ||
|
|
eea94769bf | ||
|
|
bdecbc2c1d | ||
|
|
f8bd911c02 | ||
|
|
b79f8c7e64 | ||
|
|
a57993a90f | ||
|
|
1d774f29eb | ||
|
|
890619ff59 | ||
|
|
5d7eb9eb36 | ||
|
|
45bd8a9ef1 | ||
|
|
acb8e72a7c | ||
|
|
b6c70a52ac | ||
|
|
96794919a8 | ||
|
|
271dc713a3 | ||
|
|
13741b0430 | ||
|
|
8e3af711e5 | ||
|
|
e700e50924 | ||
|
|
36ef0f8d5c | ||
|
|
f09deb5efc | ||
|
|
26a0e31b32 | ||
|
|
21430dca41 | ||
|
|
dcb81d3feb | ||
|
|
7c86feeb78 | ||
|
|
df87abbb85 | ||
|
|
bd81561e41 | ||
|
|
cc96ef20e1 | ||
|
|
a4ac65a5db | ||
|
|
3d13eb5b2e | ||
|
|
5b37d09fa9 | ||
|
|
53f3af9794 | ||
|
|
105cf53e7b | ||
|
|
29bee9fa80 | ||
|
|
dbd56637e1 | ||
|
|
264e0d49ae | ||
|
|
fe71fdf28e | ||
|
|
b3ec1b94ea | ||
|
|
95d1c3741b | ||
|
|
c5028c3b22 | ||
|
|
cef197d133 | ||
|
|
c934466e47 | ||
|
|
90d8fa8669 | ||
|
|
90cdf16709 | ||
|
|
bde97c24db | ||
|
|
9ae6533b4d | ||
|
|
579b6cae90 | ||
|
|
9aea26805d | ||
|
|
40c7f40d12 | ||
|
|
61b45cd830 | ||
|
|
f53fd903e5 | ||
|
|
98082187b5 | ||
|
|
1fb8c33cbf | ||
|
|
6ceb0f0ea9 | ||
|
|
bebd3c74da | ||
|
|
e1628da9cb | ||
|
|
eb1583b35b | ||
|
|
ec485b5a3e | ||
|
|
80afe2fa2f | ||
|
|
4149d0147f | ||
|
|
47dbe94b02 | ||
|
|
fd4f5e6b12 | ||
|
|
40b1eb2192 | ||
|
|
933793aad8 | ||
|
|
2285069a52 | ||
|
|
2e3139b5fc | ||
|
|
93306d287b | ||
|
|
d0827a119e | ||
|
|
1bbdab03d0 | ||
|
|
7f78617561 | ||
|
|
7707b99773 | ||
|
|
760f4feca3 | ||
|
|
03712bd99b | ||
|
|
1496f7f174 | ||
|
|
acb602643a | ||
|
|
0772f83369 | ||
|
|
42d147c273 | ||
|
|
df56ba11df | ||
|
|
bbdacd68aa | ||
|
|
7f831f363e | ||
|
|
009d42ece8 | ||
|
|
e6d3c41ecc | ||
|
|
c7ac3d9ebe | ||
|
|
c8b8ad9318 | ||
|
|
482040ba55 | ||
|
|
2bcb0b1e54 | ||
|
|
30e72242a8 | ||
|
|
aaf7070757 | ||
|
|
f4c211e0dd | ||
|
|
9519fafe3a | ||
|
|
9321430818 |
@@ -0,0 +1,18 @@
|
|||||||
|
# Fins de ligne : toujours LF dans le dépôt (évite les conflits Linux/Windows)
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Fichiers binaires : pas de conversion
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.webp binary
|
||||||
|
*.pdf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
|
||||||
|
# Scripts shell : toujours LF
|
||||||
|
*.sh text eol=lf
|
||||||
@@ -37,6 +37,10 @@ yarn-error.log*
|
|||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
**/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
coverage/
|
coverage/
|
||||||
@@ -52,3 +56,4 @@ Xcf/**
|
|||||||
# Release notes
|
# Release notes
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
Ressources/
|
Ressources/
|
||||||
|
.gitea-token
|
||||||
|
|||||||
@@ -83,3 +83,5 @@ npx prisma migrate dev --name <nom_migration>
|
|||||||
- [openapi-generator](https://openapi-generator.tech/)
|
- [openapi-generator](https://openapi-generator.tech/)
|
||||||
- [openapi-typescript](https://github.com/drwpow/openapi-typescript)
|
- [openapi-typescript](https://github.com/drwpow/openapi-typescript)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,3 +23,5 @@ npx prisma migrate deploy
|
|||||||
|
|
||||||
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
|
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -175,3 +175,5 @@ components:
|
|||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
description: Token JWT obtenu via /auth/login
|
description: Token JWT obtenu via /auth/login
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,6 @@ JWT_EXPIRATION_TIME=7d
|
|||||||
|
|
||||||
# Environnement
|
# Environnement
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Log de chaque appel API (mode debug) — mettre à true pour tracer les requêtes front
|
||||||
|
# LOG_API_REQUESTS=true
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ COPY --from=builder /app/dist ./dist
|
|||||||
RUN addgroup -g 1001 -S nodejs
|
RUN addgroup -g 1001 -S nodejs
|
||||||
RUN adduser -S nestjs -u 1001
|
RUN adduser -S nestjs -u 1001
|
||||||
|
|
||||||
|
# Créer le dossier uploads et donner les permissions
|
||||||
|
RUN mkdir -p /app/uploads/photos && chown -R nestjs:nodejs /app/uploads
|
||||||
|
|
||||||
USER nestjs
|
USER nestjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -37,6 +37,8 @@
|
|||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"joi": "^18.0.0",
|
"joi": "^18.0.0",
|
||||||
"mapped-types": "^0.0.1",
|
"mapped-types": "^0.0.1",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"nodemailer": "^6.9.16",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
@@ -53,7 +55,9 @@
|
|||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/nodemailer": "^6.4.16",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les parents
|
||||||
|
model Parent {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
phoneNumber String?
|
||||||
|
address String?
|
||||||
|
status AccountStatus @default(PENDING)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
children Child[]
|
||||||
|
contracts Contract[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les enfants
|
||||||
|
model Child {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
firstName String
|
||||||
|
dateOfBirth DateTime
|
||||||
|
photoUrl String?
|
||||||
|
photoConsent Boolean @default(false)
|
||||||
|
isMultiple Boolean @default(false)
|
||||||
|
isUnborn Boolean @default(false)
|
||||||
|
parentId String
|
||||||
|
parent Parent @relation(fields: [parentId], references: [id])
|
||||||
|
contracts Contract[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les contrats
|
||||||
|
model Contract {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
parentId String
|
||||||
|
childId String
|
||||||
|
startDate DateTime
|
||||||
|
endDate DateTime?
|
||||||
|
status ContractStatus @default(ACTIVE)
|
||||||
|
parent Parent @relation(fields: [parentId], references: [id])
|
||||||
|
child Child @relation(fields: [childId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les thèmes
|
||||||
|
model Theme {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String @unique
|
||||||
|
primaryColor String
|
||||||
|
secondaryColor String
|
||||||
|
backgroundColor String
|
||||||
|
textColor String
|
||||||
|
isActive Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
appSettings AppSettings[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les paramètres de l'application
|
||||||
|
model AppSettings {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
currentThemeId String
|
||||||
|
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([currentThemeId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modèle pour les administrateurs
|
||||||
|
model Admin {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
passwordChanged Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
enum AccountStatus {
|
||||||
|
PENDING
|
||||||
|
VALIDATED
|
||||||
|
REJECTED
|
||||||
|
SUSPENDED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContractStatus {
|
||||||
|
ACTIVE
|
||||||
|
ENDED
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test POST /auth/register/am (ticket #90)
|
||||||
|
# Usage: ./scripts/test-register-am.sh [BASE_URL]
|
||||||
|
# Exemple: ./scripts/test-register-am.sh https://app.ptits-pas.fr/api/v1
|
||||||
|
# ./scripts/test-register-am.sh http://localhost:3000/api/v1
|
||||||
|
|
||||||
|
BASE_URL="${1:-http://localhost:3000/api/v1}"
|
||||||
|
echo "Testing POST $BASE_URL/auth/register/am"
|
||||||
|
echo "---"
|
||||||
|
|
||||||
|
curl -s -w "\n\nHTTP %{http_code}\n" -X POST "$BASE_URL/auth/register/am" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email": "marie.dupont.test@ptits-pas.fr",
|
||||||
|
"prenom": "Marie",
|
||||||
|
"nom": "DUPONT",
|
||||||
|
"telephone": "0612345678",
|
||||||
|
"adresse": "1 rue Test",
|
||||||
|
"code_postal": "75001",
|
||||||
|
"ville": "Paris",
|
||||||
|
"consentement_photo": true,
|
||||||
|
"nir": "123456789012345",
|
||||||
|
"numero_agrement": "AGR-2024-001",
|
||||||
|
"capacite_accueil": 4,
|
||||||
|
"acceptation_cgu": true,
|
||||||
|
"acceptation_privacy": true
|
||||||
|
}'
|
||||||
@@ -14,6 +14,8 @@ 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';
|
import { EnfantsModule } from './routes/enfants/enfants.module';
|
||||||
|
import { AppConfigModule } from './modules/config/config.module';
|
||||||
|
import { DocumentsLegauxModule } from './modules/documents-legaux';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -49,6 +51,8 @@ import { EnfantsModule } from './routes/enfants/enfants.module';
|
|||||||
ParentsModule,
|
ParentsModule,
|
||||||
EnfantsModule,
|
EnfantsModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
AppConfigModule,
|
||||||
|
DocumentsLegauxModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
/** Clés à masquer dans les logs (corps de requête) */
|
||||||
|
const SENSITIVE_KEYS = [
|
||||||
|
'password',
|
||||||
|
'smtp_password',
|
||||||
|
'token',
|
||||||
|
'accessToken',
|
||||||
|
'refreshToken',
|
||||||
|
'secret',
|
||||||
|
];
|
||||||
|
|
||||||
|
function maskBody(body: unknown): unknown {
|
||||||
|
if (body === null || body === undefined) return body;
|
||||||
|
if (typeof body !== 'object') return body;
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(body)) {
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
const isSensitive = SENSITIVE_KEYS.some((s) => lower.includes(s));
|
||||||
|
out[key] = isSensitive ? '***' : value;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogRequestInterceptor implements NestInterceptor {
|
||||||
|
private readonly enabled: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.enabled =
|
||||||
|
process.env.LOG_API_REQUESTS === 'true' ||
|
||||||
|
process.env.LOG_API_REQUESTS === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||||
|
if (!this.enabled) return next.handle();
|
||||||
|
|
||||||
|
const http = context.switchToHttp();
|
||||||
|
const req = http.getRequest<Request>();
|
||||||
|
const { method, url, body, query } = req;
|
||||||
|
const hasBody = body && Object.keys(body).length > 0;
|
||||||
|
|
||||||
|
const logLine = [
|
||||||
|
`[API] ${method} ${url}`,
|
||||||
|
Object.keys(query || {}).length ? `query=${JSON.stringify(query)}` : '',
|
||||||
|
hasBody ? `body=${JSON.stringify(maskBody(body))}` : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
console.log(logLine);
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap({
|
||||||
|
next: () => {
|
||||||
|
// Optionnel: log du statut en fin de requête (si besoin plus tard)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Users } from './users.entity';
|
||||||
|
import { DocumentLegal } from './document-legal.entity';
|
||||||
|
|
||||||
|
@Entity('acceptations_documents')
|
||||||
|
export class AcceptationDocument {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Users, { nullable: false, onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'id_utilisateur' })
|
||||||
|
utilisateur: Users;
|
||||||
|
|
||||||
|
@ManyToOne(() => DocumentLegal, { nullable: true })
|
||||||
|
@JoinColumn({ name: 'id_document' })
|
||||||
|
document: DocumentLegal | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||||
|
type_document: 'cgu' | 'privacy';
|
||||||
|
|
||||||
|
@Column({ type: 'integer', nullable: false })
|
||||||
|
version_document: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'accepte_le', type: 'timestamptz' })
|
||||||
|
accepteLe: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'inet', nullable: true })
|
||||||
|
ip_address: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
user_agent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Users } from './users.entity';
|
||||||
|
|
||||||
|
@Entity('configuration')
|
||||||
|
export class Configuration {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 100, unique: true, nullable: false })
|
||||||
|
cle: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
valeur: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'json' | 'encrypted';
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||||
|
categorie: 'email' | 'app' | 'security' | null;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'modifie_le' })
|
||||||
|
modifieLe: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => Users, { nullable: true })
|
||||||
|
@JoinColumn({ name: 'modifie_par' })
|
||||||
|
modifiePar: Users | null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Users } from './users.entity';
|
||||||
|
|
||||||
|
@Entity('documents_legaux')
|
||||||
|
export class DocumentLegal {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50, nullable: false })
|
||||||
|
type: 'cgu' | 'privacy';
|
||||||
|
|
||||||
|
@Column({ type: 'integer', nullable: false })
|
||||||
|
version: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: false })
|
||||||
|
fichier_nom: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 500, nullable: false })
|
||||||
|
fichier_path: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 64, nullable: false })
|
||||||
|
fichier_hash: string;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
actif: boolean;
|
||||||
|
|
||||||
|
@ManyToOne(() => Users, { nullable: true })
|
||||||
|
@JoinColumn({ name: 'televerse_par' })
|
||||||
|
televersePar: Users | null;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'televerse_le', type: 'timestamptz' })
|
||||||
|
televerseLe: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'active_le', type: 'timestamptz', nullable: true })
|
||||||
|
activeLe: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ export class Users {
|
|||||||
@Column({ unique: true, name: 'email' })
|
@Column({ unique: true, name: 'email' })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@Column({ name: 'password' })
|
@Column({ name: 'password', nullable: true })
|
||||||
password: string;
|
password?: string;
|
||||||
|
|
||||||
@Column({ name: 'prenom', nullable: true })
|
@Column({ name: 'prenom', nullable: true })
|
||||||
prenom?: string;
|
prenom?: string;
|
||||||
@@ -96,12 +96,6 @@ export class Users {
|
|||||||
@Column({ nullable: true, name: 'telephone' })
|
@Column({ nullable: true, name: 'telephone' })
|
||||||
telephone?: string;
|
telephone?: string;
|
||||||
|
|
||||||
@Column({ name: 'mobile', nullable: true })
|
|
||||||
mobile?: string;
|
|
||||||
|
|
||||||
@Column({ name: 'telephone_fixe', nullable: true })
|
|
||||||
telephone_fixe?: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true, name: 'adresse' })
|
@Column({ nullable: true, name: 'adresse' })
|
||||||
adresse?: string;
|
adresse?: string;
|
||||||
|
|
||||||
@@ -117,6 +111,12 @@ export class Users {
|
|||||||
@Column({ default: false, name: 'changement_mdp_obligatoire' })
|
@Column({ default: false, name: 'changement_mdp_obligatoire' })
|
||||||
changement_mdp_obligatoire: boolean;
|
changement_mdp_obligatoire: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'token_creation_mdp', length: 255 })
|
||||||
|
token_creation_mdp?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamptz', nullable: true, name: 'token_creation_mdp_expire_le' })
|
||||||
|
token_creation_mdp_expire_le?: Date;
|
||||||
|
|
||||||
@Column({ nullable: true, name: 'ville' })
|
@Column({ nullable: true, name: 'ville' })
|
||||||
ville?: string;
|
ville?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import { NestFactory, Reflector } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
|
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
|
||||||
import { DocumentBuilder } from '@nestjs/swagger';
|
import { DocumentBuilder } from '@nestjs/swagger';
|
||||||
import { AuthGuard } from './common/guards/auth.guard';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
import { RolesGuard } from './common/guards/roles.guard';
|
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { LogRequestInterceptor } from './common/interceptors/log-request.interceptor';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule,
|
const app = await NestFactory.create(AppModule,
|
||||||
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
|
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
|
||||||
app.enableCors();
|
|
||||||
|
// Log de chaque appel API si LOG_API_REQUESTS=true (mode debug)
|
||||||
|
app.useGlobalInterceptors(new LogRequestInterceptor());
|
||||||
|
|
||||||
|
// Configuration CORS pour autoriser les requêtes depuis localhost (dev) et production
|
||||||
|
app.enableCors({
|
||||||
|
origin: true, // Autorise toutes les origines (dev) - à restreindre en prod
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
|
HttpStatus,
|
||||||
|
HttpException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AppConfigService } from './config.service';
|
||||||
|
import { UpdateConfigDto } from './dto/update-config.dto';
|
||||||
|
import { TestSmtpDto } from './dto/test-smtp.dto';
|
||||||
|
|
||||||
|
@Controller('configuration')
|
||||||
|
export class ConfigController {
|
||||||
|
constructor(private readonly configService: AppConfigService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si la configuration initiale est terminée
|
||||||
|
* GET /api/v1/configuration/setup/status
|
||||||
|
*/
|
||||||
|
@Get('setup/status')
|
||||||
|
async getSetupStatus() {
|
||||||
|
try {
|
||||||
|
const isCompleted = this.configService.isSetupCompleted();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
setupCompleted: isCompleted,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur lors de la vérification du statut de configuration',
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marquer la configuration initiale comme terminée
|
||||||
|
* POST /api/v1/configuration/setup/complete
|
||||||
|
*/
|
||||||
|
@Post('setup/complete')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin')
|
||||||
|
async completeSetup(@Request() req: any) {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id ?? null;
|
||||||
|
|
||||||
|
await this.configService.markSetupCompleted(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Configuration initiale terminée avec succès',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur lors de la finalisation de la configuration',
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test de la connexion SMTP
|
||||||
|
* POST /api/v1/configuration/test-smtp
|
||||||
|
*/
|
||||||
|
@Post('test-smtp')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin')
|
||||||
|
async testSmtp(@Body() testSmtpDto: TestSmtpDto) {
|
||||||
|
try {
|
||||||
|
const result = await this.configService.testSmtpConnection(testSmtpDto.testEmail);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Connexion SMTP réussie. Email de test envoyé.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Échec du test SMTP',
|
||||||
|
error: result.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur lors du test SMTP',
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mise à jour multiple des configurations
|
||||||
|
* PATCH /api/v1/configuration/bulk
|
||||||
|
*/
|
||||||
|
@Patch('bulk')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin')
|
||||||
|
async updateBulk(@Body() updateConfigDto: UpdateConfigDto, @Request() req: any) {
|
||||||
|
try {
|
||||||
|
// TODO: Récupérer l'ID utilisateur depuis le JWT
|
||||||
|
const userId = req.user?.id || null;
|
||||||
|
|
||||||
|
let updated = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Parcourir toutes les clés du DTO
|
||||||
|
for (const [key, value] of Object.entries(updateConfigDto)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
try {
|
||||||
|
await this.configService.set(key, value, userId);
|
||||||
|
updated++;
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`${key}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recharger le cache après les modifications
|
||||||
|
await this.configService.loadCache();
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Certaines configurations n\'ont pas pu être mises à jour',
|
||||||
|
updated,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Configuration mise à jour avec succès',
|
||||||
|
updated,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur lors de la mise à jour des configurations',
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer toutes les configurations (pour l'admin)
|
||||||
|
* GET /api/v1/configuration
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin')
|
||||||
|
async getAll() {
|
||||||
|
try {
|
||||||
|
const configs = await this.configService.getAll();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: configs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur lors de la récupération des configurations',
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les configurations par catégorie
|
||||||
|
* GET /api/v1/configuration/:category
|
||||||
|
*/
|
||||||
|
@Get(':category')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin')
|
||||||
|
async getByCategory(@Param('category') category: string) {
|
||||||
|
try {
|
||||||
|
if (!['email', 'app', 'security'].includes(category)) {
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Catégorie invalide. Valeurs acceptées: email, app, security',
|
||||||
|
},
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configs = await this.configService.getByCategory(category);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: configs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new HttpException(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Erreur lors de la récupération des configurations',
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Configuration } from '../../entities/configuration.entity';
|
||||||
|
import { AppConfigService } from './config.service';
|
||||||
|
import { ConfigController } from './config.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Configuration])],
|
||||||
|
controllers: [ConfigController],
|
||||||
|
providers: [AppConfigService],
|
||||||
|
exports: [AppConfigService],
|
||||||
|
})
|
||||||
|
export class AppConfigModule {}
|
||||||
|
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Configuration } from '../../entities/configuration.entity';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppConfigService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(AppConfigService.name);
|
||||||
|
private cache: Map<string, any> = new Map();
|
||||||
|
private readonly ENCRYPTION_KEY: string;
|
||||||
|
private readonly ENCRYPTION_ALGORITHM = 'aes-256-cbc';
|
||||||
|
private readonly IV_LENGTH = 16;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Configuration)
|
||||||
|
private configRepo: Repository<Configuration>,
|
||||||
|
) {
|
||||||
|
// Clé de chiffrement depuis les variables d'environnement
|
||||||
|
// En production, cette clé doit être générée de manière sécurisée
|
||||||
|
this.ENCRYPTION_KEY =
|
||||||
|
process.env.CONFIG_ENCRYPTION_KEY ||
|
||||||
|
crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
if (!process.env.CONFIG_ENCRYPTION_KEY) {
|
||||||
|
this.logger.warn(
|
||||||
|
'⚠️ CONFIG_ENCRYPTION_KEY non définie. Utilisation d\'une clé temporaire (NON RECOMMANDÉ EN PRODUCTION)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chargement du cache au démarrage de l'application
|
||||||
|
*/
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.loadCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chargement de toutes les configurations en cache
|
||||||
|
*/
|
||||||
|
async loadCache(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const configs = await this.configRepo.find();
|
||||||
|
this.logger.log(`📦 Chargement de ${configs.length} configurations en cache`);
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
let value = config.valeur;
|
||||||
|
|
||||||
|
// Déchiffrement si nécessaire
|
||||||
|
if (config.type === 'encrypted' && value) {
|
||||||
|
try {
|
||||||
|
value = this.decrypt(value);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`❌ Erreur de déchiffrement pour la clé '${config.cle}'`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion de type
|
||||||
|
const convertedValue = this.convertType(value, config.type);
|
||||||
|
this.cache.set(config.cle, convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('✅ Cache de configuration chargé avec succès');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Erreur lors du chargement du cache', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupération d'une valeur de configuration
|
||||||
|
* @param key Clé de configuration
|
||||||
|
* @param defaultValue Valeur par défaut si la clé n'existe pas
|
||||||
|
* @returns Valeur de configuration
|
||||||
|
*/
|
||||||
|
get<T = any>(key: string, defaultValue?: T): T {
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
return this.cache.get(key) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`⚠️ Configuration '${key}' non trouvée et aucune valeur par défaut fournie`);
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mise à jour d'une valeur de configuration
|
||||||
|
* @param key Clé de configuration
|
||||||
|
* @param value Nouvelle valeur
|
||||||
|
* @param userId ID de l'utilisateur qui modifie
|
||||||
|
*/
|
||||||
|
async set(key: string, value: any, userId?: string): Promise<void> {
|
||||||
|
const config = await this.configRepo.findOne({ where: { cle: key } });
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Configuration '${key}' non trouvée`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueToStore = value !== null && value !== undefined ? String(value) : null;
|
||||||
|
|
||||||
|
// Chiffrement si nécessaire
|
||||||
|
if (config.type === 'encrypted' && valueToStore) {
|
||||||
|
valueToStore = this.encrypt(valueToStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.valeur = valueToStore;
|
||||||
|
config.modifieLe = new Date();
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
config.modifiePar = { id: userId } as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.configRepo.save(config);
|
||||||
|
|
||||||
|
// Mise à jour du cache (avec la valeur déchiffrée)
|
||||||
|
this.cache.set(key, value);
|
||||||
|
|
||||||
|
this.logger.log(`✅ Configuration '${key}' mise à jour`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupération de toutes les configurations par catégorie
|
||||||
|
* @param category Catégorie de configuration
|
||||||
|
* @returns Objet clé/valeur des configurations
|
||||||
|
*/
|
||||||
|
async getByCategory(category: string): Promise<Record<string, any>> {
|
||||||
|
const configs = await this.configRepo.find({
|
||||||
|
where: { categorie: category as any },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
let value = config.valeur;
|
||||||
|
|
||||||
|
// Masquer les mots de passe
|
||||||
|
if (config.type === 'encrypted') {
|
||||||
|
value = value ? '***********' : null;
|
||||||
|
} else {
|
||||||
|
value = this.convertType(value, config.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[config.cle] = {
|
||||||
|
value,
|
||||||
|
type: config.type,
|
||||||
|
description: config.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupération de toutes les configurations (pour l'admin)
|
||||||
|
* @returns Liste de toutes les configurations
|
||||||
|
*/
|
||||||
|
async getAll(): Promise<Configuration[]> {
|
||||||
|
const configs = await this.configRepo.find({
|
||||||
|
order: { categorie: 'ASC', cle: 'ASC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Masquer les valeurs chiffrées
|
||||||
|
return configs.map((config) => ({
|
||||||
|
...config,
|
||||||
|
valeur: config.type === 'encrypted' && config.valeur ? '***********' : config.valeur,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test de la configuration SMTP
|
||||||
|
* @param testEmail Email de destination pour le test
|
||||||
|
* @returns Objet avec success et error éventuel
|
||||||
|
*/
|
||||||
|
async testSmtpConnection(testEmail?: string): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
this.logger.log('🧪 Test de connexion SMTP...');
|
||||||
|
|
||||||
|
// Récupération de la configuration SMTP
|
||||||
|
const smtpHost = this.get<string>('smtp_host');
|
||||||
|
const smtpPort = this.get<number>('smtp_port');
|
||||||
|
const smtpSecure = this.get<boolean>('smtp_secure');
|
||||||
|
const smtpAuthRequired = this.get<boolean>('smtp_auth_required');
|
||||||
|
const smtpUser = this.get<string>('smtp_user');
|
||||||
|
const smtpPassword = this.get<string>('smtp_password');
|
||||||
|
const emailFromName = this.get<string>('email_from_name');
|
||||||
|
const emailFromAddress = this.get<string>('email_from_address');
|
||||||
|
|
||||||
|
// Import dynamique de nodemailer
|
||||||
|
const nodemailer = await import('nodemailer');
|
||||||
|
|
||||||
|
// Configuration du transporteur
|
||||||
|
const transportConfig: any = {
|
||||||
|
host: smtpHost,
|
||||||
|
port: smtpPort,
|
||||||
|
secure: smtpSecure,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (smtpAuthRequired && smtpUser && smtpPassword) {
|
||||||
|
transportConfig.auth = {
|
||||||
|
user: smtpUser,
|
||||||
|
pass: smtpPassword,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport(transportConfig);
|
||||||
|
|
||||||
|
// Vérification de la connexion
|
||||||
|
await transporter.verify();
|
||||||
|
this.logger.log('✅ Connexion SMTP vérifiée');
|
||||||
|
|
||||||
|
// Si un email de test est fourni, on envoie un email
|
||||||
|
if (testEmail) {
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"${emailFromName}" <${emailFromAddress}>`,
|
||||||
|
to: testEmail,
|
||||||
|
subject: '🧪 Test de configuration SMTP - P\'titsPas',
|
||||||
|
text: 'Ceci est un email de test pour vérifier la configuration SMTP de votre application P\'titsPas.',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2 style="color: #4CAF50;">✅ Test de configuration SMTP réussi !</h2>
|
||||||
|
<p>Ceci est un email de test pour vérifier la configuration SMTP de votre application <strong>P'titsPas</strong>.</p>
|
||||||
|
<p>Si vous recevez cet email, cela signifie que votre configuration SMTP fonctionne correctement.</p>
|
||||||
|
<hr style="border: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="color: #666; font-size: 12px;">
|
||||||
|
Cet email a été envoyé automatiquement depuis votre application P'titsPas.<br>
|
||||||
|
Configuration testée le ${new Date().toLocaleString('fr-FR')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
this.logger.log(`📧 Email de test envoyé à ${testEmail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Échec du test SMTP', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Erreur inconnue lors du test SMTP',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérification si la configuration initiale est terminée
|
||||||
|
* @returns true si la configuration est terminée
|
||||||
|
*/
|
||||||
|
isSetupCompleted(): boolean {
|
||||||
|
return this.get<boolean>('setup_completed', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marquer la configuration initiale comme terminée
|
||||||
|
* @param userId ID de l'utilisateur qui termine la configuration (null si non authentifié)
|
||||||
|
*/
|
||||||
|
async markSetupCompleted(userId: string | null): Promise<void> {
|
||||||
|
await this.set('setup_completed', 'true', userId ?? undefined);
|
||||||
|
this.logger.log('✅ Configuration initiale marquée comme terminée');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conversion de type selon le type de configuration
|
||||||
|
* @param value Valeur à convertir
|
||||||
|
* @param type Type cible
|
||||||
|
* @returns Valeur convertie
|
||||||
|
*/
|
||||||
|
private convertType(value: string | null, type: string): any {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
return parseFloat(value);
|
||||||
|
case 'boolean':
|
||||||
|
return value === 'true' || value === '1';
|
||||||
|
case 'json':
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case 'string':
|
||||||
|
case 'encrypted':
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chiffrement AES-256-CBC
|
||||||
|
* @param text Texte à chiffrer
|
||||||
|
* @returns Texte chiffré (format: iv:encrypted)
|
||||||
|
*/
|
||||||
|
private encrypt(text: string): string {
|
||||||
|
const iv = crypto.randomBytes(this.IV_LENGTH);
|
||||||
|
const key = Buffer.from(this.ENCRYPTION_KEY, 'hex');
|
||||||
|
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
|
// Format: iv:encrypted
|
||||||
|
return `${iv.toString('hex')}:${encrypted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déchiffrement AES-256-CBC
|
||||||
|
* @param encryptedText Texte chiffré (format: iv:encrypted)
|
||||||
|
* @returns Texte déchiffré
|
||||||
|
*/
|
||||||
|
private decrypt(encryptedText: string): string {
|
||||||
|
const parts = encryptedText.split(':');
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
throw new Error('Format de chiffrement invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
const iv = Buffer.from(parts[0], 'hex');
|
||||||
|
const encrypted = parts[1];
|
||||||
|
const key = Buffer.from(this.ENCRYPTION_KEY, 'hex');
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IsEmail } from 'class-validator';
|
||||||
|
|
||||||
|
export class TestSmtpDto {
|
||||||
|
@IsEmail()
|
||||||
|
testEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { IsString, IsOptional, IsNumber, IsBoolean, IsEmail, IsUrl } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateConfigDto {
|
||||||
|
// Configuration Email (SMTP)
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
smtp_host?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
smtp_port?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
smtp_secure?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
smtp_auth_required?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
smtp_user?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
smtp_password?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
email_from_name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email_from_address?: string;
|
||||||
|
|
||||||
|
// Configuration Application
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
app_name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUrl()
|
||||||
|
app_url?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
app_logo_url?: string;
|
||||||
|
|
||||||
|
// Configuration Sécurité
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
password_reset_token_expiry_days?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
jwt_expiry_hours?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
max_upload_size_mb?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
bcrypt_rounds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './config.module';
|
||||||
|
export * from './config.service';
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Body,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
|
Res,
|
||||||
|
HttpStatus,
|
||||||
|
BadRequestException,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
StreamableFile,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import { DocumentsLegauxService } from './documents-legaux.service';
|
||||||
|
import { UploadDocumentDto } from './dto/upload-document.dto';
|
||||||
|
import { DocumentsActifsResponseDto } from './dto/documents-actifs.dto';
|
||||||
|
import { DocumentVersionDto } from './dto/document-version.dto';
|
||||||
|
|
||||||
|
@Controller('documents-legaux')
|
||||||
|
export class DocumentsLegauxController {
|
||||||
|
constructor(private readonly documentsService: DocumentsLegauxService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/documents-legaux/actifs
|
||||||
|
* Récupérer les documents actifs (CGU + Privacy)
|
||||||
|
* PUBLIC
|
||||||
|
*/
|
||||||
|
@Get('actifs')
|
||||||
|
async getDocumentsActifs(): Promise<DocumentsActifsResponseDto> {
|
||||||
|
const { cgu, privacy } = await this.documentsService.getDocumentsActifs();
|
||||||
|
|
||||||
|
return {
|
||||||
|
cgu: {
|
||||||
|
id: cgu.id,
|
||||||
|
type: cgu.type,
|
||||||
|
version: cgu.version,
|
||||||
|
url: `/api/v1/documents-legaux/${cgu.id}/download`,
|
||||||
|
activeLe: cgu.activeLe,
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
id: privacy.id,
|
||||||
|
type: privacy.type,
|
||||||
|
version: privacy.version,
|
||||||
|
url: `/api/v1/documents-legaux/${privacy.id}/download`,
|
||||||
|
activeLe: privacy.activeLe,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/documents-legaux/:type/versions
|
||||||
|
* Lister toutes les versions d'un type de document
|
||||||
|
* ADMIN ONLY
|
||||||
|
*/
|
||||||
|
@Get(':type/versions')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin', 'administrateur')
|
||||||
|
async listerVersions(@Param('type') type: string): Promise<DocumentVersionDto[]> {
|
||||||
|
if (type !== 'cgu' && type !== 'privacy') {
|
||||||
|
throw new BadRequestException('Le type doit être "cgu" ou "privacy"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const documents = await this.documentsService.listerVersions(type as 'cgu' | 'privacy');
|
||||||
|
|
||||||
|
return documents.map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
version: doc.version,
|
||||||
|
fichier_nom: doc.fichier_nom,
|
||||||
|
actif: doc.actif,
|
||||||
|
televersePar: doc.televersePar
|
||||||
|
? {
|
||||||
|
id: doc.televersePar.id,
|
||||||
|
prenom: doc.televersePar.prenom,
|
||||||
|
nom: doc.televersePar.nom,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
televerseLe: doc.televerseLe,
|
||||||
|
activeLe: doc.activeLe,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/documents-legaux
|
||||||
|
* Upload une nouvelle version d'un document
|
||||||
|
* ADMIN ONLY
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin', 'administrateur')
|
||||||
|
async uploadDocument(
|
||||||
|
@Body() uploadDto: UploadDocumentDto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
// @CurrentUser() user: any, // TODO: Décommenter quand le guard sera implémenté
|
||||||
|
) {
|
||||||
|
if (!file) {
|
||||||
|
throw new BadRequestException('Aucun fichier fourni');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Récupérer l'ID utilisateur depuis le guard
|
||||||
|
const userId = '00000000-0000-0000-0000-000000000000'; // Temporaire
|
||||||
|
|
||||||
|
const document = await this.documentsService.uploadNouvelleVersion(
|
||||||
|
uploadDto.type,
|
||||||
|
file,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: document.id,
|
||||||
|
type: document.type,
|
||||||
|
version: document.version,
|
||||||
|
fichier_nom: document.fichier_nom,
|
||||||
|
actif: document.actif,
|
||||||
|
televerseLe: document.televerseLe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/v1/documents-legaux/:id/activer
|
||||||
|
* Activer une version d'un document
|
||||||
|
* ADMIN ONLY
|
||||||
|
*/
|
||||||
|
@Patch(':id/activer')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin', 'administrateur')
|
||||||
|
async activerVersion(@Param('id', ParseUUIDPipe) documentId: string) {
|
||||||
|
await this.documentsService.activerVersion(documentId);
|
||||||
|
|
||||||
|
// Récupérer le document pour retourner les infos
|
||||||
|
const documents = await this.documentsService.listerVersions('cgu');
|
||||||
|
const document = documents.find((d) => d.id === documentId);
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
const documentsPrivacy = await this.documentsService.listerVersions('privacy');
|
||||||
|
const docPrivacy = documentsPrivacy.find((d) => d.id === documentId);
|
||||||
|
|
||||||
|
if (!docPrivacy) {
|
||||||
|
throw new BadRequestException('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Document activé avec succès',
|
||||||
|
documentId: docPrivacy.id,
|
||||||
|
type: docPrivacy.type,
|
||||||
|
version: docPrivacy.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Document activé avec succès',
|
||||||
|
documentId: document.id,
|
||||||
|
type: document.type,
|
||||||
|
version: document.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/documents-legaux/:id/download
|
||||||
|
* Télécharger un document
|
||||||
|
* PUBLIC
|
||||||
|
*/
|
||||||
|
@Get(':id/download')
|
||||||
|
async telechargerDocument(
|
||||||
|
@Param('id', ParseUUIDPipe) documentId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
) {
|
||||||
|
const { stream, filename } = await this.documentsService.telechargerDocument(documentId);
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(HttpStatus.OK).send(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/documents-legaux/:id/verifier-integrite
|
||||||
|
* Vérifier l'intégrité d'un document (hash SHA-256)
|
||||||
|
* ADMIN ONLY
|
||||||
|
*/
|
||||||
|
@Get(':id/verifier-integrite')
|
||||||
|
// @UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
// @Roles('super_admin', 'administrateur')
|
||||||
|
async verifierIntegrite(@Param('id', ParseUUIDPipe) documentId: string) {
|
||||||
|
const integre = await this.documentsService.verifierIntegrite(documentId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentId,
|
||||||
|
integre,
|
||||||
|
message: integre
|
||||||
|
? 'Le document est intègre (hash valide)'
|
||||||
|
: 'ALERTE : Le document a été modifié (hash invalide)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { DocumentLegal } from '../../entities/document-legal.entity';
|
||||||
|
import { AcceptationDocument } from '../../entities/acceptation-document.entity';
|
||||||
|
import { DocumentsLegauxService } from './documents-legaux.service';
|
||||||
|
import { DocumentsLegauxController } from './documents-legaux.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([DocumentLegal, AcceptationDocument])],
|
||||||
|
providers: [DocumentsLegauxService],
|
||||||
|
controllers: [DocumentsLegauxController],
|
||||||
|
exports: [DocumentsLegauxService],
|
||||||
|
})
|
||||||
|
export class DocumentsLegauxModule {}
|
||||||
|
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { DocumentLegal } from '../../entities/document-legal.entity';
|
||||||
|
import { AcceptationDocument } from '../../entities/acceptation-document.entity';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentsLegauxService {
|
||||||
|
private readonly UPLOAD_DIR = '/app/documents/legaux';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DocumentLegal)
|
||||||
|
private docRepo: Repository<DocumentLegal>,
|
||||||
|
@InjectRepository(AcceptationDocument)
|
||||||
|
private acceptationRepo: Repository<AcceptationDocument>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les documents actifs (CGU + Privacy)
|
||||||
|
*/
|
||||||
|
async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> {
|
||||||
|
const cgu = await this.docRepo.findOne({
|
||||||
|
where: { type: 'cgu', actif: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const privacy = await this.docRepo.findOne({
|
||||||
|
where: { type: 'privacy', actif: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cgu || !privacy) {
|
||||||
|
throw new NotFoundException('Documents légaux manquants');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cgu, privacy };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploader une nouvelle version d'un document
|
||||||
|
*/
|
||||||
|
async uploadNouvelleVersion(
|
||||||
|
type: 'cgu' | 'privacy',
|
||||||
|
file: Express.Multer.File,
|
||||||
|
userId: string,
|
||||||
|
): Promise<DocumentLegal> {
|
||||||
|
// Validation du type de fichier
|
||||||
|
if (file.mimetype !== 'application/pdf') {
|
||||||
|
throw new BadRequestException('Seuls les fichiers PDF sont acceptés');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation de la taille (max 10MB)
|
||||||
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
throw new BadRequestException('Le fichier ne doit pas dépasser 10MB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Calculer la prochaine version
|
||||||
|
const lastDoc = await this.docRepo.findOne({
|
||||||
|
where: { type },
|
||||||
|
order: { version: 'DESC' },
|
||||||
|
});
|
||||||
|
const nouvelleVersion = (lastDoc?.version || 0) + 1;
|
||||||
|
|
||||||
|
// 2. Calculer le hash SHA-256 du fichier
|
||||||
|
const fileBuffer = file.buffer;
|
||||||
|
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||||
|
|
||||||
|
// 3. Générer le nom de fichier unique
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`;
|
||||||
|
const filePath = path.join(this.UPLOAD_DIR, fileName);
|
||||||
|
|
||||||
|
// 4. Créer le répertoire si nécessaire et sauvegarder le fichier
|
||||||
|
await fs.mkdir(this.UPLOAD_DIR, { recursive: true });
|
||||||
|
await fs.writeFile(filePath, fileBuffer);
|
||||||
|
|
||||||
|
// 5. Créer l'entrée en BDD
|
||||||
|
const document = this.docRepo.create({
|
||||||
|
type,
|
||||||
|
version: nouvelleVersion,
|
||||||
|
fichier_nom: file.originalname,
|
||||||
|
fichier_path: filePath,
|
||||||
|
fichier_hash: hash,
|
||||||
|
actif: false, // Pas actif par défaut
|
||||||
|
televersePar: { id: userId } as any,
|
||||||
|
televerseLe: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.docRepo.save(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activer une version (désactive automatiquement l'ancienne)
|
||||||
|
*/
|
||||||
|
async activerVersion(documentId: string): Promise<void> {
|
||||||
|
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new NotFoundException('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction : désactiver l'ancienne version, activer la nouvelle
|
||||||
|
await this.docRepo.manager.transaction(async (manager) => {
|
||||||
|
// Désactiver toutes les versions de ce type
|
||||||
|
await manager.update(
|
||||||
|
DocumentLegal,
|
||||||
|
{ type: document.type, actif: true },
|
||||||
|
{ actif: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activer la nouvelle version
|
||||||
|
await manager.update(
|
||||||
|
DocumentLegal,
|
||||||
|
{ id: documentId },
|
||||||
|
{ actif: true, activeLe: new Date() },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lister toutes les versions d'un type de document
|
||||||
|
*/
|
||||||
|
async listerVersions(type: 'cgu' | 'privacy'): Promise<DocumentLegal[]> {
|
||||||
|
return await this.docRepo.find({
|
||||||
|
where: { type },
|
||||||
|
order: { version: 'DESC' },
|
||||||
|
relations: ['televersePar'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharger un document (retourne le buffer et le nom)
|
||||||
|
*/
|
||||||
|
async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> {
|
||||||
|
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new NotFoundException('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileBuffer = await fs.readFile(document.fichier_path);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: fileBuffer,
|
||||||
|
filename: document.fichier_nom,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new NotFoundException('Fichier introuvable sur le système de fichiers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier l'intégrité d'un document (hash SHA-256)
|
||||||
|
*/
|
||||||
|
async verifierIntegrite(documentId: string): Promise<boolean> {
|
||||||
|
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new NotFoundException('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileBuffer = await fs.readFile(document.fichier_path);
|
||||||
|
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||||
|
|
||||||
|
return hash === document.fichier_hash;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistrer une acceptation de document (lors de l'inscription)
|
||||||
|
*/
|
||||||
|
async enregistrerAcceptation(
|
||||||
|
userId: string,
|
||||||
|
documentId: string,
|
||||||
|
typeDocument: 'cgu' | 'privacy',
|
||||||
|
versionDocument: number,
|
||||||
|
ipAddress: string | null,
|
||||||
|
userAgent: string | null,
|
||||||
|
): Promise<AcceptationDocument> {
|
||||||
|
const acceptation = this.acceptationRepo.create({
|
||||||
|
utilisateur: { id: userId } as any,
|
||||||
|
document: { id: documentId } as any,
|
||||||
|
type_document: typeDocument,
|
||||||
|
version_document: versionDocument,
|
||||||
|
ip_address: ipAddress,
|
||||||
|
user_agent: userAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.acceptationRepo.save(acceptation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer l'historique des acceptations d'un utilisateur
|
||||||
|
*/
|
||||||
|
async getAcceptationsUtilisateur(userId: string): Promise<AcceptationDocument[]> {
|
||||||
|
return await this.acceptationRepo.find({
|
||||||
|
where: { utilisateur: { id: userId } },
|
||||||
|
order: { accepteLe: 'DESC' },
|
||||||
|
relations: ['document'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export class DocumentVersionDto {
|
||||||
|
id: string;
|
||||||
|
version: number;
|
||||||
|
fichier_nom: string;
|
||||||
|
actif: boolean;
|
||||||
|
televersePar: {
|
||||||
|
id: string;
|
||||||
|
prenom?: string;
|
||||||
|
nom?: string;
|
||||||
|
} | null;
|
||||||
|
televerseLe: Date;
|
||||||
|
activeLe: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export class DocumentActifDto {
|
||||||
|
id: string;
|
||||||
|
type: 'cgu' | 'privacy';
|
||||||
|
version: number;
|
||||||
|
url: string;
|
||||||
|
activeLe: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DocumentsActifsResponseDto {
|
||||||
|
cgu: DocumentActifDto;
|
||||||
|
privacy: DocumentActifDto;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class UploadDocumentDto {
|
||||||
|
@IsEnum(['cgu', 'privacy'], { message: 'Le type doit être "cgu" ou "privacy"' })
|
||||||
|
@IsNotEmpty({ message: 'Le type est requis' })
|
||||||
|
type: 'cgu' | 'privacy';
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './documents-legaux.module';
|
||||||
|
export * from './documents-legaux.service';
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export class AssistantesMaternellesController {
|
|||||||
return this.assistantesMaternellesService.create(dto);
|
return this.assistantesMaternellesService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
||||||
@ApiResponse({ status: 200, description: 'Liste des nounous' })
|
@ApiResponse({ status: 200, description: 'Liste des nounous' })
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Body, Controller, Get, Post, Req, UnauthorizedException, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Req, UnauthorizedException, BadRequestException, UseGuards } from '@nestjs/common';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './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 './dto/register.dto';
|
||||||
|
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
|
||||||
|
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
|
||||||
|
import { ChangePasswordRequiredDto } from './dto/change-password.dto';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiResponse, 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 type { Request } from 'express';
|
||||||
@@ -30,11 +33,38 @@ export class AuthController {
|
|||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Post('register')
|
@Post('register')
|
||||||
@ApiOperation({ summary: 'Inscription' })
|
@ApiOperation({ summary: 'Inscription (OBSOLÈTE - utiliser /register/parent)' })
|
||||||
|
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||||
async register(@Body() dto: RegisterDto) {
|
async register(@Body() dto: RegisterDto) {
|
||||||
return this.authService.register(dto);
|
return this.authService.register(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('register/parent')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Inscription Parent COMPLÈTE - Workflow 6 étapes',
|
||||||
|
description: 'Crée Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU en une transaction'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
|
||||||
|
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||||
|
async inscrireParentComplet(@Body() dto: RegisterParentCompletDto) {
|
||||||
|
return this.authService.inscrireParentComplet(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('register/am')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Inscription Assistante Maternelle COMPLÈTE',
|
||||||
|
description: 'Crée User AM + entrée assistantes_maternelles (identité + infos pro + photo + CGU) en une transaction',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
|
||||||
|
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
|
||||||
|
async inscrireAMComplet(@Body() dto: RegisterAMCompletDto) {
|
||||||
|
return this.authService.inscrireAMComplet(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
@ApiBearerAuth('refresh_token')
|
@ApiBearerAuth('refresh_token')
|
||||||
@@ -62,6 +92,7 @@ export class AuthController {
|
|||||||
prenom: user.prenom ?? '',
|
prenom: user.prenom ?? '',
|
||||||
nom: user.nom ?? '',
|
nom: user.nom ?? '',
|
||||||
statut: user.statut,
|
statut: user.statut,
|
||||||
|
changement_mdp_obligatoire: user.changement_mdp_obligatoire,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,5 +102,31 @@ export class AuthController {
|
|||||||
logout(@User() currentUser: Users) {
|
logout(@User() currentUser: Users) {
|
||||||
return this.authService.logout(currentUser.id);
|
return this.authService.logout(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('change-password-required')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth('access-token')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Changement de mot de passe obligatoire',
|
||||||
|
description: 'Permet de changer le mot de passe lors de la première connexion (flag changement_mdp_obligatoire)'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Mot de passe changé avec succès' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Mot de passe actuel incorrect ou confirmation non correspondante' })
|
||||||
|
@ApiResponse({ status: 403, description: 'Changement de mot de passe non requis pour cet utilisateur' })
|
||||||
|
async changePasswordRequired(
|
||||||
|
@User() currentUser: Users,
|
||||||
|
@Body() dto: ChangePasswordRequiredDto,
|
||||||
|
) {
|
||||||
|
// Vérifier que les mots de passe correspondent
|
||||||
|
if (dto.nouveau_mot_de_passe !== dto.confirmation_mot_de_passe) {
|
||||||
|
throw new BadRequestException('Les mots de passe ne correspondent pas');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authService.changePasswordRequired(
|
||||||
|
currentUser.id,
|
||||||
|
dto.mot_de_passe_actuel,
|
||||||
|
dto.nouveau_mot_de_passe,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import { forwardRef, Module } from '@nestjs/common';
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { Users } from 'src/entities/users.entity';
|
||||||
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { Children } from 'src/entities/children.entity';
|
||||||
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
|
import { AppConfigModule } from 'src/modules/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]),
|
||||||
forwardRef(() => UserModule),
|
forwardRef(() => UserModule),
|
||||||
|
AppConfigModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: (config: ConfigService) => ({
|
useFactory: (config: ConfigService) => ({
|
||||||
|
|||||||
@@ -2,14 +2,27 @@ import {
|
|||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
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 * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
|
||||||
|
import { RegisterAMCompletDto } from './dto/register-am-complet.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 { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { Children, StatutEnfantType } from 'src/entities/children.entity';
|
||||||
|
import { ParentsChildren } from 'src/entities/parents_children.entity';
|
||||||
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { AppConfigService } from 'src/modules/config/config.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -17,6 +30,13 @@ export class AuthService {
|
|||||||
private readonly usersService: UserService,
|
private readonly usersService: UserService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly appConfigService: AppConfigService,
|
||||||
|
@InjectRepository(Parents)
|
||||||
|
private readonly parentsRepo: Repository<Parents>,
|
||||||
|
@InjectRepository(Users)
|
||||||
|
private readonly usersRepo: Repository<Users>,
|
||||||
|
@InjectRepository(Children)
|
||||||
|
private readonly childrenRepo: Repository<Children>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,29 +63,37 @@ export class AuthService {
|
|||||||
* Connexion utilisateur
|
* Connexion utilisateur
|
||||||
*/
|
*/
|
||||||
async login(dto: LoginDto) {
|
async login(dto: LoginDto) {
|
||||||
try {
|
const user = await this.usersService.findByEmailOrNull(dto.email);
|
||||||
const user = await this.usersService.findByEmailOrNull(dto.email);
|
|
||||||
|
|
||||||
if (!user) {
|
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) {
|
|
||||||
// throw new UnauthorizedException('Mot de passe invalide');
|
|
||||||
// }
|
|
||||||
|
|
||||||
return this.generateTokens(user.id, user.email, user.role);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur de connexion :', error);
|
|
||||||
throw new UnauthorizedException('Identifiants invalides');
|
throw new UnauthorizedException('Identifiants invalides');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier que le mot de passe existe (compte activé)
|
||||||
|
if (!user.password) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'Compte non activé. Veuillez créer votre mot de passe via le lien reçu par email.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le mot de passe
|
||||||
|
const isMatch = await bcrypt.compare(dto.password, user.password);
|
||||||
|
if (!isMatch) {
|
||||||
|
throw new UnauthorizedException('Identifiants invalides');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le statut du compte
|
||||||
|
if (user.statut === StatutUtilisateurType.EN_ATTENTE) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'Votre compte est en attente de validation par un gestionnaire.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.statut === StatutUtilisateurType.SUSPENDU) {
|
||||||
|
throw new UnauthorizedException('Votre compte a été suspendu. Contactez un administrateur.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.generateTokens(user.id, user.email, user.role);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +117,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inscription utilisateur lambda (parent ou assistante maternelle)
|
* Inscription utilisateur OBSOLÈTE - Utiliser inscrireParentComplet() ou registerAM()
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
async register(registerDto: RegisterDto) {
|
async register(registerDto: RegisterDto) {
|
||||||
const exists = await this.usersService.findByEmailOrNull(registerDto.email);
|
const exists = await this.usersService.findByEmailOrNull(registerDto.email);
|
||||||
@@ -129,9 +158,316 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
|
||||||
|
* Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
|
||||||
|
*/
|
||||||
|
async inscrireParentComplet(dto: RegisterParentCompletDto) {
|
||||||
|
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
|
||||||
|
throw new BadRequestException('L\'acceptation des CGU et de la politique de confidentialité est obligatoire');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dto.enfants || dto.enfants.length === 0) {
|
||||||
|
throw new BadRequestException('Au moins un enfant est requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existe = await this.usersService.findByEmailOrNull(dto.email);
|
||||||
|
if (existe) {
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.co_parent_email) {
|
||||||
|
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
|
||||||
|
if (coParentExiste) {
|
||||||
|
throw new ConflictException('L\'email du co-parent est déjà utilisé');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const joursExpirationToken = await this.appConfigService.get<number>(
|
||||||
|
'password_reset_token_expiry_days',
|
||||||
|
7,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenCreationMdp = crypto.randomUUID();
|
||||||
|
const dateExpiration = new Date();
|
||||||
|
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
|
||||||
|
|
||||||
|
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
|
||||||
|
const parent1 = manager.create(Users, {
|
||||||
|
email: dto.email,
|
||||||
|
prenom: dto.prenom,
|
||||||
|
nom: dto.nom,
|
||||||
|
role: RoleType.PARENT,
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
telephone: dto.telephone,
|
||||||
|
adresse: dto.adresse,
|
||||||
|
code_postal: dto.code_postal,
|
||||||
|
ville: dto.ville,
|
||||||
|
token_creation_mdp: tokenCreationMdp,
|
||||||
|
token_creation_mdp_expire_le: dateExpiration,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parent1Enregistre = await manager.save(Users, parent1);
|
||||||
|
|
||||||
|
let parent2Enregistre: Users | null = null;
|
||||||
|
let tokenCoParent: string | null = null;
|
||||||
|
|
||||||
|
if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) {
|
||||||
|
tokenCoParent = crypto.randomUUID();
|
||||||
|
const dateExpirationCoParent = new Date();
|
||||||
|
dateExpirationCoParent.setDate(dateExpirationCoParent.getDate() + joursExpirationToken);
|
||||||
|
|
||||||
|
const parent2 = manager.create(Users, {
|
||||||
|
email: dto.co_parent_email,
|
||||||
|
prenom: dto.co_parent_prenom,
|
||||||
|
nom: dto.co_parent_nom,
|
||||||
|
role: RoleType.PARENT,
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
telephone: dto.co_parent_telephone,
|
||||||
|
adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse,
|
||||||
|
code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal,
|
||||||
|
ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
|
||||||
|
token_creation_mdp: tokenCoParent,
|
||||||
|
token_creation_mdp_expire_le: dateExpirationCoParent,
|
||||||
|
});
|
||||||
|
|
||||||
|
parent2Enregistre = await manager.save(Users, parent2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entiteParent = manager.create(Parents, {
|
||||||
|
user_id: parent1Enregistre.id,
|
||||||
|
});
|
||||||
|
entiteParent.user = parent1Enregistre;
|
||||||
|
if (parent2Enregistre) {
|
||||||
|
entiteParent.co_parent = parent2Enregistre;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.save(Parents, entiteParent);
|
||||||
|
|
||||||
|
if (parent2Enregistre) {
|
||||||
|
const entiteCoParent = manager.create(Parents, {
|
||||||
|
user_id: parent2Enregistre.id,
|
||||||
|
});
|
||||||
|
entiteCoParent.user = parent2Enregistre;
|
||||||
|
entiteCoParent.co_parent = parent1Enregistre;
|
||||||
|
|
||||||
|
await manager.save(Parents, entiteCoParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfantsEnregistres: Children[] = [];
|
||||||
|
for (const enfantDto of dto.enfants) {
|
||||||
|
let urlPhoto: string | null = null;
|
||||||
|
|
||||||
|
if (enfantDto.photo_base64 && enfantDto.photo_filename) {
|
||||||
|
urlPhoto = await this.sauvegarderPhotoDepuisBase64(
|
||||||
|
enfantDto.photo_base64,
|
||||||
|
enfantDto.photo_filename,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfant = new Children();
|
||||||
|
enfant.first_name = enfantDto.prenom;
|
||||||
|
enfant.last_name = enfantDto.nom || dto.nom;
|
||||||
|
enfant.gender = enfantDto.genre;
|
||||||
|
enfant.birth_date = enfantDto.date_naissance ? new Date(enfantDto.date_naissance) : undefined;
|
||||||
|
enfant.due_date = enfantDto.date_previsionnelle_naissance
|
||||||
|
? new Date(enfantDto.date_previsionnelle_naissance)
|
||||||
|
: undefined;
|
||||||
|
enfant.photo_url = urlPhoto || undefined;
|
||||||
|
enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE;
|
||||||
|
enfant.consent_photo = false;
|
||||||
|
enfant.is_multiple = enfantDto.grossesse_multiple || false;
|
||||||
|
|
||||||
|
const enfantEnregistre = await manager.save(Children, enfant);
|
||||||
|
enfantsEnregistres.push(enfantEnregistre);
|
||||||
|
|
||||||
|
const lienParentEnfant1 = manager.create(ParentsChildren, {
|
||||||
|
parentId: parent1Enregistre.id,
|
||||||
|
enfantId: enfantEnregistre.id,
|
||||||
|
});
|
||||||
|
await manager.save(ParentsChildren, lienParentEnfant1);
|
||||||
|
|
||||||
|
if (parent2Enregistre) {
|
||||||
|
const lienParentEnfant2 = manager.create(ParentsChildren, {
|
||||||
|
parentId: parent2Enregistre.id,
|
||||||
|
enfantId: enfantEnregistre.id,
|
||||||
|
});
|
||||||
|
await manager.save(ParentsChildren, lienParentEnfant2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parent1: parent1Enregistre,
|
||||||
|
parent2: parent2Enregistre,
|
||||||
|
enfants: enfantsEnregistres,
|
||||||
|
tokenCreationMdp,
|
||||||
|
tokenCoParent,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
|
||||||
|
parent_id: resultat.parent1.id,
|
||||||
|
co_parent_id: resultat.parent2?.id,
|
||||||
|
enfants_ids: resultat.enfants.map(e => e.id),
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
|
||||||
|
* Crée User (role AM) + entrée assistantes_maternelles, token création MDP
|
||||||
|
*/
|
||||||
|
async inscrireAMComplet(dto: RegisterAMCompletDto) {
|
||||||
|
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"L'acceptation des CGU et de la politique de confidentialité est obligatoire",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existe = await this.usersService.findByEmailOrNull(dto.email);
|
||||||
|
if (existe) {
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
}
|
||||||
|
|
||||||
|
const joursExpirationToken = await this.appConfigService.get<number>(
|
||||||
|
'password_reset_token_expiry_days',
|
||||||
|
7,
|
||||||
|
);
|
||||||
|
const tokenCreationMdp = crypto.randomUUID();
|
||||||
|
const dateExpiration = new Date();
|
||||||
|
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
|
||||||
|
|
||||||
|
let urlPhoto: string | null = null;
|
||||||
|
if (dto.photo_base64 && dto.photo_filename) {
|
||||||
|
urlPhoto = await this.sauvegarderPhotoDepuisBase64(dto.photo_base64, dto.photo_filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateConsentementPhoto =
|
||||||
|
dto.consentement_photo ? new Date() : undefined;
|
||||||
|
|
||||||
|
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
|
||||||
|
const user = manager.create(Users, {
|
||||||
|
email: dto.email,
|
||||||
|
prenom: dto.prenom,
|
||||||
|
nom: dto.nom,
|
||||||
|
role: RoleType.ASSISTANTE_MATERNELLE,
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
telephone: dto.telephone,
|
||||||
|
adresse: dto.adresse,
|
||||||
|
code_postal: dto.code_postal,
|
||||||
|
ville: dto.ville,
|
||||||
|
token_creation_mdp: tokenCreationMdp,
|
||||||
|
token_creation_mdp_expire_le: dateExpiration,
|
||||||
|
photo_url: urlPhoto ?? undefined,
|
||||||
|
consentement_photo: dto.consentement_photo,
|
||||||
|
date_consentement_photo: dateConsentementPhoto,
|
||||||
|
date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
|
||||||
|
});
|
||||||
|
const userEnregistre = await manager.save(Users, user);
|
||||||
|
|
||||||
|
const amRepo = manager.getRepository(AssistanteMaternelle);
|
||||||
|
const am = amRepo.create({
|
||||||
|
user_id: userEnregistre.id,
|
||||||
|
approval_number: dto.numero_agrement,
|
||||||
|
nir: dto.nir,
|
||||||
|
max_children: dto.capacite_accueil,
|
||||||
|
biography: dto.biographie,
|
||||||
|
residence_city: dto.ville ?? undefined,
|
||||||
|
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
|
||||||
|
available: true,
|
||||||
|
});
|
||||||
|
await amRepo.save(am);
|
||||||
|
|
||||||
|
return { user: userEnregistre };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
|
||||||
|
user_id: resultat.user.id,
|
||||||
|
statut: StatutUtilisateurType.EN_ATTENTE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde une photo depuis base64 vers le système de fichiers
|
||||||
|
*/
|
||||||
|
private async sauvegarderPhotoDepuisBase64(donneesBase64: string, nomFichier: string): Promise<string> {
|
||||||
|
const correspondances = donneesBase64.match(/^data:image\/(\w+);base64,(.+)$/);
|
||||||
|
if (!correspondances) {
|
||||||
|
throw new BadRequestException('Format de photo invalide (doit être base64)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = correspondances[1];
|
||||||
|
const tamponImage = Buffer.from(correspondances[2], 'base64');
|
||||||
|
|
||||||
|
const dossierUpload = '/app/uploads/photos';
|
||||||
|
await fs.mkdir(dossierUpload, { recursive: true });
|
||||||
|
|
||||||
|
const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;
|
||||||
|
const cheminFichier = path.join(dossierUpload, nomFichierUnique);
|
||||||
|
|
||||||
|
await fs.writeFile(cheminFichier, tamponImage);
|
||||||
|
|
||||||
|
return `/uploads/photos/${nomFichierUnique}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changement de mot de passe obligatoire (première connexion)
|
||||||
|
*/
|
||||||
|
async changePasswordRequired(
|
||||||
|
userId: string,
|
||||||
|
motDePasseActuel: string,
|
||||||
|
nouveauMotDePasse: string,
|
||||||
|
) {
|
||||||
|
const user = await this.usersRepo.findOne({ where: { id: userId } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('Utilisateur introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le changement est bien obligatoire
|
||||||
|
if (!user.changement_mdp_obligatoire) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Le changement de mot de passe n\'est pas requis pour cet utilisateur',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'utilisateur a un mot de passe
|
||||||
|
if (!user.password) {
|
||||||
|
throw new BadRequestException('Compte non activé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le mot de passe actuel
|
||||||
|
const motDePasseValide = await bcrypt.compare(motDePasseActuel, user.password);
|
||||||
|
if (!motDePasseValide) {
|
||||||
|
throw new BadRequestException('Mot de passe actuel incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que le nouveau mot de passe est différent de l'ancien
|
||||||
|
const memeMotDePasse = await bcrypt.compare(nouveauMotDePasse, user.password);
|
||||||
|
if (memeMotDePasse) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Le nouveau mot de passe doit être différent de l\'ancien',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasher et sauvegarder le nouveau mot de passe
|
||||||
|
const sel = await bcrypt.genSalt(12);
|
||||||
|
user.password = await bcrypt.hash(nouveauMotDePasse, sel);
|
||||||
|
user.changement_mdp_obligatoire = false;
|
||||||
|
user.modifie_le = new Date();
|
||||||
|
|
||||||
|
await this.usersRepo.save(user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Mot de passe changé avec succès',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async logout(userId: string) {
|
async logout(userId: string) {
|
||||||
// Pour le moment envoyer un message clair
|
|
||||||
return { success: true, message: 'Deconnexion'}
|
return { success: true, message: 'Deconnexion'}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsString, MinLength, Matches } from 'class-validator';
|
||||||
|
|
||||||
|
export class ChangePasswordRequiredDto {
|
||||||
|
@ApiProperty({ description: 'Mot de passe actuel' })
|
||||||
|
@IsString()
|
||||||
|
mot_de_passe_actuel: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Nouveau mot de passe (min 8 caractères, 1 majuscule, 1 chiffre)',
|
||||||
|
minLength: 8
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caractères' })
|
||||||
|
@Matches(/^(?=.*[A-Z])(?=.*\d)/, {
|
||||||
|
message: 'Le mot de passe doit contenir au moins une majuscule et un chiffre'
|
||||||
|
})
|
||||||
|
nouveau_mot_de_passe: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Confirmation du nouveau mot de passe' })
|
||||||
|
@IsString()
|
||||||
|
confirmation_mot_de_passe: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsEnum,
|
||||||
|
IsDateString,
|
||||||
|
IsBoolean,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { GenreType } from 'src/entities/children.entity';
|
||||||
|
|
||||||
|
export class EnfantInscriptionDto {
|
||||||
|
@ApiProperty({ example: 'Emma', required: false, description: 'Prénom de l\'enfant (obligatoire si déjà né)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
|
||||||
|
prenom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'MARTIN', required: false, description: 'Nom de l\'enfant (hérité des parents si non fourni)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
|
||||||
|
nom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2023-02-15', required: false, description: 'Date de naissance (si enfant déjà né)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_naissance?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2025-06-15', required: false, description: 'Date prévisionnelle de naissance (si enfant à naître)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_previsionnelle_naissance?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: GenreType, example: GenreType.F })
|
||||||
|
@IsEnum(GenreType, { message: 'Le genre doit être H, F ou Autre' })
|
||||||
|
@IsNotEmpty({ message: 'Le genre est requis' })
|
||||||
|
genre: GenreType;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
|
||||||
|
required: false,
|
||||||
|
description: 'Photo de l\'enfant en base64 (obligatoire si déjà né)'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_base64?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'emma_martin.jpg', required: false, description: 'Nom du fichier photo' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_filename?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
grossesse_multiple?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,4 +19,7 @@ export class ProfileResponseDto {
|
|||||||
|
|
||||||
@ApiProperty({ enum: StatutUtilisateurType })
|
@ApiProperty({ enum: StatutUtilisateurType })
|
||||||
statut: StatutUtilisateurType;
|
statut: StatutUtilisateurType;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Indique si le changement de mot de passe est obligatoire à la première connexion' })
|
||||||
|
changement_mdp_obligatoire: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
Matches,
|
||||||
|
IsDateString,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class RegisterAMCompletDto {
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 1 : IDENTITÉ (Obligatoire)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'marie.dupont@ptits-pas.fr' })
|
||||||
|
@IsEmail({}, { message: 'Email invalide' })
|
||||||
|
@IsNotEmpty({ message: "L'email est requis" })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Marie' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le prénom est requis' })
|
||||||
|
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100)
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'DUPONT' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le nom est requis' })
|
||||||
|
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100)
|
||||||
|
nom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '0689567890' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le téléphone est requis' })
|
||||||
|
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||||
|
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
|
||||||
|
})
|
||||||
|
telephone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
adresse?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '95870', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10)
|
||||||
|
code_postal?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Bezons', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(150)
|
||||||
|
ville?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 2 : PHOTO + INFOS PRO
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'data:image/jpeg;base64,/9j/4AAQ...',
|
||||||
|
required: false,
|
||||||
|
description: 'Photo de profil en base64',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_base64?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'photo_profil.jpg', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photo_filename?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Consentement utilisation photo' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: 'Le consentement photo est requis' })
|
||||||
|
consentement_photo: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2024-01-15', required: false, description: 'Date de naissance' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_naissance?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Paris', required: false, description: 'Ville de naissance' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
lieu_naissance_ville?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'France', required: false, description: 'Pays de naissance' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
lieu_naissance_pays?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '123456789012345', description: 'NIR 15 chiffres' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le NIR est requis' })
|
||||||
|
@Matches(/^\d{15}$/, { message: 'Le NIR doit contenir exactement 15 chiffres' })
|
||||||
|
nir: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'AGR-2024-12345', description: "Numéro d'agrément" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: "Le numéro d'agrément est requis" })
|
||||||
|
@MaxLength(50)
|
||||||
|
numero_agrement: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2024-06-01', required: false, description: "Date d'obtention de l'agrément" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
date_agrement?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 4, description: 'Capacité d\'accueil (nombre d\'enfants)', minimum: 1, maximum: 10 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(1, { message: 'La capacité doit être au moins 1' })
|
||||||
|
@Max(10, { message: 'La capacité ne peut pas dépasser 10' })
|
||||||
|
capacite_accueil: number;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 3 : PRÉSENTATION (Optionnel)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Assistante maternelle expérimentée, accueil bienveillant...',
|
||||||
|
required: false,
|
||||||
|
description: 'Présentation / biographie (max 2000 caractères)',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
|
||||||
|
biographie?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 4 : ACCEPTATION CGU (Obligatoire)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: "Acceptation des CGU" })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: "L'acceptation des CGU est requise" })
|
||||||
|
acceptation_cgu: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: "L'acceptation de la politique de confidentialité est requise" })
|
||||||
|
acceptation_privacy: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsBoolean,
|
||||||
|
IsArray,
|
||||||
|
ValidateNested,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
Matches,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { SituationFamilialeType } from 'src/entities/users.entity';
|
||||||
|
import { EnfantInscriptionDto } from './enfant-inscription.dto';
|
||||||
|
|
||||||
|
export class RegisterParentCompletDto {
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 1 : PARENT 1 (Obligatoire)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'claire.martin@ptits-pas.fr' })
|
||||||
|
@IsEmail({}, { message: 'Email invalide' })
|
||||||
|
@IsNotEmpty({ message: 'L\'email est requis' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Claire' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le prénom est requis' })
|
||||||
|
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'MARTIN' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le nom est requis' })
|
||||||
|
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
|
||||||
|
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
|
||||||
|
nom: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '0689567890' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: 'Le téléphone est requis' })
|
||||||
|
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||||
|
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
|
||||||
|
})
|
||||||
|
telephone: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
adresse?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '95870', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10)
|
||||||
|
code_postal?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Bezons', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(150)
|
||||||
|
ville?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 2 : PARENT 2 / CO-PARENT (Optionnel)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail({}, { message: 'Email du co-parent invalide' })
|
||||||
|
co_parent_email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Thomas', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_prenom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'MARTIN', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_nom?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '0678456789', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
|
||||||
|
message: 'Le numéro de téléphone du co-parent doit être valide',
|
||||||
|
})
|
||||||
|
co_parent_telephone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Le co-parent habite à la même adresse', required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
co_parent_meme_adresse?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_adresse?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_code_postal?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
co_parent_ville?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 3 : ENFANT(S) (Au moins 1 requis)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: [EnfantInscriptionDto],
|
||||||
|
description: 'Liste des enfants (au moins 1 requis)',
|
||||||
|
example: [{
|
||||||
|
prenom: 'Emma',
|
||||||
|
nom: 'MARTIN',
|
||||||
|
date_naissance: '2023-02-15',
|
||||||
|
genre: 'F',
|
||||||
|
photo_base64: 'data:image/jpeg;base64,...',
|
||||||
|
photo_filename: 'emma_martin.jpg'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
@IsArray({ message: 'La liste des enfants doit être un tableau' })
|
||||||
|
@IsNotEmpty({ message: 'Au moins un enfant est requis' })
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => EnfantInscriptionDto)
|
||||||
|
enfants: EnfantInscriptionDto[];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 4 : PRÉSENTATION DU DOSSIER (Optionnel)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'Nous recherchons une assistante maternelle bienveillante pour nos triplés...',
|
||||||
|
required: false,
|
||||||
|
description: 'Présentation du dossier (max 2000 caractères)'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
|
||||||
|
presentation_dossier?: string;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ÉTAPE 5 : ACCEPTATION CGU (Obligatoire)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Acceptation des Conditions Générales d\'Utilisation' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: 'L\'acceptation des CGU est requise' })
|
||||||
|
acceptation_cgu: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty({ message: 'L\'acceptation de la politique de confidentialité est requise' })
|
||||||
|
acceptation_privacy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ export class CreateEnfantsDto {
|
|||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
last_name?: string;
|
last_name?: string;
|
||||||
|
|
||||||
@ApiProperty({ enum: GenreType, required: false })
|
@ApiProperty({ enum: GenreType })
|
||||||
@IsOptional()
|
|
||||||
@IsEnum(GenreType)
|
@IsEnum(GenreType)
|
||||||
gender?: GenreType;
|
@IsNotEmpty()
|
||||||
|
gender: GenreType;
|
||||||
|
|
||||||
@ApiProperty({ example: '2018-06-24', required: false })
|
@ApiProperty({ example: '2018-06-24', required: false })
|
||||||
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)
|
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger';
|
||||||
|
import { diskStorage } from 'multer';
|
||||||
|
import { extname } from 'path';
|
||||||
import { EnfantsService } from './enfants.service';
|
import { EnfantsService } from './enfants.service';
|
||||||
import { CreateEnfantsDto } from './dto/create_enfants.dto';
|
import { CreateEnfantsDto } from './dto/create_enfants.dto';
|
||||||
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
|
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
|
||||||
@@ -28,8 +33,34 @@ export class EnfantsController {
|
|||||||
|
|
||||||
@Roles(RoleType.PARENT)
|
@Roles(RoleType.PARENT)
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) {
|
@ApiConsumes('multipart/form-data')
|
||||||
return this.enfantsService.create(dto, currentUser);
|
@UseInterceptors(
|
||||||
|
FileInterceptor('photo', {
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: './uploads/photos',
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
|
const ext = extname(file.originalname);
|
||||||
|
cb(null, `enfant-${uniqueSuffix}${ext}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
|
||||||
|
return cb(new Error('Seules les images sont autorisées'), false);
|
||||||
|
}
|
||||||
|
cb(null, true);
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
fileSize: 5 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
create(
|
||||||
|
@Body() dto: CreateEnfantsDto,
|
||||||
|
@UploadedFile() photo: Express.Multer.File,
|
||||||
|
@User() currentUser: Users,
|
||||||
|
) {
|
||||||
|
return this.enfantsService.create(dto, currentUser, photo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
|
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ export class EnfantsService {
|
|||||||
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// Création d’un enfant
|
// Création d'un enfant
|
||||||
async create(dto: CreateEnfantsDto, currentUser: Users): Promise<Children> {
|
async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise<Children> {
|
||||||
const parent = await this.parentsRepository.findOne({
|
const parent = await this.parentsRepository.findOne({
|
||||||
where: { user_id: currentUser.id },
|
where: { user_id: currentUser.id },
|
||||||
|
relations: ['co_parent'],
|
||||||
});
|
});
|
||||||
if (!parent) throw new NotFoundException('Parent introuvable');
|
if (!parent) throw new NotFoundException('Parent introuvable');
|
||||||
|
|
||||||
@@ -46,17 +47,34 @@ export class EnfantsService {
|
|||||||
});
|
});
|
||||||
if (exist) throw new ConflictException('Cet enfant existe déjà');
|
if (exist) throw new ConflictException('Cet enfant existe déjà');
|
||||||
|
|
||||||
|
// Gestion de la photo uploadée
|
||||||
|
if (photoFile) {
|
||||||
|
dto.photo_url = `/uploads/photos/${photoFile.filename}`;
|
||||||
|
if (dto.consent_photo) {
|
||||||
|
dto.consent_photo_at = new Date().toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Création
|
// Création
|
||||||
const child = this.childrenRepository.create(dto);
|
const child = this.childrenRepository.create(dto);
|
||||||
await this.childrenRepository.save(child);
|
await this.childrenRepository.save(child);
|
||||||
|
|
||||||
// Lien parent-enfant
|
// Lien parent-enfant (Parent 1)
|
||||||
const parentLink = this.parentsChildrenRepository.create({
|
const parentLink = this.parentsChildrenRepository.create({
|
||||||
parentId: parent.user_id,
|
parentId: parent.user_id,
|
||||||
enfantId: child.id,
|
enfantId: child.id,
|
||||||
});
|
});
|
||||||
await this.parentsChildrenRepository.save(parentLink);
|
await this.parentsChildrenRepository.save(parentLink);
|
||||||
|
|
||||||
|
// Rattachement automatique au co-parent s'il existe
|
||||||
|
if (parent.co_parent) {
|
||||||
|
const coParentLink = this.parentsChildrenRepository.create({
|
||||||
|
parentId: parent.co_parent.id,
|
||||||
|
enfantId: child.id,
|
||||||
|
});
|
||||||
|
await this.parentsChildrenRepository.save(coParentLink);
|
||||||
|
}
|
||||||
|
|
||||||
return this.findOne(child.id, currentUser);
|
return this.findOne(child.id, currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { UpdateParentsDto } from '../user/dto/update_parent.dto';
|
|||||||
export class ParentsController {
|
export class ParentsController {
|
||||||
constructor(private readonly parentsService: ParentsService) {}
|
constructor(private readonly parentsService: ParentsService) {}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@Get()
|
@Get()
|
||||||
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class GestionnairesController {
|
|||||||
return this.gestionnairesService.create(dto);
|
return this.gestionnairesService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Liste des gestionnaires' })
|
@ApiOperation({ summary: 'Liste des gestionnaires' })
|
||||||
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
|
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
|
||||||
@Get()
|
@Get()
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { GestionnairesService } from './gestionnaires.service';
|
|||||||
import { GestionnairesController } from './gestionnaires.controller';
|
import { GestionnairesController } from './gestionnaires.controller';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users } from 'src/entities/users.entity';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Users])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Users]),
|
||||||
|
AuthModule,
|
||||||
|
],
|
||||||
controllers: [GestionnairesController],
|
controllers: [GestionnairesController],
|
||||||
providers: [GestionnairesService],
|
providers: [GestionnairesService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class UserController {
|
|||||||
|
|
||||||
// Lister tous les utilisateurs (super_admin uniquement)
|
// Lister tous les utilisateurs (super_admin uniquement)
|
||||||
@Get()
|
@Get()
|
||||||
@Roles(RoleType.SUPER_ADMIN)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
|
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.userService.findAll();
|
return this.userService.findAll();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ParentsModule } from '../parents/parents.module';
|
|||||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
|
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature(
|
imports: [TypeOrmModule.forFeature(
|
||||||
@@ -20,6 +21,7 @@ import { Parents } from 'src/entities/parents.entity';
|
|||||||
]), forwardRef(() => AuthModule),
|
]), forwardRef(() => AuthModule),
|
||||||
ParentsModule,
|
ParentsModule,
|
||||||
AssistantesMaternellesModule,
|
AssistantesMaternellesModule,
|
||||||
|
GestionnairesModule,
|
||||||
],
|
],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService],
|
providers: [UserService],
|
||||||
|
|||||||
@@ -46,19 +46,20 @@ CREATE TABLE utilisateurs (
|
|||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
||||||
password TEXT NOT NULL,
|
password TEXT, -- NULL avant création via token
|
||||||
prenom VARCHAR(100),
|
prenom VARCHAR(100),
|
||||||
nom VARCHAR(100),
|
nom VARCHAR(100),
|
||||||
genre genre_type,
|
genre genre_type,
|
||||||
role role_type NOT NULL,
|
role role_type NOT NULL,
|
||||||
statut statut_utilisateur_type DEFAULT 'en_attente',
|
statut statut_utilisateur_type DEFAULT 'en_attente',
|
||||||
mobile VARCHAR(20),
|
telephone VARCHAR(20), -- Unifié (mobile privilégié)
|
||||||
telephone_fixe VARCHAR(20),
|
|
||||||
adresse TEXT,
|
adresse TEXT,
|
||||||
date_naissance DATE,
|
date_naissance DATE,
|
||||||
photo_url TEXT,
|
photo_url TEXT, -- Obligatoire pour AM, non utilisé pour parents
|
||||||
consentement_photo BOOLEAN DEFAULT false,
|
consentement_photo BOOLEAN DEFAULT false,
|
||||||
date_consentement_photo TIMESTAMPTZ,
|
date_consentement_photo TIMESTAMPTZ,
|
||||||
|
token_creation_mdp VARCHAR(255), -- Token pour créer MDP après validation
|
||||||
|
token_creation_mdp_expire_le TIMESTAMPTZ, -- Expiration 7 jours
|
||||||
changement_mdp_obligatoire BOOLEAN DEFAULT false,
|
changement_mdp_obligatoire BOOLEAN DEFAULT false,
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
cree_le TIMESTAMPTZ DEFAULT now(),
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now(),
|
modifie_le TIMESTAMPTZ DEFAULT now(),
|
||||||
@@ -68,20 +69,26 @@ CREATE TABLE utilisateurs (
|
|||||||
situation_familiale situation_familiale_type
|
situation_familiale situation_familiale_type
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Index pour recherche par token
|
||||||
|
CREATE INDEX idx_utilisateurs_token_creation_mdp
|
||||||
|
ON utilisateurs(token_creation_mdp)
|
||||||
|
WHERE token_creation_mdp IS NOT NULL;
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
-- Table : assistantes_maternelles
|
-- Table : assistantes_maternelles
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
CREATE TABLE assistantes_maternelles (
|
CREATE TABLE assistantes_maternelles (
|
||||||
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
||||||
numero_agrement VARCHAR(50),
|
numero_agrement VARCHAR(50),
|
||||||
date_agrement DATE,
|
|
||||||
nir_chiffre CHAR(15),
|
nir_chiffre CHAR(15),
|
||||||
annee_experience SMALLINT,
|
|
||||||
specialite VARCHAR (100),
|
|
||||||
nb_max_enfants INT,
|
nb_max_enfants INT,
|
||||||
place_disponible INT,
|
|
||||||
biographie TEXT,
|
biographie TEXT,
|
||||||
disponible BOOLEAN DEFAULT true
|
disponible BOOLEAN DEFAULT true,
|
||||||
|
ville_residence VARCHAR(100),
|
||||||
|
date_agrement DATE,
|
||||||
|
annee_experience SMALLINT,
|
||||||
|
specialite VARCHAR(100),
|
||||||
|
place_disponible INT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ==========================================================
|
-- ==========================================================
|
||||||
@@ -100,7 +107,7 @@ CREATE TABLE enfants (
|
|||||||
statut statut_enfant_type,
|
statut statut_enfant_type,
|
||||||
prenom VARCHAR(100),
|
prenom VARCHAR(100),
|
||||||
nom VARCHAR(100),
|
nom VARCHAR(100),
|
||||||
genre genre_type,
|
genre genre_type NOT NULL, -- Obligatoire selon CDC
|
||||||
date_naissance DATE,
|
date_naissance DATE,
|
||||||
date_prevue_naissance DATE,
|
date_prevue_naissance DATE,
|
||||||
photo_url TEXT,
|
photo_url TEXT,
|
||||||
@@ -241,3 +248,125 @@ CREATE TABLE validations (
|
|||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
cree_le TIMESTAMPTZ DEFAULT now(),
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
modifie_le TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Table : configuration
|
||||||
|
-- ==========================================================
|
||||||
|
CREATE TABLE configuration (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
cle VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
valeur TEXT,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
categorie VARCHAR(50),
|
||||||
|
description TEXT,
|
||||||
|
modifie_le TIMESTAMPTZ DEFAULT now(),
|
||||||
|
modifie_par UUID REFERENCES utilisateurs(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour performance
|
||||||
|
CREATE INDEX idx_configuration_cle ON configuration(cle);
|
||||||
|
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
|
||||||
|
|
||||||
|
-- Seed initial de configuration
|
||||||
|
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
|
||||||
|
-- === Configuration Email (SMTP) ===
|
||||||
|
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
|
||||||
|
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
|
||||||
|
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
|
||||||
|
('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'),
|
||||||
|
('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'),
|
||||||
|
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
|
||||||
|
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
|
||||||
|
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
|
||||||
|
|
||||||
|
-- === Configuration Application ===
|
||||||
|
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
|
||||||
|
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
|
||||||
|
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
|
||||||
|
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
|
||||||
|
|
||||||
|
-- === Configuration Sécurité ===
|
||||||
|
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
|
||||||
|
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
|
||||||
|
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
|
||||||
|
('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe');
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Table : documents_legaux
|
||||||
|
-- ==========================================================
|
||||||
|
CREATE TABLE documents_legaux (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
|
||||||
|
version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté)
|
||||||
|
fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier
|
||||||
|
fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage
|
||||||
|
fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité
|
||||||
|
actif BOOLEAN DEFAULT false, -- Version actuellement active
|
||||||
|
televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé
|
||||||
|
televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload
|
||||||
|
active_le TIMESTAMPTZ, -- Date d'activation
|
||||||
|
UNIQUE(type, version) -- Pas de doublon version
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour performance
|
||||||
|
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
|
||||||
|
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Table : acceptations_documents
|
||||||
|
-- ==========================================================
|
||||||
|
CREATE TABLE acceptations_documents (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
||||||
|
id_document UUID REFERENCES documents_legaux(id),
|
||||||
|
type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
|
||||||
|
version_document INTEGER NOT NULL, -- Version acceptée
|
||||||
|
accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation
|
||||||
|
ip_address INET, -- IP de l'utilisateur (RGPD)
|
||||||
|
user_agent TEXT -- Navigateur (preuve)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour performance
|
||||||
|
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
|
||||||
|
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Modification Table : utilisateurs (ajout colonnes documents)
|
||||||
|
-- ==========================================================
|
||||||
|
ALTER TABLE utilisateurs
|
||||||
|
ADD COLUMN IF NOT EXISTS cgu_version_acceptee INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS cgu_acceptee_le TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS privacy_version_acceptee INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Seed : Documents légaux génériques v1
|
||||||
|
-- ==========================================================
|
||||||
|
INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES
|
||||||
|
('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()),
|
||||||
|
('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now());
|
||||||
|
|
||||||
|
-- ==========================================================
|
||||||
|
-- Seed : Super Administrateur par défaut
|
||||||
|
-- ==========================================================
|
||||||
|
-- Email: admin@ptits-pas.fr
|
||||||
|
-- Mot de passe: 4dm1n1strateur (hashé bcrypt)
|
||||||
|
-- IMPORTANT: Changer ce mot de passe en production !
|
||||||
|
-- ==========================================================
|
||||||
|
INSERT INTO utilisateurs (
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
prenom,
|
||||||
|
nom,
|
||||||
|
role,
|
||||||
|
statut,
|
||||||
|
changement_mdp_obligatoire
|
||||||
|
) VALUES (
|
||||||
|
'admin@ptits-pas.fr',
|
||||||
|
'$2b$12$plOZCW7lzLFkWgDPcE6p6u10EA4yErQt6Xcp5nyH3Sp/2.6EpNW.6',
|
||||||
|
'Super',
|
||||||
|
'Administrateur',
|
||||||
|
'super_admin',
|
||||||
|
'actif',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ docker compose -f docker-compose.dev.yml down -v
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Réinitialiser la BDD et charger les données de test (dashboard admin)
|
||||||
|
|
||||||
|
Depuis la **racine du projet** (ptitspas-app, où se trouve `docker-compose.yml`) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/reset-and-seed-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce script : arrête les conteneurs, supprime le volume Postgres, redémarre la base (le schéma est recréé via `BDD.sql`), puis exécute `database/seed/03_seed_test_data.sql`. Tu obtiens un super_admin (`admin@ptits-pas.fr`) plus 9 comptes de test (1 admin, 1 gestionnaire, 2 AM, 5 parents) avec **mot de passe : `password`**. Idéal pour développer le ticket #92 (dashboard admin).
|
||||||
|
|
||||||
## Importation automatique des données de test
|
## Importation automatique des données de test
|
||||||
|
|
||||||
Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`.
|
Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`.
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- ENUMS
|
|
||||||
-- ==========================================================
|
|
||||||
DO $$ BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
|
|
||||||
CREATE TYPE role_type AS ENUM ('parent', 'gestionnaire', 'super_admin', 'assistante_maternelle','administrateur');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN
|
|
||||||
CREATE TYPE genre_type AS ENUM ('H', 'F', 'Autre');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN
|
|
||||||
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN
|
|
||||||
CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN
|
|
||||||
CREATE TYPE statut_dossier_type AS ENUM ('envoye','accepte','refuse');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN
|
|
||||||
CREATE TYPE statut_contrat_type AS ENUM ('brouillon','en_attente_signature','valide','resilie');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN
|
|
||||||
CREATE TYPE statut_avenant_type AS ENUM ('propose','accepte','refuse');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN
|
|
||||||
CREATE TYPE type_evenement_type AS ENUM ('absence_enfant','conge_am','conge_parent','arret_maladie_am','evenement_rpe');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN
|
|
||||||
CREATE TYPE statut_evenement_type AS ENUM ('propose','valide','refuse');
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN
|
|
||||||
CREATE TYPE statut_validation_type AS ENUM ('en_attente','valide','refuse');
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : utilisateurs
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE utilisateurs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
email VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
prenom VARCHAR(100),
|
|
||||||
nom VARCHAR(100),
|
|
||||||
genre genre_type,
|
|
||||||
role role_type NOT NULL,
|
|
||||||
statut statut_utilisateur_type DEFAULT 'en_attente',
|
|
||||||
telephone VARCHAR(20),
|
|
||||||
adresse TEXT,
|
|
||||||
photo_url TEXT,
|
|
||||||
consentement_photo BOOLEAN DEFAULT false,
|
|
||||||
date_consentement_photo TIMESTAMPTZ,
|
|
||||||
changement_mdp_obligatoire BOOLEAN DEFAULT false,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
ville VARCHAR(150),
|
|
||||||
code_postal VARCHAR(10),
|
|
||||||
mobile VARCHAR(20),
|
|
||||||
telephone_fixe VARCHAR(20),
|
|
||||||
profession VARCHAR(150),
|
|
||||||
situation_familiale VARCHAR(50),
|
|
||||||
date_naissance DATE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : assistantes_maternelles
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE assistantes_maternelles (
|
|
||||||
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
|
||||||
numero_agrement VARCHAR(50),
|
|
||||||
nir_chiffre CHAR(15),
|
|
||||||
nb_max_enfants INT,
|
|
||||||
biographie TEXT,
|
|
||||||
disponible BOOLEAN DEFAULT true,
|
|
||||||
ville_residence VARCHAR(100),
|
|
||||||
date_agrement DATE,
|
|
||||||
annee_experience SMALLINT,
|
|
||||||
specialite VARCHAR(100),
|
|
||||||
place_disponible INT
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : parentschange les donnée de init
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE parents (
|
|
||||||
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
|
||||||
id_co_parent UUID REFERENCES utilisateurs(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : enfants
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE enfants (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
statut statut_enfant_type,
|
|
||||||
prenom VARCHAR(100),
|
|
||||||
nom VARCHAR(100),
|
|
||||||
genre genre_type,
|
|
||||||
date_naissance DATE,
|
|
||||||
date_prevue_naissance DATE,
|
|
||||||
photo_url TEXT,
|
|
||||||
consentement_photo BOOLEAN DEFAULT false,
|
|
||||||
date_consentement_photo TIMESTAMPTZ,
|
|
||||||
est_multiple BOOLEAN DEFAULT false
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : enfants_parents
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE enfants_parents (
|
|
||||||
id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
|
|
||||||
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (id_parent, id_enfant)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : dossiers
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE dossiers (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
|
|
||||||
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
|
|
||||||
presentation TEXT,
|
|
||||||
type_contrat VARCHAR(50),
|
|
||||||
repas BOOLEAN DEFAULT false,
|
|
||||||
budget NUMERIC(10,2),
|
|
||||||
planning_souhaite JSONB,
|
|
||||||
statut statut_dossier_type DEFAULT 'envoye',
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : messages
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE messages (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_dossier UUID REFERENCES dossiers(id) ON DELETE CASCADE,
|
|
||||||
id_expediteur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
|
||||||
contenu TEXT,
|
|
||||||
re_redige_par_ia BOOLEAN DEFAULT false,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : contrats
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE contrats (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_dossier UUID UNIQUE REFERENCES dossiers(id) ON DELETE CASCADE,
|
|
||||||
planning JSONB,
|
|
||||||
tarif_horaire NUMERIC(6,2),
|
|
||||||
indemnites_repas NUMERIC(6,2),
|
|
||||||
date_debut DATE,
|
|
||||||
statut statut_contrat_type DEFAULT 'brouillon',
|
|
||||||
signe_parent BOOLEAN DEFAULT false,
|
|
||||||
signe_am BOOLEAN DEFAULT false,
|
|
||||||
finalise_le TIMESTAMPTZ,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : avenants_contrats
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE avenants_contrats (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_contrat UUID REFERENCES contrats(id) ON DELETE CASCADE,
|
|
||||||
modifications JSONB,
|
|
||||||
initie_par UUID REFERENCES utilisateurs(id),
|
|
||||||
statut statut_avenant_type DEFAULT 'propose',
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : evenements
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE evenements (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
type type_evenement_type,
|
|
||||||
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
|
|
||||||
id_am UUID REFERENCES utilisateurs(id),
|
|
||||||
id_parent UUID REFERENCES parents(id_utilisateur),
|
|
||||||
cree_par UUID REFERENCES utilisateurs(id),
|
|
||||||
date_debut TIMESTAMPTZ,
|
|
||||||
date_fin TIMESTAMPTZ,
|
|
||||||
commentaires TEXT,
|
|
||||||
statut statut_evenement_type DEFAULT 'propose',
|
|
||||||
delai_grace TIMESTAMPTZ,
|
|
||||||
urgent BOOLEAN DEFAULT false,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : signalements_bugs
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE signalements_bugs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_utilisateur UUID REFERENCES utilisateurs(id),
|
|
||||||
description TEXT,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : uploads
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE uploads (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
|
|
||||||
fichier_url TEXT NOT NULL,
|
|
||||||
type VARCHAR(50),
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : notifications
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE notifications (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
|
||||||
contenu TEXT,
|
|
||||||
lu BOOLEAN DEFAULT false,
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Table : validations
|
|
||||||
-- ==========================================================
|
|
||||||
CREATE TABLE validations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
id_utilisateur UUID REFERENCES utilisateurs(id),
|
|
||||||
type VARCHAR(50),
|
|
||||||
statut statut_validation_type DEFAULT 'en_attente',
|
|
||||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
modifie_le TIMESTAMPTZ DEFAULT now(),
|
|
||||||
valide_par UUID REFERENCES utilisateurs(id),
|
|
||||||
commentaire TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- Initialisation d'un administrateur par défaut
|
|
||||||
-- ==========================================================
|
|
||||||
|
|
||||||
-- ==========================================================
|
|
||||||
-- SEED: Super Administrateur par défaut
|
|
||||||
-- ==========================================================
|
|
||||||
-- Mot de passe: 4dm1n1strateur
|
|
||||||
INSERT INTO utilisateurs (
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
prenom,
|
|
||||||
nom,
|
|
||||||
role,
|
|
||||||
statut,
|
|
||||||
cree_le,
|
|
||||||
modifie_le
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
gen_random_uuid(),
|
|
||||||
'admin@ptits-pas.fr',
|
|
||||||
'$2b$12$Fo5ly1YlTj3O6lXf.IUgoeUqEebBGpmoM5zLbzZx.CueorSE7z2E2',
|
|
||||||
'Admin',
|
|
||||||
'Système',
|
|
||||||
'super_admin',
|
|
||||||
'actif',
|
|
||||||
now(),
|
|
||||||
now()
|
|
||||||
)
|
|
||||||
ON CONFLICT (email) DO NOTHING;
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- 02_indexes.sql : Index FK + colonnes critiques
|
|
||||||
-- =============================================
|
|
||||||
|
|
||||||
-- Recommandation : exécuter après 01_init.sql
|
|
||||||
|
|
||||||
-- ===========
|
|
||||||
-- UTILISATEURS
|
|
||||||
-- ===========
|
|
||||||
-- Recherche par email (insensibilité à la casse pour lookup)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_lower_courriel
|
|
||||||
ON utilisateurs (LOWER(courriel));
|
|
||||||
|
|
||||||
-- ===========
|
|
||||||
-- ASSISTANTES_MATERNELLES
|
|
||||||
-- ===========
|
|
||||||
-- FK -> utilisateurs(id)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_am_id_utilisateur
|
|
||||||
ON assistantes_maternelles (id_utilisateur);
|
|
||||||
|
|
||||||
-- =======
|
|
||||||
-- PARENTS
|
|
||||||
-- =======
|
|
||||||
-- FK -> utilisateurs(id)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_parents_id_utilisateur
|
|
||||||
ON parents (id_utilisateur);
|
|
||||||
|
|
||||||
-- Co-parent (nullable)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_parents_id_co_parent
|
|
||||||
ON parents (id_co_parent);
|
|
||||||
|
|
||||||
-- =======
|
|
||||||
-- ENFANTS
|
|
||||||
-- =======
|
|
||||||
-- (souvent filtrés par statut / date_naissance ? à activer si besoin)
|
|
||||||
-- CREATE INDEX IF NOT EXISTS idx_enfants_statut ON enfants (statut);
|
|
||||||
-- CREATE INDEX IF NOT EXISTS idx_enfants_date_naissance ON enfants (date_naissance);
|
|
||||||
|
|
||||||
-- ================
|
|
||||||
-- ENFANTS_PARENTS (N:N)
|
|
||||||
-- ================
|
|
||||||
-- PK composite déjà en place (id_parent, id_enfant), ajouter index individuels si jointures unilatérales fréquentes
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_enfants_parents_id_parent
|
|
||||||
ON enfants_parents (id_parent);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_enfants_parents_id_enfant
|
|
||||||
ON enfants_parents (id_enfant);
|
|
||||||
|
|
||||||
-- ========
|
|
||||||
-- DOSSIERS
|
|
||||||
-- ========
|
|
||||||
-- FK -> parent / enfant
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_dossiers_id_parent
|
|
||||||
ON dossiers (id_parent);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_dossiers_id_enfant
|
|
||||||
ON dossiers (id_enfant);
|
|
||||||
|
|
||||||
-- Statut (filtrages "à traiter", "envoyé", etc.)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_dossiers_statut
|
|
||||||
ON dossiers (statut);
|
|
||||||
|
|
||||||
-- JSONB : si on fait des requêtes @> sur le planning souhaité
|
|
||||||
-- CREATE INDEX IF NOT EXISTS idx_dossiers_planning_souhaite_gin
|
|
||||||
-- ON dossiers USING GIN (planning_souhaite);
|
|
||||||
|
|
||||||
-- ========
|
|
||||||
-- MESSAGES
|
|
||||||
-- ========
|
|
||||||
-- Filtrage par dossier + tri chronologique
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_id_dossier_cree_le
|
|
||||||
ON messages (id_dossier, cree_le);
|
|
||||||
|
|
||||||
-- Recherche par expéditeur
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_id_expediteur_cree_le
|
|
||||||
ON messages (id_expediteur, cree_le);
|
|
||||||
|
|
||||||
-- =========
|
|
||||||
-- CONTRATS
|
|
||||||
-- =========
|
|
||||||
-- UNIQUE(id_dossier) existe déjà -> index implicite
|
|
||||||
-- Tri / filtres fréquents
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_contrats_statut
|
|
||||||
ON contrats (statut);
|
|
||||||
|
|
||||||
-- JSONB planning : activer si on requête par clé
|
|
||||||
-- CREATE INDEX IF NOT EXISTS idx_contrats_planning_gin
|
|
||||||
-- ON contrats USING GIN (planning);
|
|
||||||
|
|
||||||
-- ==================
|
|
||||||
-- AVENANTS_CONTRATS
|
|
||||||
-- ==================
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_avenants_contrats_id_contrat_cree_le
|
|
||||||
ON avenants_contrats (id_contrat, cree_le);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_avenants_contrats_initie_par
|
|
||||||
ON avenants_contrats (initie_par);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_avenants_contrats_statut
|
|
||||||
ON avenants_contrats (statut);
|
|
||||||
|
|
||||||
-- =========
|
|
||||||
-- EVENEMENTS
|
|
||||||
-- =========
|
|
||||||
-- Accès par enfant + période
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evenements_id_enfant_date_debut
|
|
||||||
ON evenements (id_enfant, date_debut);
|
|
||||||
|
|
||||||
-- Filtrage par auteur / AM / parent
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evenements_cree_par
|
|
||||||
ON evenements (cree_par);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evenements_id_am
|
|
||||||
ON evenements (id_am);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evenements_id_parent
|
|
||||||
ON evenements (id_parent);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evenements_type
|
|
||||||
ON evenements (type);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_evenements_statut
|
|
||||||
ON evenements (statut);
|
|
||||||
|
|
||||||
-- =================
|
|
||||||
-- SIGNALEMENTS_BUGS
|
|
||||||
-- =================
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_signalements_bugs_id_utilisateur_cree_le
|
|
||||||
ON signalements_bugs (id_utilisateur, cree_le);
|
|
||||||
|
|
||||||
-- =======
|
|
||||||
-- UPLOADS
|
|
||||||
-- =======
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_uploads_id_utilisateur_cree_le
|
|
||||||
ON uploads (id_utilisateur, cree_le);
|
|
||||||
|
|
||||||
-- =============
|
|
||||||
-- NOTIFICATIONS
|
|
||||||
-- =============
|
|
||||||
-- Requêtes fréquentes : non lues + ordre chrono
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_lu_cree_le
|
|
||||||
ON notifications (id_utilisateur, lu, cree_le);
|
|
||||||
|
|
||||||
-- Option : index partiel pour "non lues"
|
|
||||||
-- CREATE INDEX IF NOT EXISTS idx_notifications_non_lues
|
|
||||||
-- ON notifications (id_utilisateur, cree_le)
|
|
||||||
-- WHERE lu = false;
|
|
||||||
|
|
||||||
-- ===========
|
|
||||||
-- VALIDATIONS
|
|
||||||
-- ===========
|
|
||||||
-- Requêtes par utilisateur validé, par statut et par date
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_validations_id_utilisateur_cree_le
|
|
||||||
ON validations (id_utilisateur, cree_le);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_validations_statut
|
|
||||||
ON validations (statut);
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- 03_checks.sql : Contraintes CHECK & NOT NULL
|
|
||||||
-- A exécuter après 01_init.sql (et 02_indexes.sql)
|
|
||||||
-- =============================================
|
|
||||||
|
|
||||||
-- ==============
|
|
||||||
-- UTILISATEURS
|
|
||||||
-- ==============
|
|
||||||
-- (Regex email déjà présente dans 01_init.sql)
|
|
||||||
-- Optionnel : forcer prenom/nom non vides si fournis
|
|
||||||
ALTER TABLE utilisateurs
|
|
||||||
ADD CONSTRAINT chk_utilisateurs_prenom_non_vide
|
|
||||||
CHECK (prenom IS NULL OR btrim(prenom) <> ''),
|
|
||||||
ADD CONSTRAINT chk_utilisateurs_nom_non_vide
|
|
||||||
CHECK (nom IS NULL OR btrim(nom) <> '');
|
|
||||||
|
|
||||||
-- =========================
|
|
||||||
-- ASSISTANTES_MATERNELLES
|
|
||||||
-- =========================
|
|
||||||
-- NIR : aujourd’hui en 15 chiffres (Sprint 2 : chiffrement)
|
|
||||||
ALTER TABLE assistantes_maternelles
|
|
||||||
ADD CONSTRAINT chk_am_nir_format
|
|
||||||
CHECK (nir_chiffre IS NULL OR nir_chiffre ~ '^[0-9]{15}$'),
|
|
||||||
ADD CONSTRAINT chk_am_nb_max_enfants
|
|
||||||
CHECK (nb_max_enfants IS NULL OR nb_max_enfants BETWEEN 0 AND 10),
|
|
||||||
ADD CONSTRAINT chk_am_ville_non_vide
|
|
||||||
CHECK (ville_residence IS NULL OR btrim(ville_residence) <> '');
|
|
||||||
|
|
||||||
-- =========
|
|
||||||
-- PARENTS
|
|
||||||
-- =========
|
|
||||||
-- Interdiction d’être co-parent de soi-même
|
|
||||||
ALTER TABLE parents
|
|
||||||
ADD CONSTRAINT chk_parents_co_parent_diff
|
|
||||||
CHECK (id_co_parent IS NULL OR id_co_parent <> id_utilisateur);
|
|
||||||
|
|
||||||
-- =========
|
|
||||||
-- ENFANTS
|
|
||||||
-- =========
|
|
||||||
-- Cohérence statut / dates de naissance
|
|
||||||
ALTER TABLE enfants
|
|
||||||
ADD CONSTRAINT chk_enfants_dates_exclusives
|
|
||||||
CHECK (NOT (date_naissance IS NOT NULL AND date_prevue_naissance IS NOT NULL)),
|
|
||||||
ADD CONSTRAINT chk_enfants_statut_dates
|
|
||||||
CHECK (
|
|
||||||
-- a_naitre => date_prevue_naissance requise
|
|
||||||
(statut = 'a_naitre' AND date_prevue_naissance IS NOT NULL)
|
|
||||||
OR
|
|
||||||
-- actif/scolarise => date_naissance requise
|
|
||||||
(statut IN ('actif','scolarise') AND date_naissance IS NOT NULL)
|
|
||||||
OR statut IS NULL -- si statut non encore fixé
|
|
||||||
),
|
|
||||||
ADD CONSTRAINT chk_enfants_consentement_coherent
|
|
||||||
CHECK (
|
|
||||||
(consentement_photo = true AND date_consentement_photo IS NOT NULL)
|
|
||||||
OR
|
|
||||||
(consentement_photo = false AND date_consentement_photo IS NULL)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- =================
|
|
||||||
-- ENFANTS_PARENTS
|
|
||||||
-- =================
|
|
||||||
-- (PK composite déjà en place, rien à ajouter ici)
|
|
||||||
|
|
||||||
-- ========
|
|
||||||
-- DOSSIERS
|
|
||||||
-- ========
|
|
||||||
ALTER TABLE dossiers
|
|
||||||
ADD CONSTRAINT chk_dossiers_budget_nonneg
|
|
||||||
CHECK (budget IS NULL OR budget >= 0),
|
|
||||||
ADD CONSTRAINT chk_dossiers_type_contrat_non_vide
|
|
||||||
CHECK (type_contrat IS NULL OR btrim(type_contrat) <> ''),
|
|
||||||
ADD CONSTRAINT chk_dossiers_planning_json
|
|
||||||
CHECK (planning_souhaite IS NULL OR jsonb_typeof(planning_souhaite) = 'object');
|
|
||||||
|
|
||||||
-- ========
|
|
||||||
-- MESSAGES
|
|
||||||
-- ========
|
|
||||||
-- Contenu obligatoire, non vide
|
|
||||||
ALTER TABLE messages
|
|
||||||
ALTER COLUMN contenu SET NOT NULL;
|
|
||||||
ALTER TABLE messages
|
|
||||||
ADD CONSTRAINT chk_messages_contenu_non_vide
|
|
||||||
CHECK (btrim(contenu) <> '');
|
|
||||||
|
|
||||||
-- =========
|
|
||||||
-- CONTRATS
|
|
||||||
-- =========
|
|
||||||
ALTER TABLE contrats
|
|
||||||
ADD CONSTRAINT chk_contrats_tarif_nonneg
|
|
||||||
CHECK (tarif_horaire IS NULL OR tarif_horaire >= 0),
|
|
||||||
ADD CONSTRAINT chk_contrats_indemnites_nonneg
|
|
||||||
CHECK (indemnites_repas IS NULL OR indemnites_repas >= 0);
|
|
||||||
|
|
||||||
-- ==================
|
|
||||||
-- AVENANTS_CONTRATS
|
|
||||||
-- ==================
|
|
||||||
-- Rien de spécifique (statut enum déjà en place)
|
|
||||||
|
|
||||||
-- =========
|
|
||||||
-- EVENEMENTS
|
|
||||||
-- =========
|
|
||||||
ALTER TABLE evenements
|
|
||||||
ADD CONSTRAINT chk_evenements_dates_coherentes
|
|
||||||
CHECK (date_fin IS NULL OR date_debut IS NULL OR date_fin >= date_debut);
|
|
||||||
|
|
||||||
-- =================
|
|
||||||
-- SIGNALEMENTS_BUGS
|
|
||||||
-- =================
|
|
||||||
-- Description obligatoire, non vide
|
|
||||||
ALTER TABLE signalements_bugs
|
|
||||||
ALTER COLUMN description SET NOT NULL;
|
|
||||||
ALTER TABLE signalements_bugs
|
|
||||||
ADD CONSTRAINT chk_bugs_description_non_vide
|
|
||||||
CHECK (btrim(description) <> '');
|
|
||||||
|
|
||||||
-- =======
|
|
||||||
-- UPLOADS
|
|
||||||
-- =======
|
|
||||||
-- URL obligatoire + format basique (chemin absolu ou http(s))
|
|
||||||
ALTER TABLE uploads
|
|
||||||
ALTER COLUMN fichier_url SET NOT NULL;
|
|
||||||
ALTER TABLE uploads
|
|
||||||
ADD CONSTRAINT chk_uploads_url_format
|
|
||||||
CHECK (fichier_url ~ '^(https?://.+|/[^\\s]+)$');
|
|
||||||
|
|
||||||
-- =============
|
|
||||||
-- NOTIFICATIONS
|
|
||||||
-- =============
|
|
||||||
-- Contenu obligatoire, non vide
|
|
||||||
ALTER TABLE notifications
|
|
||||||
ALTER COLUMN contenu SET NOT NULL;
|
|
||||||
ALTER TABLE notifications
|
|
||||||
ADD CONSTRAINT chk_notifications_contenu_non_vide
|
|
||||||
CHECK (btrim(contenu) <> '');
|
|
||||||
|
|
||||||
-- ===========
|
|
||||||
-- VALIDATIONS
|
|
||||||
-- ===========
|
|
||||||
-- Rien de plus ici (Sprint 1 Ticket 8 enrichira la table)
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
-- ==========================================================
|
|
||||||
-- 04_fk_policies.sql : normalisation des politiques ON DELETE
|
|
||||||
-- A exécuter après 01_init.sql et 03_checks.sql
|
|
||||||
-- ==========================================================
|
|
||||||
|
|
||||||
-- Helper: Drop FK d'une table/colonne si elle existe (par son/leurs noms de colonne)
|
|
||||||
-- puis recrée la contrainte avec la clause fournie
|
|
||||||
-- Utilise information_schema pour retrouver le nom de contrainte auto-généré
|
|
||||||
-- NB: schema = public
|
|
||||||
|
|
||||||
-- ========== messages.id_expediteur -> utilisateurs.id : SET NULL (au lieu de CASCADE)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='messages'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='id_expediteur';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.messages DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.messages
|
|
||||||
ADD CONSTRAINT fk_messages_id_expediteur
|
|
||||||
FOREIGN KEY (id_expediteur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== parents.id_co_parent -> utilisateurs.id : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='parents'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='id_co_parent';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.parents DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.parents
|
|
||||||
ADD CONSTRAINT fk_parents_id_co_parent
|
|
||||||
FOREIGN KEY (id_co_parent) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== avenants_contrats.initie_par -> utilisateurs.id : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='avenants_contrats'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='initie_par';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.avenants_contrats DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.avenants_contrats
|
|
||||||
ADD CONSTRAINT fk_avenants_contrats_initie_par
|
|
||||||
FOREIGN KEY (initie_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== evenements.id_am -> utilisateurs.id : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='evenements'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='id_am';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.evenements
|
|
||||||
ADD CONSTRAINT fk_evenements_id_am
|
|
||||||
FOREIGN KEY (id_am) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== evenements.id_parent -> parents.id_utilisateur : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='evenements'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='id_parent';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.evenements
|
|
||||||
ADD CONSTRAINT fk_evenements_id_parent
|
|
||||||
FOREIGN KEY (id_parent) REFERENCES public.parents(id_utilisateur) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== evenements.cree_par -> utilisateurs.id : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='evenements'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='cree_par';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.evenements
|
|
||||||
ADD CONSTRAINT fk_evenements_cree_par
|
|
||||||
FOREIGN KEY (cree_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== signalements_bugs.id_utilisateur -> utilisateurs.id : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='signalements_bugs'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='id_utilisateur';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.signalements_bugs DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.signalements_bugs
|
|
||||||
ADD CONSTRAINT fk_signalements_bugs_id_utilisateur
|
|
||||||
FOREIGN KEY (id_utilisateur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ========== validations.id_utilisateur -> utilisateurs.id : SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema='public'
|
|
||||||
AND tc.table_name='validations'
|
|
||||||
AND tc.constraint_type='FOREIGN KEY'
|
|
||||||
AND kcu.column_name='id_utilisateur';
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.validations DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.validations
|
|
||||||
ADD CONSTRAINT fk_validations_id_utilisateur
|
|
||||||
FOREIGN KEY (id_utilisateur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- NB:
|
|
||||||
-- D'autres FK déjà correctes : CASCADE (assistantes_maternelles, parents, enfants_parents, dossiers, messages.id_dossier, contrats, avenants_contrats.id_contrat, evenements.id_enfant), SET NULL (uploads).
|
|
||||||
-- On laisse ON UPDATE par défaut (NO ACTION), car les UUID ne changent pas.
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- 05_triggers.sql : Timestamps automatiques
|
|
||||||
-- - Ajoute (si absent) cree_le DEFAULT now() et modifie_le DEFAULT now()
|
|
||||||
-- - Crée un trigger BEFORE UPDATE pour mettre à jour modifie_le
|
|
||||||
-- - Idempotent (DROP TRIGGER IF EXISTS / IF NOT EXISTS)
|
|
||||||
-- A exécuter après 01_init.sql, 02_indexes.sql, 03_checks.sql
|
|
||||||
-- =============================================
|
|
||||||
|
|
||||||
-- 1) Fonction unique de mise à jour du timestamp
|
|
||||||
CREATE OR REPLACE FUNCTION set_modifie_le()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.modifie_le := NOW();
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
-- Helper macro-like: pour chaque table, s'assurer des colonnes & trigger
|
|
||||||
-- (on ne peut pas faire de macro, donc on répète pour chaque table)
|
|
||||||
|
|
||||||
-- Liste des tables concernées :
|
|
||||||
-- utilisateurs, assistantes_maternelles, parents, enfants, enfants_parents,
|
|
||||||
-- dossiers, messages, contrats, avenants_contrats, evenements,
|
|
||||||
-- signalements_bugs, uploads, notifications, validations
|
|
||||||
|
|
||||||
-- ========== UTILISATEURS
|
|
||||||
ALTER TABLE utilisateurs
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_utilisateurs_set_modifie_le ON utilisateurs;
|
|
||||||
CREATE TRIGGER trg_utilisateurs_set_modifie_le
|
|
||||||
BEFORE UPDATE ON utilisateurs
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== ASSISTANTES_MATERNELLES
|
|
||||||
ALTER TABLE assistantes_maternelles
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_am_set_modifie_le ON assistantes_maternelles;
|
|
||||||
CREATE TRIGGER trg_am_set_modifie_le
|
|
||||||
BEFORE UPDATE ON assistantes_maternelles
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== PARENTS
|
|
||||||
ALTER TABLE parents
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_parents_set_modifie_le ON parents;
|
|
||||||
CREATE TRIGGER trg_parents_set_modifie_le
|
|
||||||
BEFORE UPDATE ON parents
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== ENFANTS
|
|
||||||
ALTER TABLE enfants
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_enfants_set_modifie_le ON enfants;
|
|
||||||
CREATE TRIGGER trg_enfants_set_modifie_le
|
|
||||||
BEFORE UPDATE ON enfants
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== ENFANTS_PARENTS (table de liaison)
|
|
||||||
ALTER TABLE enfants_parents
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_enfants_parents_set_modifie_le ON enfants_parents;
|
|
||||||
CREATE TRIGGER trg_enfants_parents_set_modifie_le
|
|
||||||
BEFORE UPDATE ON enfants_parents
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== DOSSIERS
|
|
||||||
ALTER TABLE dossiers
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_dossiers_set_modifie_le ON dossiers;
|
|
||||||
CREATE TRIGGER trg_dossiers_set_modifie_le
|
|
||||||
BEFORE UPDATE ON dossiers
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== MESSAGES
|
|
||||||
ALTER TABLE messages
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_messages_set_modifie_le ON messages;
|
|
||||||
CREATE TRIGGER trg_messages_set_modifie_le
|
|
||||||
BEFORE UPDATE ON messages
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== CONTRATS
|
|
||||||
ALTER TABLE contrats
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_contrats_set_modifie_le ON contrats;
|
|
||||||
CREATE TRIGGER trg_contrats_set_modifie_le
|
|
||||||
BEFORE UPDATE ON contrats
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== AVENANTS_CONTRATS
|
|
||||||
ALTER TABLE avenants_contrats
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_avenants_contrats_set_modifie_le ON avenants_contrats;
|
|
||||||
CREATE TRIGGER trg_avenants_contrats_set_modifie_le
|
|
||||||
BEFORE UPDATE ON avenants_contrats
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== EVENEMENTS
|
|
||||||
ALTER TABLE evenements
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_evenements_set_modifie_le ON evenements;
|
|
||||||
CREATE TRIGGER trg_evenements_set_modifie_le
|
|
||||||
BEFORE UPDATE ON evenements
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== SIGNALEMENTS_BUGS
|
|
||||||
ALTER TABLE signalements_bugs
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_signalements_bugs_set_modifie_le ON signalements_bugs;
|
|
||||||
CREATE TRIGGER trg_signalements_bugs_set_modifie_le
|
|
||||||
BEFORE UPDATE ON signalements_bugs
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== UPLOADS
|
|
||||||
ALTER TABLE uploads
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_uploads_set_modifie_le ON uploads;
|
|
||||||
CREATE TRIGGER trg_uploads_set_modifie_le
|
|
||||||
BEFORE UPDATE ON uploads
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== NOTIFICATIONS
|
|
||||||
ALTER TABLE notifications
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_notifications_set_modifie_le ON notifications;
|
|
||||||
CREATE TRIGGER trg_notifications_set_modifie_le
|
|
||||||
BEFORE UPDATE ON notifications
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
|
|
||||||
-- ========== VALIDATIONS
|
|
||||||
ALTER TABLE validations
|
|
||||||
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
|
|
||||||
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
|
|
||||||
DROP TRIGGER IF EXISTS trg_validations_set_modifie_le ON validations;
|
|
||||||
CREATE TRIGGER trg_validations_set_modifie_le
|
|
||||||
BEFORE UPDATE ON validations
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
-- ==========================================================
|
|
||||||
-- 06_validations_enrich.sql : Traçabilité complète des validations
|
|
||||||
-- - Ajoute la colonne 'valide_par' (FK -> utilisateurs.id)
|
|
||||||
-- - ON DELETE SET NULL pour conserver l'historique
|
|
||||||
-- - Ajoute index utiles pour les requêtes (valideur, statut, date)
|
|
||||||
-- A exécuter après : 01_init.sql, 02_indexes.sql, 03_checks.sql, 04_fk_policies.sql, 05_triggers.sql
|
|
||||||
-- ==========================================================
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
-- 1) Colonne 'valide_par' si absente
|
|
||||||
ALTER TABLE validations
|
|
||||||
ADD COLUMN IF NOT EXISTS valide_par UUID NULL;
|
|
||||||
|
|
||||||
-- 2) FK vers utilisateurs(id), ON DELETE SET NULL
|
|
||||||
DO $$
|
|
||||||
DECLARE conname text;
|
|
||||||
BEGIN
|
|
||||||
SELECT tc.constraint_name INTO conname
|
|
||||||
FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.key_column_usage kcu
|
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
|
||||||
AND tc.table_schema = kcu.table_schema
|
|
||||||
WHERE tc.table_schema = 'public'
|
|
||||||
AND tc.table_name = 'validations'
|
|
||||||
AND tc.constraint_type= 'FOREIGN KEY'
|
|
||||||
AND kcu.column_name = 'valide_par';
|
|
||||||
|
|
||||||
IF conname IS NOT NULL THEN
|
|
||||||
EXECUTE format('ALTER TABLE public.validations DROP CONSTRAINT %I', conname);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
EXECUTE $sql$
|
|
||||||
ALTER TABLE public.validations
|
|
||||||
ADD CONSTRAINT fk_validations_valide_par
|
|
||||||
FOREIGN KEY (valide_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
|
|
||||||
$sql$;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- 3) Index pour accélérer les recherches
|
|
||||||
-- - qui a validé quoi récemment ?
|
|
||||||
-- - toutes les validations par statut / par date
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_validations_valide_par_cree_le
|
|
||||||
ON validations (valide_par, cree_le);
|
|
||||||
|
|
||||||
-- Certains existent peut-être déjà : on sécurise
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_validations_id_utilisateur_cree_le
|
|
||||||
ON validations (id_utilisateur, cree_le);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_validations_statut
|
|
||||||
ON validations (statut);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
-- Script d'importation des données CSV dans la base Postgres du docker dev
|
|
||||||
-- À exécuter dans le conteneur ou via psql connecté à la base
|
|
||||||
-- psql -U admin -d ptitpas_db -f /docker-entrypoint-initdb.d/07_import.sql
|
|
||||||
-- Exemple d'utilisation :
|
|
||||||
|
|
||||||
|
|
||||||
-- Import utilisateurs
|
|
||||||
\copy utilisateurs FROM 'bdd/data_test/utilisateurs.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import assistantes_maternelles
|
|
||||||
\copy assistantes_maternelles FROM 'bdd/data_test/assistantes_maternelles.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import parents
|
|
||||||
\copy parents FROM 'bdd/data_test/parents.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import enfants
|
|
||||||
\copy enfants FROM 'bdd/data_test/enfants.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import enfants_parents
|
|
||||||
\copy enfants_parents FROM 'bdd/data_test/enfants_parents.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import dossiers
|
|
||||||
\copy dossiers FROM 'bdd/data_test/dossiers.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import contrats
|
|
||||||
\copy contrats FROM 'bdd/data_test/contrats.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import validations
|
|
||||||
\copy validations FROM 'bdd/data_test/validations.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import notifications
|
|
||||||
\copy notifications FROM 'bdd/data_test/notifications.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import uploads
|
|
||||||
\copy uploads FROM 'bdd/data_test/uploads.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Import evenements
|
|
||||||
\copy evenements FROM 'bdd/data_test/evenements.csv' DELIMITER ',' CSV HEADER;
|
|
||||||
|
|
||||||
-- Remarque :
|
|
||||||
-- Les chemins doivent être accessibles depuis le conteneur Docker (monter le dossier si besoin)
|
|
||||||
-- Adapter l'utilisateur, la base et le chemin si nécessaire
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 03_seed_test_data.sql : Données de test complètes (dashboard admin)
|
||||||
|
-- Aligné sur utilisateurs-test-complet.json
|
||||||
|
-- Mot de passe universel : password (bcrypt)
|
||||||
|
-- À exécuter après BDD.sql (init DB)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Hash bcrypt pour "password" (10 rounds)
|
||||||
|
|
||||||
|
-- ========== UTILISATEURS (1 admin + 1 gestionnaire + 2 AM + 5 parents) ==========
|
||||||
|
-- On garde admin@ptits-pas.fr (super_admin) déjà créé par BDD.sql
|
||||||
|
|
||||||
|
INSERT INTO utilisateurs (id, email, password, prenom, nom, role, statut, telephone, adresse, ville, code_postal, profession, situation_familiale, date_naissance, consentement_photo)
|
||||||
|
VALUES
|
||||||
|
('a0000001-0001-0001-0001-000000000001', 'sophie.bernard@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Sophie', 'BERNARD', 'administrateur', 'actif', '0678123456', '12 Avenue Gabriel Péri', 'Bezons', '95870', 'Responsable administrative', 'marie', '1978-03-15', false),
|
||||||
|
('a0000002-0002-0002-0002-000000000002', 'lucas.moreau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Lucas', 'MOREAU', 'gestionnaire', 'actif', '0687234567', '8 Rue Jean Jaurès', 'Bezons', '95870', 'Gestionnaire des placements', 'celibataire', '1985-09-22', false),
|
||||||
|
('a0000003-0003-0003-0003-000000000003', 'marie.dubois@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Marie', 'DUBOIS', 'assistante_maternelle', 'actif', '0696345678', '25 Rue de la République', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1980-06-08', true),
|
||||||
|
('a0000004-0004-0004-0004-000000000004', 'fatima.elmansouri@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Fatima', 'EL MANSOURI', 'assistante_maternelle', 'actif', '0675456789', '17 Boulevard Aristide Briand', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1975-11-12', true),
|
||||||
|
('a0000005-0005-0005-0005-000000000005', 'claire.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Claire', 'MARTIN', 'parent', 'actif', '0689567890', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Infirmière', 'marie', '1990-04-03', false),
|
||||||
|
('a0000006-0006-0006-0006-000000000006', 'thomas.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Thomas', 'MARTIN', 'parent', 'actif', '0678456789', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Ingénieur', 'marie', '1988-07-18', false),
|
||||||
|
('a0000007-0007-0007-0007-000000000007', 'amelie.durand@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Amélie', 'DURAND', 'parent', 'actif', '0667788990', '23 Rue Victor Hugo', 'Bezons', '95870', 'Comptable', 'divorce', '1987-12-14', false),
|
||||||
|
('a0000008-0008-0008-0008-000000000008', 'julien.rousseau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Julien', 'ROUSSEAU', 'parent', 'actif', '0656677889', '14 Rue Pasteur', 'Bezons', '95870', 'Commercial', 'divorce', '1985-08-29', false),
|
||||||
|
('a0000009-0009-0009-0009-000000000009', 'david.lecomte@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'David', 'LECOMTE', 'parent', 'actif', '0645566778', '31 Rue Émile Zola', 'Bezons', '95870', 'Développeur web', 'parent_isole', '1992-10-07', false)
|
||||||
|
ON CONFLICT (email) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== PARENTS (avec co-parent pour le couple Martin) ==========
|
||||||
|
INSERT INTO parents (id_utilisateur, id_co_parent)
|
||||||
|
VALUES
|
||||||
|
('a0000005-0005-0005-0005-000000000005', 'a0000006-0006-0006-0006-000000000006'),
|
||||||
|
('a0000006-0006-0006-0006-000000000006', 'a0000005-0005-0005-0005-000000000005'),
|
||||||
|
('a0000007-0007-0007-0007-000000000007', NULL),
|
||||||
|
('a0000008-0008-0008-0008-000000000008', NULL),
|
||||||
|
('a0000009-0009-0009-0009-000000000009', NULL)
|
||||||
|
ON CONFLICT (id_utilisateur) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== ASSISTANTES MATERNELLES ==========
|
||||||
|
INSERT INTO assistantes_maternelles (id_utilisateur, numero_agrement, nir_chiffre, nb_max_enfants, biographie, date_agrement, ville_residence, disponible, place_disponible)
|
||||||
|
VALUES
|
||||||
|
('a0000003-0003-0003-0003-000000000003', 'AGR-2019-095001', '280069512345671', 4, 'Assistante maternelle agréée depuis 2019. Spécialité bébés 0-18 mois. Accueil bienveillant et cadre sécurisant. 2 places disponibles.', '2019-09-01', 'Bezons', true, 2),
|
||||||
|
('a0000004-0004-0004-0004-000000000004', 'AGR-2017-095002', '275119512345672', 3, 'Assistante maternelle expérimentée. Spécialité 1-3 ans. Accueil à la journée. 1 place disponible.', '2017-06-15', 'Bezons', true, 1)
|
||||||
|
ON CONFLICT (id_utilisateur) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== ENFANTS ==========
|
||||||
|
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple)
|
||||||
|
VALUES
|
||||||
|
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'actif', true),
|
||||||
|
('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'actif', true),
|
||||||
|
('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'actif', true),
|
||||||
|
('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'actif', false),
|
||||||
|
('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'actif', false),
|
||||||
|
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'actif', false)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- ========== ENFANTS_PARENTS (liaison N:N) ==========
|
||||||
|
-- Martin (Claire + Thomas) -> Emma, Noah, Léa
|
||||||
|
INSERT INTO enfants_parents (id_parent, id_enfant)
|
||||||
|
VALUES
|
||||||
|
('a0000005-0005-0005-0005-000000000005', 'e0000001-0001-0001-0001-000000000001'),
|
||||||
|
('a0000005-0005-0005-0005-000000000005', 'e0000002-0002-0002-0002-000000000002'),
|
||||||
|
('a0000005-0005-0005-0005-000000000005', 'e0000003-0003-0003-0003-000000000003'),
|
||||||
|
('a0000006-0006-0006-0006-000000000006', 'e0000001-0001-0001-0001-000000000001'),
|
||||||
|
('a0000006-0006-0006-0006-000000000006', 'e0000002-0002-0002-0002-000000000002'),
|
||||||
|
('a0000006-0006-0006-0006-000000000006', 'e0000003-0003-0003-0003-000000000003'),
|
||||||
|
('a0000007-0007-0007-0007-000000000007', 'e0000004-0004-0004-0004-000000000004'),
|
||||||
|
('a0000007-0007-0007-0007-000000000007', 'e0000005-0005-0005-0005-000000000005'),
|
||||||
|
('a0000008-0008-0008-0008-000000000008', 'e0000004-0004-0004-0004-000000000004'),
|
||||||
|
('a0000008-0008-0008-0008-000000000008', 'e0000005-0005-0005-0005-000000000005'),
|
||||||
|
('a0000009-0009-0009-0009-000000000009', 'e0000006-0006-0006-0006-000000000006')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
volumes:
|
volumes:
|
||||||
- ./database/migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql
|
- ./database/BDD.sql:/docker-entrypoint-initdb.d/01_init.sql
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- ptitspas_network
|
- ptitspas_network
|
||||||
@@ -55,6 +55,8 @@ services:
|
|||||||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
|
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
|
||||||
JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES}
|
JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES}
|
||||||
NODE_ENV: ${NODE_ENV}
|
NODE_ENV: ${NODE_ENV}
|
||||||
|
LOG_API_REQUESTS: ${LOG_API_REQUESTS:-false}
|
||||||
|
CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -13,12 +13,21 @@ Ce fichier sert d'index pour naviguer dans toute la documentation du projet.
|
|||||||
- [**02 - Architecture**](./02_ARCHITECTURE.md) - Vue d'ensemble de l'architecture mono-repo et multi-conteneurs
|
- [**02 - Architecture**](./02_ARCHITECTURE.md) - Vue d'ensemble de l'architecture mono-repo et multi-conteneurs
|
||||||
- [**03 - Déploiement**](./03_DEPLOYMENT.md) - Guide complet de déploiement et configuration CI/CD
|
- [**03 - Déploiement**](./03_DEPLOYMENT.md) - Guide complet de déploiement et configuration CI/CD
|
||||||
|
|
||||||
|
### Planification
|
||||||
|
- [**04 - Roadmap Générale**](./04_ROADMAP-GENERALE.md) - Roadmap complète du projet (Phases 1 à 5+)
|
||||||
|
|
||||||
### Développement
|
### Développement
|
||||||
- [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles
|
- [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles
|
||||||
- [**11 - API Documentation**](./11_API.md) - Documentation complète des endpoints REST
|
- [**11 - API Documentation**](./11_API.md) - Documentation complète des endpoints REST
|
||||||
|
|
||||||
### Workflows Fonctionnels
|
### Workflows Fonctionnels
|
||||||
- [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs
|
- [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs
|
||||||
|
- [**21 - Configuration Système**](./21_CONFIGURATION-SYSTEME.md) - Configuration on-premise dynamique
|
||||||
|
- [**22 - Documents Légaux**](./22_DOCUMENTS-LEGAUX.md) - Gestion CGU/Privacy avec versioning
|
||||||
|
- [**23 - Liste des Tickets**](./23_LISTE-TICKETS.md) - 61 tickets Phase 1 détaillés
|
||||||
|
- [**24 - Décisions Projet**](./24_DECISIONS-PROJET.md) - Décisions architecturales et fonctionnelles
|
||||||
|
- [**25 - Backlog Phase 2**](./25_PHASE-2-BACKLOG.md) - Fonctionnalités techniques reportées
|
||||||
|
- [**26 - API Gitea**](./26_GITEA-API.md) - Procédure d'utilisation de l'API Gitea (issues, PR, branches, labels)
|
||||||
|
|
||||||
### Administration (À créer)
|
### Administration (À créer)
|
||||||
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs
|
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
# 🗺️ Roadmap Générale - Projet P'titsPas
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : 28 Novembre 2025
|
||||||
|
**Auteur** : Équipe PtitsPas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Avertissement
|
||||||
|
|
||||||
|
Les **Phases 2, 3, 4+** sont des **ébauches indicatives** qui seront affinées au fur et à mesure du développement et des retours utilisateurs. Certaines fonctionnalités mentionnées (comme la facturation) ne seront peut-être pas développées ou seront remplacées par d'autres priorités.
|
||||||
|
|
||||||
|
**Seule la Phase 1 est détaillée et validée.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Vue d'ensemble
|
||||||
|
|
||||||
|
| Phase | Focus | Estimation | Statut |
|
||||||
|
|-------|-------|------------|--------|
|
||||||
|
| **Phase 1** | Comptes & Auth | ~173h | ✅ Détaillée (61 tickets) |
|
||||||
|
| **Phase 2** | Recherche & Contact | ~100h | 📋 Ébauche |
|
||||||
|
| **Phase 3** | Contrats & Planning | ~120h | 📋 Ébauche |
|
||||||
|
| **Phase 4** | Suivi & Avancé | ~140h+ | 📋 Ébauche |
|
||||||
|
| **Phase 5+** | Optimisations | ~200h+ | 📋 Ébauche |
|
||||||
|
| **TOTAL** | | **~733h+** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Phase 1 (v1.0.0) - 🔐 Création de comptes & Authentification
|
||||||
|
|
||||||
|
**Objectif** : MVP fonctionnel avec gestion des utilisateurs
|
||||||
|
|
||||||
|
### Fonctionnalités
|
||||||
|
|
||||||
|
- ✅ Configuration système (on-premise)
|
||||||
|
- ✅ Authentification & Sécurité
|
||||||
|
- ✅ Inscription Parents (workflow 6 étapes)
|
||||||
|
- ✅ Inscription Assistantes Maternelles (workflow 5 panneaux)
|
||||||
|
- ✅ Validation par Gestionnaires (dashboard 2 onglets)
|
||||||
|
- ✅ Documents légaux (CGU/Privacy avec versioning)
|
||||||
|
- ✅ Upload photos (enfants, AM)
|
||||||
|
- ✅ Notifications email (validation, refus, création MDP)
|
||||||
|
- ✅ Logging & Monitoring
|
||||||
|
- ✅ Tests & Documentation
|
||||||
|
|
||||||
|
### Versions incrémentales
|
||||||
|
|
||||||
|
| Version | Objectif | Tickets | Estimation |
|
||||||
|
|---------|----------|---------|------------|
|
||||||
|
| **0.1.0** | MVP Fonctionnel | ~21 | ~45h |
|
||||||
|
| **0.2.0** | Sécurité & RGPD | ~10 | ~35h |
|
||||||
|
| **0.3.0** | Interfaces Complètes | ~17 | ~52h |
|
||||||
|
| **0.4.0** | Tests & Documentation | ~6 | ~24h |
|
||||||
|
| **0.5.0** | Monitoring & Optimisations | ~7 | ~17h |
|
||||||
|
| **1.0.0** | 🎉 **Release Phase 1** | **61** | **~173h** |
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
Application installable avec création et validation de comptes utilisateurs.
|
||||||
|
|
||||||
|
**Référence** : [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Phase 2 (v2.0.0) - 🤝 Mise en relation & Communication
|
||||||
|
|
||||||
|
**Objectif** : Permettre aux parents de trouver et contacter des assistantes maternelles
|
||||||
|
|
||||||
|
### Fonctionnalités (ébauche)
|
||||||
|
|
||||||
|
- 🔍 **Recherche d'AM**
|
||||||
|
- Recherche par critères (ville, capacité, disponibilité, tarifs)
|
||||||
|
- Filtres avancés
|
||||||
|
- Géolocalisation (optionnel)
|
||||||
|
|
||||||
|
- 👤 **Profils détaillés AM**
|
||||||
|
- Présentation complète
|
||||||
|
- Photos du lieu de garde
|
||||||
|
- Expérience et qualifications
|
||||||
|
- Avis/Témoignages (optionnel)
|
||||||
|
|
||||||
|
- 💬 **Messagerie interne**
|
||||||
|
- Conversations sécurisées Parent ↔ AM
|
||||||
|
- Pièces jointes
|
||||||
|
- Historique des échanges
|
||||||
|
|
||||||
|
- 📨 **Demandes de contact**
|
||||||
|
- Workflow de demande Parent → AM
|
||||||
|
- Validation/Refus par AM
|
||||||
|
- Notifications
|
||||||
|
|
||||||
|
- ⭐ **Favoris/Shortlist**
|
||||||
|
- AM sauvegardées par parents
|
||||||
|
- Comparaison de profils
|
||||||
|
|
||||||
|
### Estimation
|
||||||
|
|
||||||
|
~100h (à affiner)
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
Parents peuvent trouver, consulter et contacter des assistantes maternelles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Phase 3 (v3.0.0) - 📄 Contrats & Planning
|
||||||
|
|
||||||
|
**Objectif** : Formaliser les gardes et gérer les plannings
|
||||||
|
|
||||||
|
### Fonctionnalités (ébauche)
|
||||||
|
|
||||||
|
- 📄 **Gestion des contrats**
|
||||||
|
- Création contrats (modèle type personnalisable)
|
||||||
|
- Signature électronique ou validation
|
||||||
|
- Stockage documents contractuels (PDF)
|
||||||
|
- Historique des contrats
|
||||||
|
- Renouvellement/Modification
|
||||||
|
|
||||||
|
- 📅 **Planning & Disponibilités**
|
||||||
|
- Calendrier AM (disponibilités, absences, congés)
|
||||||
|
- Réservations/Demandes de garde
|
||||||
|
- Validation/Refus par AM
|
||||||
|
- Vue planning Parent (enfants gardés)
|
||||||
|
- Alertes conflits de planning
|
||||||
|
- Export calendrier (iCal)
|
||||||
|
|
||||||
|
### Estimation
|
||||||
|
|
||||||
|
~120h (à affiner)
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
Contrats formalisés + Planning opérationnel pour gérer les gardes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Phase 4 (v4.0.0) - 📊 Suivi & Fonctionnalités avancées
|
||||||
|
|
||||||
|
**Objectif** : Suivi quotidien des enfants et fonctionnalités complémentaires
|
||||||
|
|
||||||
|
### Fonctionnalités (ébauche)
|
||||||
|
|
||||||
|
- 📔 **Suivi des Enfants (Carnet de liaison numérique)**
|
||||||
|
- Activités quotidiennes (repas, sieste, jeux)
|
||||||
|
- Photos/Vidéos sécurisées (partage Parent ↔ AM)
|
||||||
|
- Notes/Observations
|
||||||
|
- Suivi médical (médicaments, allergies, vaccins)
|
||||||
|
- Historique complet par enfant
|
||||||
|
- Export PDF (bilan mensuel)
|
||||||
|
|
||||||
|
- 🎯 **Autres fonctionnalités à définir**
|
||||||
|
- ⚠️ **Pas de facturation** (décision validée)
|
||||||
|
- Fonctionnalités à déterminer selon retours utilisateurs Phase 2 et 3
|
||||||
|
|
||||||
|
### Estimation
|
||||||
|
|
||||||
|
~140h+ (à affiner)
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
Suivi quotidien des enfants + Fonctionnalités complémentaires.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Phase 5+ (v5.0.0+) - 🚀 Optimisations & Améliorations
|
||||||
|
|
||||||
|
**Objectif** : Optimisations, monitoring, et fonctionnalités premium
|
||||||
|
|
||||||
|
### Fonctionnalités (ébauche)
|
||||||
|
|
||||||
|
#### 📊 Statistiques & Reporting
|
||||||
|
- Dashboard gestionnaire (stats inscriptions, validations)
|
||||||
|
- Rapports collectivité (CSV/PDF)
|
||||||
|
- Graphiques évolution
|
||||||
|
- Tableaux de bord personnalisés
|
||||||
|
|
||||||
|
#### 🔒 RGPD avancé
|
||||||
|
- Droit à l'oubli (suppression compte)
|
||||||
|
- Export données personnelles (portabilité)
|
||||||
|
- Anonymisation automatique comptes inactifs
|
||||||
|
- Audit trail complet
|
||||||
|
|
||||||
|
#### 📈 Monitoring & Infrastructure
|
||||||
|
- Métriques système (CPU, RAM, BDD)
|
||||||
|
- Dashboard monitoring admin
|
||||||
|
- Sauvegarde automatique BDD (cron)
|
||||||
|
- Procédures de restauration
|
||||||
|
- Alertes automatiques
|
||||||
|
|
||||||
|
#### 📚 Documentation & Formation
|
||||||
|
- Guides utilisateur (Gestionnaire, Parent, AM)
|
||||||
|
- Vidéos tutoriels
|
||||||
|
- FAQ interactive
|
||||||
|
- Base de connaissances
|
||||||
|
|
||||||
|
#### 🎨 Améliorations UX
|
||||||
|
- Mode sombre
|
||||||
|
- Notifications push (PWA)
|
||||||
|
- Accessibilité (WCAG 2.1)
|
||||||
|
- Multi-langue (i18n)
|
||||||
|
- Responsive avancé
|
||||||
|
|
||||||
|
#### 🌟 Fonctionnalités Premium (optionnel)
|
||||||
|
- Géolocalisation AM (carte interactive)
|
||||||
|
- Système d'avis/notation
|
||||||
|
- Badges/Certifications AM
|
||||||
|
- Intégrations tierces (CAF, etc.)
|
||||||
|
- Application mobile native
|
||||||
|
|
||||||
|
### Estimation
|
||||||
|
|
||||||
|
~200h+ (à affiner)
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
|
||||||
|
Application mature, optimisée et riche en fonctionnalités.
|
||||||
|
|
||||||
|
**Référence** : [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) (anciennes fonctionnalités techniques)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Logique de progression
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 : "Je peux créer un compte"
|
||||||
|
↓
|
||||||
|
Phase 2 : "Je peux trouver et contacter une AM"
|
||||||
|
↓
|
||||||
|
Phase 3 : "Je peux signer un contrat et gérer le planning"
|
||||||
|
↓
|
||||||
|
Phase 4 : "Je peux suivre mon enfant au quotidien"
|
||||||
|
↓
|
||||||
|
Phase 5+ : "L'application est optimisée et riche en fonctionnalités"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔢 Schéma de versioning
|
||||||
|
|
||||||
|
```
|
||||||
|
X.Y.Z
|
||||||
|
|
||||||
|
X = Phase majeure (0 = dev Phase 1, 1 = Phase 1 livrée, 2 = Phase 2 livrée, etc.)
|
||||||
|
Y = Version incrémentale dans la phase (0.1, 0.2, 0.3... → 1.0)
|
||||||
|
Z = Patch/Hotfix (0 par défaut, incrémenté pour corrections)
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
- 0.1.0 → Phase 1 en dev, Version 1 (MVP)
|
||||||
|
- 0.1.1 → Phase 1 en dev, Version 1, Patch 1 (correction bug)
|
||||||
|
- 0.2.0 → Phase 1 en dev, Version 2 (Sécurité)
|
||||||
|
- 1.0.0 → Livraison finale Phase 1
|
||||||
|
- 1.0.1 → Patch Phase 1
|
||||||
|
- 2.0.0 → Livraison finale Phase 2
|
||||||
|
- 3.0.0 → Livraison finale Phase 3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Critères de passage entre phases
|
||||||
|
|
||||||
|
### Phase 1 → Phase 2
|
||||||
|
- ✅ Phase 1 terminée (61 tickets)
|
||||||
|
- ✅ Application déployée en production (au moins 1 collectivité)
|
||||||
|
- ✅ Utilisateurs réels (au moins 10 comptes validés)
|
||||||
|
- ✅ Feedback terrain collecté
|
||||||
|
- ✅ Bugs critiques corrigés
|
||||||
|
|
||||||
|
### Phase 2 → Phase 3
|
||||||
|
- ✅ Phase 2 terminée
|
||||||
|
- ✅ Recherche et messagerie utilisées activement
|
||||||
|
- ✅ Au moins 5 mises en relation réussies
|
||||||
|
- ✅ Feedback utilisateurs positif
|
||||||
|
- ✅ Besoin de formalisation des contrats exprimé
|
||||||
|
|
||||||
|
### Phase 3 → Phase 4
|
||||||
|
- ✅ Phase 3 terminée
|
||||||
|
- ✅ Contrats et planning utilisés activement
|
||||||
|
- ✅ Au moins 10 contrats signés
|
||||||
|
- ✅ Feedback utilisateurs positif
|
||||||
|
- ✅ Besoin de suivi quotidien exprimé
|
||||||
|
|
||||||
|
### Phase 4 → Phase 5+
|
||||||
|
- ✅ Phase 4 terminée
|
||||||
|
- ✅ Application stable en production
|
||||||
|
- ✅ Base utilisateurs significative (50+ comptes actifs)
|
||||||
|
- ✅ Demandes d'optimisations et fonctionnalités avancées
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes importantes
|
||||||
|
|
||||||
|
1. **Flexibilité** : Cette roadmap est indicative et sera ajustée en fonction :
|
||||||
|
- Des retours utilisateurs
|
||||||
|
- Des priorités des collectivités
|
||||||
|
- Des contraintes techniques découvertes
|
||||||
|
- Des évolutions réglementaires
|
||||||
|
|
||||||
|
2. **Priorisation** : Les fonctionnalités de chaque phase peuvent être réorganisées selon :
|
||||||
|
- L'urgence métier
|
||||||
|
- La valeur ajoutée
|
||||||
|
- La complexité technique
|
||||||
|
- Les dépendances
|
||||||
|
|
||||||
|
3. **Décisions actées** :
|
||||||
|
- ❌ Pas de facturation automatique (gestion externe)
|
||||||
|
- ❌ Pas de SMS (email uniquement)
|
||||||
|
- ✅ Application on-premise (auto-hébergée)
|
||||||
|
- ✅ Configuration dynamique (pas de hardcoding)
|
||||||
|
|
||||||
|
4. **Documentation** : Chaque phase aura sa propre documentation détaillée avant démarrage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documents de référence
|
||||||
|
|
||||||
|
- [00_INDEX.md](./00_INDEX.md) - Index général de la documentation
|
||||||
|
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) - Cahier des charges v1.3
|
||||||
|
- [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow création de comptes
|
||||||
|
- [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md) - Liste des 61 tickets Phase 1
|
||||||
|
- [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md) - Décisions architecturales
|
||||||
|
- [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) - Anciennes fonctionnalités techniques
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 28 Novembre 2025
|
||||||
|
**Version** : 1.0
|
||||||
|
**Statut** : 📋 Roadmap indicative - Phase 1 détaillée et validée
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Ticket #14 – Note pour modifications backend
|
||||||
|
|
||||||
|
**Contexte :** Première connexion admin → panneau Paramètres, déblocage après clic sur « Sauvegarder ». Le front appelle `POST /api/v1/configuration/setup/complete` au clic sur Sauvegarder.
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
|
||||||
|
Erreur renvoyée par le back :
|
||||||
|
`invalid input syntax for type uuid: "system"`
|
||||||
|
|
||||||
|
- Le controller fait `const userId = req.user?.id || 'system'` puis `markSetupCompleted(userId)`.
|
||||||
|
- Le service `set()` fait `config.modifiePar = { id: userId }` ; la colonne `modifie_par` est une FK UUID vers `users`.
|
||||||
|
- La chaîne `"system"` n’est pas un UUID valide → erreur PostgreSQL.
|
||||||
|
|
||||||
|
## Modifications à apporter au backend
|
||||||
|
|
||||||
|
**Option A – Accepter l’absence d’utilisateur (recommandé si la route peut être appelée sans JWT)**
|
||||||
|
|
||||||
|
1. **`config.controller.ts`** (route `completeSetup`)
|
||||||
|
- Remplacer :
|
||||||
|
`const userId = req.user?.id || 'system';`
|
||||||
|
- Par :
|
||||||
|
`const userId = req.user?.id ?? null;`
|
||||||
|
|
||||||
|
2. **`config.service.ts`** (`markSetupCompleted`)
|
||||||
|
- Changer la signature :
|
||||||
|
`async markSetupCompleted(userId: string | null): Promise<void>`
|
||||||
|
- Et appeler :
|
||||||
|
`await this.set('setup_completed', 'true', userId ?? undefined);`
|
||||||
|
- Dans `set()`, ne pas remplir `modifiePar` quand `userId` est absent (déjà le cas si `if (userId)`).
|
||||||
|
|
||||||
|
**Option B – Imposer un utilisateur authentifié**
|
||||||
|
|
||||||
|
- Activer le guard JWT (et éventuellement RolesGuard) sur `POST /configuration/setup/complete` pour que `req.user` soit toujours défini, et garder `userId = req.user.id` (plus de fallback `'system'`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Une fois le back modifié, le flux « Sauvegarder » → déblocage des panneaux fonctionne sans erreur.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 📋 Documentation Technique - Workflow de Création de Compte
|
# 📋 Documentation Technique - Workflow de Création de Compte
|
||||||
|
|
||||||
**Version** : 1.0
|
**Version** : 1.0
|
||||||
**Date** : 24 Novembre 2025
|
**Date** : 24 Novembre 2025
|
||||||
**Auteur** : Équipe PtitsPas
|
**Auteur** : Équipe PtitsPas
|
||||||
**Référence CDC** : Cahier des Charges P'titsPas V1.3
|
**Référence CDC** : Cahier des Charges P'titsPas V1.3
|
||||||
|
|||||||
@@ -0,0 +1,667 @@
|
|||||||
|
# 🔧 Documentation Technique - Configuration Système On-Premise
|
||||||
|
|
||||||
|
**Version** : 1.1
|
||||||
|
**Date** : 9 Février 2026
|
||||||
|
**Auteur** : Équipe PtitsPas
|
||||||
|
**Référence** : Architecture On-Premise
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Table des matières
|
||||||
|
|
||||||
|
1. [Vue d'ensemble](#vue-densemble)
|
||||||
|
2. [Architecture de configuration](#architecture-de-configuration)
|
||||||
|
3. [Table configuration](#table-configuration)
|
||||||
|
4. [Service Configuration](#service-configuration)
|
||||||
|
5. [Workflow Setup Initial](#workflow-setup-initial)
|
||||||
|
6. [APIs Configuration](#apis-configuration)
|
||||||
|
7. [Interface Admin](#interface-admin)
|
||||||
|
8. [Exemples de configuration](#exemples-de-configuration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Vue d'ensemble
|
||||||
|
|
||||||
|
### Problématique
|
||||||
|
|
||||||
|
L'application P'titsPas est déployée **on-premise** chez différentes collectivités. Chaque collectivité a :
|
||||||
|
- Son propre serveur SMTP
|
||||||
|
- Ses propres ports et configurations réseau
|
||||||
|
- Son propre nom de domaine
|
||||||
|
- Sa propre charte graphique
|
||||||
|
|
||||||
|
**Solution** : Configuration dynamique stockée en base de données, modifiable via interface web.
|
||||||
|
|
||||||
|
### Principes
|
||||||
|
|
||||||
|
1. ✅ **Pas de hardcoding** : Aucune valeur en dur dans le code
|
||||||
|
2. ✅ **Pas de redéploiement** : Modification sans rebuild Docker
|
||||||
|
3. ✅ **Sécurité** : Mots de passe chiffrés en AES-256
|
||||||
|
4. ✅ **Traçabilité** : Qui a modifié quoi et quand
|
||||||
|
5. ✅ **Setup wizard** : Configuration guidée à la première connexion
|
||||||
|
6. ✅ **Validation** : Test SMTP avant sauvegarde
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture de configuration
|
||||||
|
|
||||||
|
### Flux de données
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Application │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ Frontend │─────▶│ Backend │ │
|
||||||
|
│ │ (Admin) │ │ ConfigAPI │ │
|
||||||
|
│ └──────────────┘ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ ConfigService│ │
|
||||||
|
│ │ (Cache) │ │
|
||||||
|
│ └──────┬───────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────┐ │
|
||||||
|
│ │ PostgreSQL │ │
|
||||||
|
│ │ configuration│ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composants
|
||||||
|
|
||||||
|
1. **Table `configuration`** : Stockage clé/valeur en BDD
|
||||||
|
2. **ConfigService** : Cache en mémoire + chiffrement
|
||||||
|
3. **ConfigAPI** : Endpoints REST pour CRUD
|
||||||
|
4. **Guard Setup** : Redirection forcée si config incomplète
|
||||||
|
5. **Interface Admin** : Panneau Paramètres (3 sections) dans le dashboard, première config + accès permanent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Table configuration
|
||||||
|
|
||||||
|
### Schéma SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Table de configuration système (clé/valeur)
|
||||||
|
CREATE TABLE configuration (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
cle VARCHAR(100) UNIQUE NOT NULL, -- Clé unique (ex: 'smtp_host')
|
||||||
|
valeur TEXT, -- Valeur (peut être NULL)
|
||||||
|
type VARCHAR(50) NOT NULL, -- Type: 'string', 'number', 'boolean', 'json', 'encrypted'
|
||||||
|
categorie VARCHAR(50), -- Catégorie: 'email', 'app', 'security'
|
||||||
|
description TEXT, -- Description pour l'interface admin
|
||||||
|
modifie_le TIMESTAMPTZ DEFAULT now(), -- Date dernière modification
|
||||||
|
modifie_par UUID REFERENCES utilisateurs(id) -- Qui a modifié (traçabilité)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour performance
|
||||||
|
CREATE INDEX idx_configuration_cle ON configuration(cle);
|
||||||
|
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed initial
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
|
||||||
|
-- === Configuration Email (SMTP) ===
|
||||||
|
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
|
||||||
|
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
|
||||||
|
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
|
||||||
|
('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'),
|
||||||
|
('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'),
|
||||||
|
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
|
||||||
|
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
|
||||||
|
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
|
||||||
|
|
||||||
|
-- === Configuration Application ===
|
||||||
|
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
|
||||||
|
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
|
||||||
|
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
|
||||||
|
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
|
||||||
|
|
||||||
|
-- === Configuration Sécurité ===
|
||||||
|
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
|
||||||
|
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
|
||||||
|
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
|
||||||
|
('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types de données
|
||||||
|
|
||||||
|
| Type | Description | Exemple |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `string` | Chaîne de caractères | `"mail.example.com"` |
|
||||||
|
| `number` | Nombre entier ou décimal | `587` |
|
||||||
|
| `boolean` | Booléen | `true` / `false` |
|
||||||
|
| `json` | Objet JSON | `{"key": "value"}` |
|
||||||
|
| `encrypted` | Chaîne chiffrée AES-256 | `"a3f8b2..."` (hash) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Service Configuration
|
||||||
|
|
||||||
|
### Responsabilités
|
||||||
|
|
||||||
|
1. **Cache en mémoire** : Chargement au démarrage
|
||||||
|
2. **Lecture** : `get(key, defaultValue)`
|
||||||
|
3. **Écriture** : `set(key, value, userId)`
|
||||||
|
4. **Chiffrement** : AES-256 pour type `encrypted`
|
||||||
|
5. **Conversion de types** : string → number/boolean/json
|
||||||
|
6. **Test SMTP** : Validation connexion
|
||||||
|
|
||||||
|
### Implémentation (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// backend/src/config/config.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Configuration } from './entities/configuration.entity';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigService {
|
||||||
|
private cache: Map<string, any> = new Map();
|
||||||
|
private readonly ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Configuration)
|
||||||
|
private configRepo: Repository<Configuration>,
|
||||||
|
) {
|
||||||
|
this.loadCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chargement du cache au démarrage
|
||||||
|
async loadCache() {
|
||||||
|
const configs = await this.configRepo.find();
|
||||||
|
configs.forEach(config => {
|
||||||
|
let value = config.valeur;
|
||||||
|
|
||||||
|
// Déchiffrement si nécessaire
|
||||||
|
if (config.type === 'encrypted' && value) {
|
||||||
|
value = this.decrypt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion de type
|
||||||
|
value = this.convertType(value, config.type);
|
||||||
|
|
||||||
|
this.cache.set(config.cle, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération d'une valeur
|
||||||
|
get(key: string, defaultValue?: any): any {
|
||||||
|
return this.cache.has(key) ? this.cache.get(key) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour d'une valeur
|
||||||
|
async set(key: string, value: any, userId: string): Promise<void> {
|
||||||
|
const config = await this.configRepo.findOne({ where: { cle: key } });
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Configuration key '${key}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueToStore = String(value);
|
||||||
|
|
||||||
|
// Chiffrement si nécessaire
|
||||||
|
if (config.type === 'encrypted') {
|
||||||
|
valueToStore = this.encrypt(valueToStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.valeur = valueToStore;
|
||||||
|
config.modifie_par = userId;
|
||||||
|
config.modifie_le = new Date();
|
||||||
|
|
||||||
|
await this.configRepo.save(config);
|
||||||
|
|
||||||
|
// Mise à jour du cache
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupération de toutes les configs par catégorie
|
||||||
|
async getByCategory(category: string): Promise<any> {
|
||||||
|
const configs = await this.configRepo.find({ where: { categorie: category } });
|
||||||
|
|
||||||
|
return configs.reduce((acc, config) => {
|
||||||
|
let value = config.valeur;
|
||||||
|
|
||||||
|
if (config.type === 'encrypted') {
|
||||||
|
value = '***********'; // Masquer les mots de passe
|
||||||
|
} else {
|
||||||
|
value = this.convertType(value, config.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[config.cle] = {
|
||||||
|
value,
|
||||||
|
description: config.description,
|
||||||
|
type: config.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test de connexion SMTP
|
||||||
|
async testSmtpConnection(): Promise<boolean> {
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: this.get('smtp_host'),
|
||||||
|
port: this.get('smtp_port'),
|
||||||
|
secure: this.get('smtp_secure'),
|
||||||
|
auth: this.get('smtp_auth_required') ? {
|
||||||
|
user: this.get('smtp_user'),
|
||||||
|
pass: this.get('smtp_password'),
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transporter.verify();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SMTP test failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilitaires
|
||||||
|
private convertType(value: string, type: string): any {
|
||||||
|
switch (type) {
|
||||||
|
case 'number': return Number(value);
|
||||||
|
case 'boolean': return value === 'true';
|
||||||
|
case 'json': return JSON.parse(value);
|
||||||
|
default: return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private encrypt(text: string): string {
|
||||||
|
const cipher = crypto.createCipher('aes-256-cbc', this.ENCRYPTION_KEY);
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private decrypt(text: string): string {
|
||||||
|
const decipher = crypto.createDecipher('aes-256-cbc', this.ENCRYPTION_KEY);
|
||||||
|
let decrypted = decipher.update(text, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Workflow Setup Initial
|
||||||
|
|
||||||
|
### Diagramme de séquence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Op as Opérateur
|
||||||
|
participant App as Application (Dashboard)
|
||||||
|
participant API as ConfigAPI
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
participant SMTP as Serveur SMTP
|
||||||
|
|
||||||
|
Op->>App: Première connexion (admin)
|
||||||
|
App->>API: GET /configuration/setup/status
|
||||||
|
API->>DB: setup_completed ?
|
||||||
|
DB-->>API: false
|
||||||
|
API-->>App: setupCompleted: false
|
||||||
|
|
||||||
|
App->>App: Affiche panneau Configuration<br/>et bloque les autres onglets
|
||||||
|
|
||||||
|
Op->>Op: Remplit les 3 sections<br/>(Email, Personnalisation, Avancé)
|
||||||
|
|
||||||
|
Op->>App: Clic "Tester la connexion SMTP"
|
||||||
|
App->>API: POST /api/v1/configuration/test-smtp
|
||||||
|
API->>SMTP: Test connexion
|
||||||
|
|
||||||
|
alt Test SMTP OK
|
||||||
|
SMTP-->>API: ✅ Connexion réussie
|
||||||
|
API->>Op: Envoi email de test
|
||||||
|
API-->>App: ✅ Test réussi
|
||||||
|
App-->>Op: Message: "Email de test envoyé"
|
||||||
|
else Test SMTP KO
|
||||||
|
SMTP-->>API: ❌ Erreur connexion
|
||||||
|
API-->>App: ❌ Erreur détaillée
|
||||||
|
App-->>Op: Message: "Erreur: vérifiez les paramètres"
|
||||||
|
end
|
||||||
|
|
||||||
|
Op->>App: Clic "Sauvegarder et terminer la configuration"
|
||||||
|
App->>API: PATCH /api/v1/configuration/bulk<br/>{smtp_host, smtp_port, ...}
|
||||||
|
API->>DB: UPDATE configuration SET valeur=...
|
||||||
|
API-->>App: OK
|
||||||
|
|
||||||
|
App->>API: POST /api/v1/configuration/setup/complete
|
||||||
|
API->>DB: SET setup_completed = true
|
||||||
|
API->>API: Recharger cache ConfigService
|
||||||
|
API-->>App: OK
|
||||||
|
|
||||||
|
App->>App: Débloque la navigation<br/>Message succès
|
||||||
|
Op->>App: Accès complet au dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes détaillées
|
||||||
|
|
||||||
|
#### 1. Détection configuration incomplète
|
||||||
|
|
||||||
|
**Backend** : Le `SetupGuard` (NestJS) vérifie `setup_completed`. Si false, il autorise l’accès au dashboard et aux APIs configuration (pas de redirection vers une page dédiée). Le **frontend** appelle `GET /configuration/setup/status` au chargement du dashboard admin ; si `setupCompleted === false`, il affiche directement le **panneau Paramètres** et désactive les autres onglets jusqu’à sauvegarde.
|
||||||
|
|
||||||
|
**Guard** (exemple) : exemption des routes login + dashboard + APIs configuration.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Exemptions : /auth/login, /api/v1/configuration, routes dashboard admin
|
||||||
|
// Si setup non complété : pas de redirection HTTP ; le frontend gère l’affichage du panneau Config et le blocage des onglets.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Panneau Paramètres (Frontend)
|
||||||
|
|
||||||
|
**Une seule page avec 3 sections** (blocs successifs, pas d’onglets dans le formulaire) :
|
||||||
|
|
||||||
|
##### Section 1 : Configuration Email 📧
|
||||||
|
|
||||||
|
| Champ | Type | Valeur par défaut | Obligatoire |
|
||||||
|
|-------|------|-------------------|-------------|
|
||||||
|
| Serveur SMTP | Text | `localhost` | ✅ |
|
||||||
|
| Port SMTP | Number | `25` | ✅ |
|
||||||
|
| Sécurité | Select | `Aucune` / `STARTTLS` / `SSL/TLS` | ✅ |
|
||||||
|
| Authentification requise | Checkbox | `false` | - |
|
||||||
|
| Utilisateur SMTP | Text | - | Si auth |
|
||||||
|
| Mot de passe SMTP | Password | - | Si auth |
|
||||||
|
| Nom expéditeur | Text | `P'titsPas` | ✅ |
|
||||||
|
| Email expéditeur | Email | `no-reply@ptits-pas.fr` | ✅ |
|
||||||
|
|
||||||
|
**Bouton** : "🧪 Tester la connexion SMTP"
|
||||||
|
|
||||||
|
##### Section 2 : Personnalisation 🎨
|
||||||
|
|
||||||
|
| Champ | Type | Valeur par défaut | Obligatoire |
|
||||||
|
|-------|------|-------------------|-------------|
|
||||||
|
| Nom de l'application | Text | `P'titsPas` | ✅ |
|
||||||
|
| URL de l'application | URL | `https://app.ptits-pas.fr` | ✅ |
|
||||||
|
| Logo | File (PNG/JPG) | Logo par défaut | ❌ |
|
||||||
|
|
||||||
|
##### Section 3 : Paramètres avancés ⚙️
|
||||||
|
|
||||||
|
| Champ | Type | Valeur par défaut | Obligatoire |
|
||||||
|
|-------|------|-------------------|-------------|
|
||||||
|
| Durée validité token MDP (jours) | Number | `7` | ✅ |
|
||||||
|
| Durée session JWT (heures) | Number | `24` | ✅ |
|
||||||
|
| Taille max upload (MB) | Number | `5` | ✅ |
|
||||||
|
|
||||||
|
**Bouton** : "💾 Sauvegarder et terminer la configuration" (première config) ou "💾 Enregistrer" (accès permanent).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 APIs Configuration
|
||||||
|
|
||||||
|
### Endpoint 1 : Récupérer config par catégorie
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/configuration/:category
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Paramètres** :
|
||||||
|
- `category` : `email` | `app` | `security`
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp_host": {
|
||||||
|
"value": "localhost",
|
||||||
|
"description": "Serveur SMTP",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"smtp_port": {
|
||||||
|
"value": 25,
|
||||||
|
"description": "Port SMTP",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"smtp_password": {
|
||||||
|
"value": "***********",
|
||||||
|
"description": "Mot de passe SMTP",
|
||||||
|
"type": "encrypted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Endpoint 2 : Mise à jour multiple
|
||||||
|
|
||||||
|
```http
|
||||||
|
PATCH /api/v1/configuration/bulk
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp_host": "mail.mairie-bezons.fr",
|
||||||
|
"smtp_port": 587,
|
||||||
|
"smtp_secure": false,
|
||||||
|
"smtp_auth_required": true,
|
||||||
|
"smtp_user": "noreply@mairie-bezons.fr",
|
||||||
|
"smtp_password": "SecretPassword123",
|
||||||
|
"email_from_name": "P'titsPas - Mairie de Bezons",
|
||||||
|
"email_from_address": "noreply@mairie-bezons.fr",
|
||||||
|
"app_name": "P'titsPas Bezons",
|
||||||
|
"app_url": "https://ptitspas.mairie-bezons.fr"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Configuration mise à jour avec succès",
|
||||||
|
"updated": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Endpoint 3 : Test connexion SMTP
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/configuration/test-smtp
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp_host": "mail.mairie-bezons.fr",
|
||||||
|
"smtp_port": 587,
|
||||||
|
"smtp_secure": false,
|
||||||
|
"smtp_auth_required": true,
|
||||||
|
"smtp_user": "noreply@mairie-bezons.fr",
|
||||||
|
"smtp_password": "SecretPassword123",
|
||||||
|
"test_email": "admin@mairie-bezons.fr"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Connexion SMTP réussie. Email de test envoyé à admin@mairie-bezons.fr"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 400** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Erreur de connexion SMTP",
|
||||||
|
"error": "ECONNREFUSED: Connection refused"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Interface Admin
|
||||||
|
|
||||||
|
### Panneau Paramètres / Configuration (unique)
|
||||||
|
|
||||||
|
Un **seul panneau** dans le dashboard admin, avec **3 sections** affichées sur une même page (défilement si besoin). Pas d’onglets dans le formulaire.
|
||||||
|
|
||||||
|
- **Première configuration** (au déploiement, `setup_completed === false`) : l’opérateur arrive sur le dashboard ; le panneau Configuration est affiché par défaut et les **autres onglets sont bloqués** jusqu’à clic sur « Sauvegarder et terminer la configuration » (PATCH bulk + POST setup/complete).
|
||||||
|
- **Accès permanent** : même panneau accessible via l’onglet « Configuration » / « Paramètres » du dashboard ; pas de blocage, simple modification et enregistrement (PATCH bulk).
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Dashboard Admin [ Gestionnaires ] [ Parents ] ... │
|
||||||
|
│ [ Configuration ] ← onglet actif │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 📧 Configuration Email (SMTP) │
|
||||||
|
│ Serveur SMTP * [_________________________________] │
|
||||||
|
│ Port * [____] Sécurité [▼] ☐ Auth requise │
|
||||||
|
│ Utilisateur [__________] Mot de passe [__________] │
|
||||||
|
│ Nom expéditeur * [__________] Email * [__________] │
|
||||||
|
│ [ 🧪 Tester la connexion SMTP ] │
|
||||||
|
│ │
|
||||||
|
│ ───────────────────────────────────────────────── │
|
||||||
|
│ 🎨 Personnalisation │
|
||||||
|
│ Nom application * [__________] URL * [__________] │
|
||||||
|
│ Logo [ Choisir un fichier ] │
|
||||||
|
│ ───────────────────────────────────────────────── │
|
||||||
|
│ ⚙️ Paramètres avancés │
|
||||||
|
│ Durée token MDP (jours) [__] JWT (h) [__] Upload MB [__] │
|
||||||
|
│ │
|
||||||
|
│ [ 💾 Sauvegarder et terminer la configuration ] │
|
||||||
|
│ (ou « Enregistrer » si config déjà complétée) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Exemples de configuration
|
||||||
|
|
||||||
|
### Configuration 1 : Mairie (serveur local)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp_host": "mail.mairie-bezons.fr",
|
||||||
|
"smtp_port": 25,
|
||||||
|
"smtp_secure": false,
|
||||||
|
"smtp_auth_required": false,
|
||||||
|
"email_from_name": "P'titsPas - Mairie de Bezons",
|
||||||
|
"email_from_address": "noreply@mairie-bezons.fr",
|
||||||
|
"app_name": "P'titsPas Bezons",
|
||||||
|
"app_url": "https://ptitspas.mairie-bezons.fr"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration 2 : Gmail (pour tests)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp_host": "smtp.gmail.com",
|
||||||
|
"smtp_port": 587,
|
||||||
|
"smtp_secure": false,
|
||||||
|
"smtp_auth_required": true,
|
||||||
|
"smtp_user": "contact@ptits-pas.fr",
|
||||||
|
"smtp_password": "abcd efgh ijkl mnop",
|
||||||
|
"email_from_name": "P'titsPas",
|
||||||
|
"email_from_address": "contact@ptits-pas.fr",
|
||||||
|
"app_name": "P'titsPas",
|
||||||
|
"app_url": "https://app.ptits-pas.fr"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** : Pour Gmail, utiliser un "Mot de passe d'application" (App Password)
|
||||||
|
|
||||||
|
### Configuration 3 : Office 365
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp_host": "smtp.office365.com",
|
||||||
|
"smtp_port": 587,
|
||||||
|
"smtp_secure": false,
|
||||||
|
"smtp_auth_required": true,
|
||||||
|
"smtp_user": "noreply@collectivite.fr",
|
||||||
|
"smtp_password": "MotDePasseSecurise123",
|
||||||
|
"email_from_name": "P'titsPas - Collectivité",
|
||||||
|
"email_from_address": "noreply@collectivite.fr",
|
||||||
|
"app_name": "P'titsPas",
|
||||||
|
"app_url": "https://ptitspas.collectivite.fr"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Sécurité
|
||||||
|
|
||||||
|
### Chiffrement des mots de passe
|
||||||
|
|
||||||
|
**Algorithme** : AES-256-CBC
|
||||||
|
**Clé** : Variable d'environnement `CONFIG_ENCRYPTION_KEY` (32 caractères)
|
||||||
|
|
||||||
|
**Génération de la clé** :
|
||||||
|
```bash
|
||||||
|
# Linux/Mac
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fichier `.env`** :
|
||||||
|
```env
|
||||||
|
CONFIG_ENCRYPTION_KEY=a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables d'environnement critiques
|
||||||
|
|
||||||
|
**Fichier `.env`** (à créer lors de l'installation) :
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Base de données
|
||||||
|
DATABASE_URL=postgresql://app_user:password@ptitspas-postgres:5432/ptitpas_db
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=VotreSecretJWTTresLongEtAleatoire123456789
|
||||||
|
JWT_EXPIRY=24h
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONFIG_ENCRYPTION_KEY=VotreCleDeChiffrementAES256TresLongue32Caracteres
|
||||||
|
|
||||||
|
# Application
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Références
|
||||||
|
|
||||||
|
### Documentation interne
|
||||||
|
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md)
|
||||||
|
- [02_ARCHITECTURE.md](./02_ARCHITECTURE.md)
|
||||||
|
- [03_DEPLOYMENT.md](./03_DEPLOYMENT.md)
|
||||||
|
- [10_DATABASE.md](./10_DATABASE.md)
|
||||||
|
- [11_API.md](./11_API.md)
|
||||||
|
|
||||||
|
### Documentation externe
|
||||||
|
- [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)
|
||||||
|
- [Nodemailer SMTP](https://nodemailer.com/smtp/)
|
||||||
|
- [Node.js Crypto](https://nodejs.org/api/crypto.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 9 Février 2026
|
||||||
|
**Version** : 1.1
|
||||||
|
**Statut** : ✅ Document validé
|
||||||
|
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
# 📄 Documentation Technique - Gestion Documents Légaux (CGU/Privacy)
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : 25 Novembre 2025
|
||||||
|
**Auteur** : Équipe PtitsPas
|
||||||
|
**Référence** : RGPD & Conformité juridique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Table des matières
|
||||||
|
|
||||||
|
1. [Vue d'ensemble](#vue-densemble)
|
||||||
|
2. [Architecture](#architecture)
|
||||||
|
3. [Tables BDD](#tables-bdd)
|
||||||
|
4. [Service Documents Légaux](#service-documents-légaux)
|
||||||
|
5. [Workflow Upload & Activation](#workflow-upload--activation)
|
||||||
|
6. [Workflow Acceptation Utilisateur](#workflow-acceptation-utilisateur)
|
||||||
|
7. [APIs](#apis)
|
||||||
|
8. [Interface Admin](#interface-admin)
|
||||||
|
9. [Conformité RGPD](#conformité-rgpd)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Vue d'ensemble
|
||||||
|
|
||||||
|
### Problématique
|
||||||
|
|
||||||
|
Chaque collectivité déployant P'titsPas on-premise doit pouvoir :
|
||||||
|
1. ✅ **Personnaliser** les CGU et la Politique de confidentialité
|
||||||
|
2. ✅ **Versionner** les documents (traçabilité juridique)
|
||||||
|
3. ✅ **Tracer** qui a accepté quelle version (RGPD)
|
||||||
|
4. ✅ **Prouver** l'acceptation (IP, User-Agent, horodatage)
|
||||||
|
5. ✅ **Empêcher** le retour en arrière (sécurité juridique)
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
- **Documents génériques v1** fournis par défaut (rédigés avec juriste)
|
||||||
|
- **Upload de nouvelles versions** par l'admin (PDF uniquement)
|
||||||
|
- **Versioning automatique** (incrémentation sans retour arrière)
|
||||||
|
- **Activation manuelle** (prévisualisation avant mise en prod)
|
||||||
|
- **Traçabilité complète** (hash SHA-256, IP, User-Agent)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Flux de données
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Workflow Documents │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. UPLOAD (Admin) │
|
||||||
|
│ Admin ──▶ API ──▶ File System ──▶ BDD │
|
||||||
|
│ /documents/legaux/ │
|
||||||
|
│ cgu_v4_<timestamp>.pdf │
|
||||||
|
│ │
|
||||||
|
│ 2. ACTIVATION (Admin) │
|
||||||
|
│ Admin ──▶ API ──▶ BDD (actif=true) │
|
||||||
|
│ │
|
||||||
|
│ 3. ACCEPTATION (Utilisateur) │
|
||||||
|
│ User ──▶ Frontend ──▶ API ──▶ BDD │
|
||||||
|
│ (inscription) (trace IP/UA) │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composants
|
||||||
|
|
||||||
|
1. **Table `documents_legaux`** : Stockage versions + métadonnées
|
||||||
|
2. **Table `acceptations_documents`** : Traçabilité acceptations
|
||||||
|
3. **Service `DocumentsLegauxService`** : Upload, versioning, activation
|
||||||
|
4. **API REST** : CRUD documents
|
||||||
|
5. **Interface Admin** : Upload + activation
|
||||||
|
6. **Interface Inscription** : Affichage + acceptation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Tables BDD
|
||||||
|
|
||||||
|
### Table 1 : `documents_legaux`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Table pour gérer les versions des documents légaux
|
||||||
|
CREATE TABLE documents_legaux (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
|
||||||
|
version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté)
|
||||||
|
fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier
|
||||||
|
fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage
|
||||||
|
fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité
|
||||||
|
actif BOOLEAN DEFAULT false, -- Version actuellement active
|
||||||
|
televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé
|
||||||
|
televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload
|
||||||
|
active_le TIMESTAMPTZ, -- Date d'activation
|
||||||
|
UNIQUE(type, version) -- Pas de doublon version
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour performance
|
||||||
|
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
|
||||||
|
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contraintes** :
|
||||||
|
- ✅ Un seul document `actif=true` par type à la fois
|
||||||
|
- ✅ Versioning auto-incrémenté (pas de gaps)
|
||||||
|
- ✅ Hash SHA-256 pour vérifier l'intégrité du fichier
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Table 2 : `acceptations_documents`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Table de traçabilité des acceptations (RGPD)
|
||||||
|
CREATE TABLE acceptations_documents (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
|
||||||
|
id_document UUID REFERENCES documents_legaux(id),
|
||||||
|
type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
|
||||||
|
version_document INTEGER NOT NULL, -- Version acceptée
|
||||||
|
accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation
|
||||||
|
ip_address INET, -- IP de l'utilisateur (RGPD)
|
||||||
|
user_agent TEXT -- Navigateur (preuve)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
|
||||||
|
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Données capturées** :
|
||||||
|
- ✅ **Qui** : `id_utilisateur`
|
||||||
|
- ✅ **Quoi** : `type_document`, `version_document`
|
||||||
|
- ✅ **Quand** : `accepte_le`
|
||||||
|
- ✅ **Où** : `ip_address`
|
||||||
|
- ✅ **Comment** : `user_agent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Modification table `utilisateurs`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Ajouter colonnes pour référence rapide (optionnel)
|
||||||
|
ALTER TABLE utilisateurs
|
||||||
|
ADD COLUMN cgu_version_acceptee INTEGER,
|
||||||
|
ADD COLUMN cgu_acceptee_le TIMESTAMPTZ,
|
||||||
|
ADD COLUMN privacy_version_acceptee INTEGER,
|
||||||
|
ADD COLUMN privacy_acceptee_le TIMESTAMPTZ;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** : Ces colonnes sont **redondantes** avec `acceptations_documents`, mais permettent un accès rapide sans JOIN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Seed initial
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Documents génériques v1 (fournis par défaut)
|
||||||
|
INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES
|
||||||
|
('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()),
|
||||||
|
('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fichiers à fournir** :
|
||||||
|
- `/documents/legaux/cgu_v1_default.pdf` (rédigé avec juriste)
|
||||||
|
- `/documents/legaux/privacy_v1_default.pdf` (conforme RGPD)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Service Documents Légaux
|
||||||
|
|
||||||
|
### Responsabilités
|
||||||
|
|
||||||
|
1. **Récupérer documents actifs** : `getDocumentsActifs()`
|
||||||
|
2. **Uploader nouvelle version** : `uploadNouvelleVersion(type, file, userId)`
|
||||||
|
3. **Activer une version** : `activerVersion(documentId)`
|
||||||
|
4. **Lister versions** : `listerVersions(type)`
|
||||||
|
5. **Télécharger document** : `telechargerDocument(documentId)`
|
||||||
|
|
||||||
|
### Implémentation (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// backend/src/documents-legaux/documents-legaux.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { DocumentLegal } from './entities/document-legal.entity';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentsLegauxService {
|
||||||
|
private readonly UPLOAD_DIR = '/app/documents/legaux';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DocumentLegal)
|
||||||
|
private docRepo: Repository<DocumentLegal>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Récupérer les documents actifs
|
||||||
|
async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> {
|
||||||
|
const cgu = await this.docRepo.findOne({
|
||||||
|
where: { type: 'cgu', actif: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const privacy = await this.docRepo.findOne({
|
||||||
|
where: { type: 'privacy', actif: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cgu || !privacy) {
|
||||||
|
throw new Error('Documents légaux manquants');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cgu, privacy };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uploader une nouvelle version
|
||||||
|
async uploadNouvelleVersion(
|
||||||
|
type: 'cgu' | 'privacy',
|
||||||
|
file: Express.Multer.File,
|
||||||
|
userId: string,
|
||||||
|
): Promise<DocumentLegal> {
|
||||||
|
// 1. Calculer la prochaine version
|
||||||
|
const lastDoc = await this.docRepo.findOne({
|
||||||
|
where: { type },
|
||||||
|
order: { version: 'DESC' },
|
||||||
|
});
|
||||||
|
const nouvelleVersion = (lastDoc?.version || 0) + 1;
|
||||||
|
|
||||||
|
// 2. Calculer le hash du fichier
|
||||||
|
const fileBuffer = file.buffer;
|
||||||
|
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||||
|
|
||||||
|
// 3. Générer le nom de fichier unique
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`;
|
||||||
|
const filePath = path.join(this.UPLOAD_DIR, fileName);
|
||||||
|
|
||||||
|
// 4. Sauvegarder le fichier
|
||||||
|
await fs.mkdir(this.UPLOAD_DIR, { recursive: true });
|
||||||
|
await fs.writeFile(filePath, fileBuffer);
|
||||||
|
|
||||||
|
// 5. Créer l'entrée en BDD
|
||||||
|
const document = this.docRepo.create({
|
||||||
|
type,
|
||||||
|
version: nouvelleVersion,
|
||||||
|
fichier_nom: file.originalname,
|
||||||
|
fichier_path: filePath,
|
||||||
|
fichier_hash: hash,
|
||||||
|
actif: false, // Pas actif par défaut
|
||||||
|
televerse_par: userId,
|
||||||
|
televerse_le: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.docRepo.save(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activer une version
|
||||||
|
async activerVersion(documentId: string): Promise<void> {
|
||||||
|
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction : désactiver l'ancienne version, activer la nouvelle
|
||||||
|
await this.docRepo.manager.transaction(async (manager) => {
|
||||||
|
// Désactiver toutes les versions de ce type
|
||||||
|
await manager.update(
|
||||||
|
DocumentLegal,
|
||||||
|
{ type: document.type, actif: true },
|
||||||
|
{ actif: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activer la nouvelle version
|
||||||
|
await manager.update(
|
||||||
|
DocumentLegal,
|
||||||
|
{ id: documentId },
|
||||||
|
{ actif: true, active_le: new Date() },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lister toutes les versions (pour l'admin)
|
||||||
|
async listerVersions(type: 'cgu' | 'privacy'): Promise<DocumentLegal[]> {
|
||||||
|
return await this.docRepo.find({
|
||||||
|
where: { type },
|
||||||
|
order: { version: 'DESC' },
|
||||||
|
relations: ['televerse_par'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Télécharger un document (stream)
|
||||||
|
async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> {
|
||||||
|
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileBuffer = await fs.readFile(document.fichier_path);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: fileBuffer,
|
||||||
|
filename: document.fichier_nom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier l'intégrité d'un document
|
||||||
|
async verifierIntegrite(documentId: string): Promise<boolean> {
|
||||||
|
const document = await this.docRepo.findOne({ where: { id: documentId } });
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document non trouvé');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileBuffer = await fs.readFile(document.fichier_path);
|
||||||
|
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||||
|
|
||||||
|
return hash === document.fichier_hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Workflow Upload & Activation
|
||||||
|
|
||||||
|
### Diagramme de séquence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Admin
|
||||||
|
participant API as Backend API
|
||||||
|
participant FS as File System
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
A->>API: POST /api/v1/documents-legaux<br/>{type: 'cgu', file: PDF}
|
||||||
|
|
||||||
|
API->>API: Validation fichier<br/>(PDF, max 10MB)
|
||||||
|
API->>API: Calcul hash SHA-256
|
||||||
|
|
||||||
|
API->>DB: SELECT MAX(version)<br/>WHERE type='cgu'
|
||||||
|
DB-->>API: version = 3
|
||||||
|
|
||||||
|
API->>API: Nouvelle version = 4
|
||||||
|
|
||||||
|
API->>FS: Enregistrer fichier<br/>/documents/legaux/cgu_v4_<timestamp>.pdf
|
||||||
|
FS-->>API: ✅ Fichier sauvegardé
|
||||||
|
|
||||||
|
API->>DB: INSERT INTO documents_legaux<br/>(type, version=4, actif=false)
|
||||||
|
DB-->>API: ✅ Document créé
|
||||||
|
|
||||||
|
API-->>A: 201 Created<br/>{id, version: 4, actif: false}
|
||||||
|
|
||||||
|
A->>A: Prévisualisation PDF
|
||||||
|
A->>API: PATCH /api/v1/documents-legaux/{id}/activer
|
||||||
|
|
||||||
|
API->>DB: BEGIN TRANSACTION
|
||||||
|
API->>DB: UPDATE documents_legaux<br/>SET actif=false WHERE type='cgu'
|
||||||
|
API->>DB: UPDATE documents_legaux<br/>SET actif=true, active_le=now()<br/>WHERE id={id}
|
||||||
|
API->>DB: COMMIT
|
||||||
|
|
||||||
|
API-->>A: ✅ CGU v4 activées
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 Workflow Acceptation Utilisateur
|
||||||
|
|
||||||
|
### Diagramme de séquence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as Utilisateur
|
||||||
|
participant App as Frontend
|
||||||
|
participant API as Backend
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
U->>App: Inscription (étape CGU)
|
||||||
|
|
||||||
|
App->>API: GET /api/v1/documents-legaux/actifs
|
||||||
|
API->>DB: SELECT * FROM documents_legaux<br/>WHERE actif=true
|
||||||
|
DB-->>API: {cgu: v4, privacy: v2}
|
||||||
|
API-->>App: {cgu: {version: 4, url: '...'}, privacy: {...}}
|
||||||
|
|
||||||
|
App->>App: Afficher liens PDF<br/>"CGU v4" et "Privacy v2"
|
||||||
|
|
||||||
|
U->>U: Lit les documents
|
||||||
|
U->>U: Coche "J'accepte"
|
||||||
|
|
||||||
|
App->>API: POST /api/v1/auth/register<br/>{..., cgu_version: 4, privacy_version: 2, ip, user_agent}
|
||||||
|
|
||||||
|
API->>DB: BEGIN TRANSACTION
|
||||||
|
|
||||||
|
API->>DB: INSERT INTO utilisateurs<br/>(..., cgu_version_acceptee=4, privacy_version_acceptee=2)
|
||||||
|
DB-->>API: id_utilisateur
|
||||||
|
|
||||||
|
API->>DB: INSERT INTO acceptations_documents<br/>(id_utilisateur, type='cgu', version=4, ip, user_agent)
|
||||||
|
API->>DB: INSERT INTO acceptations_documents<br/>(id_utilisateur, type='privacy', version=2, ip, user_agent)
|
||||||
|
|
||||||
|
API->>DB: COMMIT
|
||||||
|
|
||||||
|
API-->>App: ✅ Inscription réussie
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 APIs
|
||||||
|
|
||||||
|
### API 1 : Récupérer documents actifs (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/documents-legaux/actifs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cgu": {
|
||||||
|
"id": "uuid-cgu-v4",
|
||||||
|
"type": "cgu",
|
||||||
|
"version": 4,
|
||||||
|
"url": "/api/v1/documents-legaux/uuid-cgu-v4/download",
|
||||||
|
"active_le": "2025-11-20T14:30:00Z"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"id": "uuid-privacy-v2",
|
||||||
|
"type": "privacy",
|
||||||
|
"version": 2,
|
||||||
|
"url": "/api/v1/documents-legaux/uuid-privacy-v2/download",
|
||||||
|
"active_le": "2025-10-15T09:15:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API 2 : Lister versions (Admin)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/documents-legaux/:type/versions
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Paramètres** :
|
||||||
|
- `type` : `cgu` | `privacy`
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "uuid-cgu-v4",
|
||||||
|
"version": 4,
|
||||||
|
"fichier_nom": "CGU_Mairie_Bezons_2025.pdf",
|
||||||
|
"actif": true,
|
||||||
|
"televerse_par": {
|
||||||
|
"id": "uuid-admin",
|
||||||
|
"prenom": "Lucas",
|
||||||
|
"nom": "MOREAU"
|
||||||
|
},
|
||||||
|
"televerse_le": "2025-11-20T14:00:00Z",
|
||||||
|
"active_le": "2025-11-20T14:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uuid-cgu-v3",
|
||||||
|
"version": 3,
|
||||||
|
"fichier_nom": "CGU_v3.pdf",
|
||||||
|
"actif": false,
|
||||||
|
"televerse_par": {
|
||||||
|
"id": "uuid-admin",
|
||||||
|
"prenom": "Admin",
|
||||||
|
"nom": "Système"
|
||||||
|
},
|
||||||
|
"televerse_le": "2025-10-15T09:00:00Z",
|
||||||
|
"active_le": "2025-10-15T09:15:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API 3 : Upload nouvelle version (Admin)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/documents-legaux
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body** :
|
||||||
|
```
|
||||||
|
type: cgu
|
||||||
|
file: <fichier PDF>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 201** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-nouveau-doc",
|
||||||
|
"type": "cgu",
|
||||||
|
"version": 5,
|
||||||
|
"fichier_nom": "CGU_Mairie_Bezons_2025_v2.pdf",
|
||||||
|
"actif": false,
|
||||||
|
"televerse_le": "2025-11-25T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erreurs** :
|
||||||
|
- `400 Bad Request` : Fichier non PDF ou trop volumineux (>10MB)
|
||||||
|
- `401 Unauthorized` : Token manquant ou invalide
|
||||||
|
- `403 Forbidden` : Rôle insuffisant (pas super_admin)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API 4 : Activer une version (Admin)
|
||||||
|
|
||||||
|
```http
|
||||||
|
PATCH /api/v1/documents-legaux/:id/activer
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Document activé avec succès",
|
||||||
|
"documentId": "uuid-nouveau-doc",
|
||||||
|
"type": "cgu",
|
||||||
|
"version": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API 5 : Télécharger document (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/documents-legaux/:id/download
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```
|
||||||
|
Content-Type: application/pdf
|
||||||
|
Content-Disposition: attachment; filename="CGU_v5.pdf"
|
||||||
|
|
||||||
|
<binary PDF data>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API 6 : Historique acceptations utilisateur (Admin)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/users/:userId/acceptations
|
||||||
|
Authorization: Bearer <super_admin_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse 200** :
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type_document": "cgu",
|
||||||
|
"version_document": 4,
|
||||||
|
"accepte_le": "2025-11-20T15:30:00Z",
|
||||||
|
"ip_address": "192.168.1.100",
|
||||||
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type_document": "privacy",
|
||||||
|
"version_document": 2,
|
||||||
|
"accepte_le": "2025-11-20T15:30:00Z",
|
||||||
|
"ip_address": "192.168.1.100",
|
||||||
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Interface Admin
|
||||||
|
|
||||||
|
### Écran Gestion Documents Légaux
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 📄 Gestion des Documents Légaux │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ [ CGU ] [ Politique de confidentialité ] │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 📋 Conditions Générales d'Utilisation (CGU) │
|
||||||
|
│ │
|
||||||
|
│ Version active : v4 │
|
||||||
|
│ Activée le : 20/11/2025 14:30 │
|
||||||
|
│ Téléversée par : Lucas MOREAU │
|
||||||
|
│ │
|
||||||
|
│ [ 📥 Télécharger ] [ 👁️ Prévisualiser ] │
|
||||||
|
│ │
|
||||||
|
│ ───────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ 📤 Uploader une nouvelle version │
|
||||||
|
│ │
|
||||||
|
│ ⚠️ Attention : L'upload d'une nouvelle version │
|
||||||
|
│ créera la version v5. Vous pourrez la prévisualiser│
|
||||||
|
│ avant de l'activer. │
|
||||||
|
│ │
|
||||||
|
│ [ Choisir un fichier PDF ] (max 10MB) │
|
||||||
|
│ │
|
||||||
|
│ [ 📤 Uploader ] │
|
||||||
|
│ │
|
||||||
|
│ ───────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ 📜 Historique des versions │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────┐ │
|
||||||
|
│ │ ✅ v4 (Active) │ │
|
||||||
|
│ │ Activée le : 20/11/2025 14:30 │ │
|
||||||
|
│ │ Par : Lucas MOREAU │ │
|
||||||
|
│ │ Hash : a3f8b2c4...e0f1a2 ✓ │ │
|
||||||
|
│ │ [ 📥 Télécharger ] [ 👁️ Voir ] │ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────┐ │
|
||||||
|
│ │ v3 (Inactive) │ │
|
||||||
|
│ │ Activée le : 15/10/2025 09:15 │ │
|
||||||
|
│ │ Par : Admin Système │ │
|
||||||
|
│ │ Hash : b4f9c3d6...f2a3b4 ✓ │ │
|
||||||
|
│ │ [ 📥 Télécharger ] [ 👁️ Voir ] [🔄 Réactiver]│ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Conformité RGPD
|
||||||
|
|
||||||
|
### Données capturées
|
||||||
|
|
||||||
|
| Donnée | Justification RGPD | Durée conservation |
|
||||||
|
|--------|-------------------|-------------------|
|
||||||
|
| `id_utilisateur` | Traçabilité acceptation | Durée du compte |
|
||||||
|
| `version_document` | Preuve version acceptée | Durée du compte |
|
||||||
|
| `accepte_le` | Horodatage légal | Durée du compte |
|
||||||
|
| `ip_address` | Preuve origine acceptation | 1 an (recommandé) |
|
||||||
|
| `user_agent` | Preuve navigateur/appareil | 1 an (recommandé) |
|
||||||
|
|
||||||
|
### Droits utilisateur
|
||||||
|
|
||||||
|
#### Droit d'accès (Article 15)
|
||||||
|
L'utilisateur peut demander :
|
||||||
|
- Quelles versions il a acceptées
|
||||||
|
- Quand il les a acceptées
|
||||||
|
- Depuis quelle IP
|
||||||
|
|
||||||
|
**API** : `GET /api/v1/users/me/acceptations`
|
||||||
|
|
||||||
|
#### Droit à l'oubli (Article 17)
|
||||||
|
Lors de la suppression du compte :
|
||||||
|
- Suppression des données personnelles
|
||||||
|
- Conservation des acceptations anonymisées (obligation légale)
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
```sql
|
||||||
|
-- Anonymisation (pas suppression totale)
|
||||||
|
UPDATE acceptations_documents
|
||||||
|
SET ip_address = NULL,
|
||||||
|
user_agent = NULL
|
||||||
|
WHERE id_utilisateur = '<uuid>';
|
||||||
|
|
||||||
|
-- Puis suppression utilisateur
|
||||||
|
DELETE FROM utilisateurs WHERE id = '<uuid>';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Références
|
||||||
|
|
||||||
|
### Documentation interne
|
||||||
|
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md)
|
||||||
|
- [10_DATABASE.md](./10_DATABASE.md)
|
||||||
|
- [11_API.md](./11_API.md)
|
||||||
|
- [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md)
|
||||||
|
|
||||||
|
### Documentation externe
|
||||||
|
- [RGPD - Article 7 (Consentement)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre2#Article7)
|
||||||
|
- [RGPD - Article 15 (Droit d'accès)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre3#Article15)
|
||||||
|
- [RGPD - Article 17 (Droit à l'oubli)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre3#Article17)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 25 Novembre 2025
|
||||||
|
**Version** : 1.0
|
||||||
|
**Statut** : ✅ Document validé
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
# 📋 Décisions Projet - P'titsPas
|
||||||
|
|
||||||
|
**Version** : 1.1
|
||||||
|
**Date** : 9 Février 2026
|
||||||
|
**Auteur** : Équipe PtitsPas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Objectif de ce document
|
||||||
|
|
||||||
|
Ce document trace toutes les **décisions importantes** prises lors de la conception du projet P'titsPas. Il sert de référence pour comprendre les choix techniques et fonctionnels.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Décisions Architecture
|
||||||
|
|
||||||
|
### 1. Mono-repo vs Multi-repo
|
||||||
|
|
||||||
|
**Décision** : ✅ **Mono-repo**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Code cohérent dans un seul dépôt Git
|
||||||
|
- Commits atomiques (frontend + backend + BDD en même temps)
|
||||||
|
- CI/CD simplifié (un seul webhook)
|
||||||
|
- Facilite le travail collaboratif
|
||||||
|
|
||||||
|
**Structure** :
|
||||||
|
```
|
||||||
|
ptitspas-app/
|
||||||
|
├── frontend/ # Application Flutter
|
||||||
|
├── backend/ # API NestJS
|
||||||
|
├── database/ # Migrations SQL/Prisma
|
||||||
|
├── api-contracts/ # Contrats OpenAPI + Prisma
|
||||||
|
├── docs/ # Documentation
|
||||||
|
└── docker-compose.yml # Orchestration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Déploiement On-Premise
|
||||||
|
|
||||||
|
**Décision** : ✅ **Application on-premise avec configuration dynamique**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Chaque collectivité a son infrastructure (SMTP, domaines, ports)
|
||||||
|
- Autonomie totale (pas de dépendance à notre infra)
|
||||||
|
- Conformité aux politiques IT locales
|
||||||
|
- Sécurité (données restent dans le SI de la collectivité)
|
||||||
|
|
||||||
|
**Solution technique** :
|
||||||
|
- Table `configuration` en BDD (clé/valeur)
|
||||||
|
- Panneau Paramètres (3 sections) dans le dashboard à la première connexion ; navigation bloquée jusqu'à sauvegarde
|
||||||
|
- Configuration SMTP dynamique
|
||||||
|
- Personnalisation (nom app, logo, URL)
|
||||||
|
|
||||||
|
**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Gestion des fichiers uploadés
|
||||||
|
|
||||||
|
**Décision** : ✅ **Système de fichiers local avec volume Docker**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Simplicité (pas de S3/MinIO pour on-premise)
|
||||||
|
- Performance (accès direct au disque)
|
||||||
|
- Coût (pas de service externe)
|
||||||
|
|
||||||
|
**Solution technique** :
|
||||||
|
- Stockage dans `/uploads/photos/`
|
||||||
|
- Volume Docker persistant
|
||||||
|
- Service Upload avec Multer
|
||||||
|
- Validation taille (depuis config)
|
||||||
|
|
||||||
|
**Alternatives rejetées** :
|
||||||
|
- ❌ Base de données (BYTEA) : Mauvaise performance
|
||||||
|
- ❌ S3/MinIO : Overkill pour on-premise
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Documents légaux (CGU/Privacy)
|
||||||
|
|
||||||
|
**Décision** : ✅ **Versioning automatique avec traçabilité RGPD**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Conformité juridique (preuve d'acceptation)
|
||||||
|
- Flexibilité (chaque collectivité adapte ses CGU)
|
||||||
|
- Traçabilité (hash SHA-256, IP, User-Agent)
|
||||||
|
- Sécurité juridique (pas de retour en arrière)
|
||||||
|
|
||||||
|
**Solution technique** :
|
||||||
|
- Table `documents_legaux` (versioning auto-incrémenté)
|
||||||
|
- Table `acceptations_documents` (traçabilité RGPD)
|
||||||
|
- Upload PDF par l'admin
|
||||||
|
- Activation manuelle après prévisualisation
|
||||||
|
- Documents génériques v1 fournis par défaut
|
||||||
|
|
||||||
|
**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Décisions Fonctionnelles
|
||||||
|
|
||||||
|
### 5. Workflow inscription sans mot de passe
|
||||||
|
|
||||||
|
**Décision** : ✅ **Pas de mot de passe lors de l'inscription, lien email après validation**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Simplifie l'inscription (moins de friction)
|
||||||
|
- Adapté aux parents séparés/divorcés (communication difficile)
|
||||||
|
- Sécurité (token UUID avec expiration)
|
||||||
|
- Validation gestionnaire avant activation
|
||||||
|
|
||||||
|
**Workflow** :
|
||||||
|
1. Parent/AM s'inscrit (sans MDP)
|
||||||
|
2. Gestionnaire valide
|
||||||
|
3. Email envoyé avec lien création MDP (valable 7 jours)
|
||||||
|
4. Utilisateur crée son MDP
|
||||||
|
5. Compte activé
|
||||||
|
|
||||||
|
**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Genre enfant obligatoire (H/F)
|
||||||
|
|
||||||
|
**Décision** : ✅ **Genre obligatoire (H/F uniquement)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Contexte métier : Enfants à garder (pas de genre neutre)
|
||||||
|
- Conformité CDC
|
||||||
|
- Simplicité
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
- Champ `genre` ENUM('H', 'F') NOT NULL dans table `enfants`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Suppression champs téléphone
|
||||||
|
|
||||||
|
**Décision** : ✅ **Un seul champ "Téléphone" (mobile privilégié)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Simplification (plus besoin de "Mobile" et "Téléphone fixe")
|
||||||
|
- Réalité terrain : Tout le monde a un mobile
|
||||||
|
- Note dans l'interface : "(mobile privilégié)"
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
- Supprimer `mobile` et `telephone_fixe`
|
||||||
|
- Garder uniquement `telephone`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. NIR obligatoire pour Assistantes Maternelles
|
||||||
|
|
||||||
|
**Décision** : ✅ **NIR (Numéro de Sécurité sociale) obligatoire**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Conformité CDC
|
||||||
|
- Nécessaire pour génération automatique du contrat
|
||||||
|
- Stockage sécurisé (chiffrement recommandé)
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
- Champ `nir_chiffre` VARCHAR(15) NOT NULL dans `assistantes_maternelles`
|
||||||
|
- Validation : 15 chiffres exactement
|
||||||
|
- Affichage masqué dans l'interface (XXX XX XX XX XXX 123)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Suppression notifications SMS
|
||||||
|
|
||||||
|
**Décision** : ✅ **Supprimer les notifications SMS, garder uniquement Email**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Pas de plus-value identifiée
|
||||||
|
- Coût supplémentaire (service SMS)
|
||||||
|
- Complexité (intégration Twilio/OVH)
|
||||||
|
- Email suffit largement
|
||||||
|
|
||||||
|
**Action** :
|
||||||
|
- Amender le CDC v1.4
|
||||||
|
- Remplacer "Email OU SMS" par "Email"
|
||||||
|
- Supprimer toute mention de SMS
|
||||||
|
|
||||||
|
**Référence** : Ticket #57
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 Décisions Phase 2 (reportées)
|
||||||
|
|
||||||
|
### 10. Gestion erreurs SMTP (renvoyer email)
|
||||||
|
|
||||||
|
**Décision** : ⏸️ **Phase 2 ou cas par cas**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Pas prioritaire pour MVP
|
||||||
|
- Downtime SMTP rare (< 24h)
|
||||||
|
- Recréation dossier acceptable en phase de test
|
||||||
|
- Peut être ajouté plus tard si besoin terrain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Sauvegarde & Restauration automatique
|
||||||
|
|
||||||
|
**Décision** : ⏸️ **Phase finale (quand tout fonctionne)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Pas bloquant pour développement
|
||||||
|
- Nécessite application stable
|
||||||
|
- Script `pg_dump` simple à ajouter
|
||||||
|
- Documentation pour admins sys
|
||||||
|
|
||||||
|
**À faire** :
|
||||||
|
- Script `backup.sh` avec cron
|
||||||
|
- Guide sauvegarde/restauration
|
||||||
|
- Test restauration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. RGPD avancé (droit à l'oubli, export données)
|
||||||
|
|
||||||
|
**Décision** : ⏸️ **Phase 2**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Conformité de base assurée (consentement, traçabilité)
|
||||||
|
- Droit à l'oubli : Peu de demandes en phase test
|
||||||
|
- Export données : Peut être fait manuellement en phase test
|
||||||
|
|
||||||
|
**À faire** :
|
||||||
|
- API suppression compte (soft delete)
|
||||||
|
- API export données personnelles (JSON)
|
||||||
|
- Anonymisation comptes inactifs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Statistiques dashboard
|
||||||
|
|
||||||
|
**Décision** : ⏸️ **Phase 2 (besoin d'utilisateurs d'abord)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Pas de données = pas de statistiques
|
||||||
|
- Fonctionnalité importante pour vente
|
||||||
|
- Nécessite application en production
|
||||||
|
|
||||||
|
**À faire** :
|
||||||
|
- API statistiques (comptes, enfants, AM, validations)
|
||||||
|
- Widgets dashboard (graphiques)
|
||||||
|
- Export rapports
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. Migration de données
|
||||||
|
|
||||||
|
**Décision** : ❌ **Pas de migration prévue**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Pas de système existant à migrer
|
||||||
|
- Application nouvelle
|
||||||
|
- Import CSV peut être ajouté cas par cas si besoin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. Documentation utilisateur
|
||||||
|
|
||||||
|
**Décision** : ⏸️ **Phase 2 (formation présentiel prioritaire)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Premiers utilisateurs : Formation directe par l'équipe
|
||||||
|
- Documentation nécessaire pour scalabilité
|
||||||
|
- Vidéos tutoriels utiles mais pas bloquant
|
||||||
|
|
||||||
|
**À faire** :
|
||||||
|
- Guide utilisateur gestionnaire
|
||||||
|
- Guide utilisateur parent/AM
|
||||||
|
- FAQ
|
||||||
|
- Vidéos tutoriels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Décisions Sécurité
|
||||||
|
|
||||||
|
### 16. Chiffrement mots de passe configuration
|
||||||
|
|
||||||
|
**Décision** : ✅ **AES-256-CBC pour mots de passe SMTP**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Sécurité (mots de passe SMTP en clair = risque)
|
||||||
|
- Conformité sécurité
|
||||||
|
- Déchiffrement uniquement en mémoire
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
- Type `encrypted` dans table `configuration`
|
||||||
|
- Clé de chiffrement dans `.env` (32 caractères)
|
||||||
|
- Service ConfigService gère chiffrement/déchiffrement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. Hash mots de passe utilisateurs
|
||||||
|
|
||||||
|
**Décision** : ✅ **Bcrypt avec 12 rounds**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Standard industrie
|
||||||
|
- Résistant aux attaques brute-force
|
||||||
|
- Configurable (rounds dans config)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. Tokens création mot de passe
|
||||||
|
|
||||||
|
**Décision** : ✅ **UUID avec expiration 7 jours**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Sécurité (UUID impossible à deviner)
|
||||||
|
- Expiration (limite fenêtre d'attaque)
|
||||||
|
- Durée configurable (dans table `configuration`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 19. JWT sessions
|
||||||
|
|
||||||
|
**Décision** : ✅ **JWT avec expiration 24h**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Stateless (pas de session en BDD)
|
||||||
|
- Performance (pas de lookup BDD à chaque requête)
|
||||||
|
- Durée configurable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Décisions Base de Données
|
||||||
|
|
||||||
|
### 20. PostgreSQL vs autres SGBD
|
||||||
|
|
||||||
|
**Décision** : ✅ **PostgreSQL 17**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Open-source
|
||||||
|
- Robuste et performant
|
||||||
|
- Support JSON (flexibilité)
|
||||||
|
- Support UUID natif
|
||||||
|
- Communauté active
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 21. ORM : Prisma vs TypeORM
|
||||||
|
|
||||||
|
**Décision** : ✅ **Prisma** (mais TypeORM déjà en place)
|
||||||
|
|
||||||
|
**Justification Prisma** :
|
||||||
|
- Type-safety
|
||||||
|
- Migrations claires
|
||||||
|
- Génération client automatique
|
||||||
|
|
||||||
|
**Note** : Le projet YNOV utilise TypeORM. À évaluer si migration vers Prisma nécessaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 22. Migrations versionnées
|
||||||
|
|
||||||
|
**Décision** : ✅ **Migrations SQL versionnées**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Traçabilité des changements schéma
|
||||||
|
- Rollback possible
|
||||||
|
- Déploiement automatisé
|
||||||
|
|
||||||
|
**Implémentation** :
|
||||||
|
- Fichiers `XX_nom_migration.sql`
|
||||||
|
- Exécution via `prisma migrate deploy`
|
||||||
|
- User `app_admin` pour DDL
|
||||||
|
- User `app_user` pour DML (runtime)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Décisions Tests
|
||||||
|
|
||||||
|
### 23. Stratégie de tests
|
||||||
|
|
||||||
|
**Décision** : ✅ **Tests unitaires + intégration + E2E**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Qualité code
|
||||||
|
- Confiance déploiement
|
||||||
|
- Détection régression
|
||||||
|
|
||||||
|
**Couverture cible** :
|
||||||
|
- Tests unitaires : > 80%
|
||||||
|
- Tests intégration : Workflows complets
|
||||||
|
- Tests E2E : Parcours utilisateurs critiques
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Décisions Documentation
|
||||||
|
|
||||||
|
### 24. Structure documentation
|
||||||
|
|
||||||
|
**Décision** : ✅ **Documentation centralisée dans `/docs/` avec numérotation**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Lisibilité (ordre logique)
|
||||||
|
- Maintenance (tout au même endroit)
|
||||||
|
- Versioning (Git)
|
||||||
|
|
||||||
|
**Structure** :
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── 00_INDEX.md
|
||||||
|
├── 01_CAHIER-DES-CHARGES.md
|
||||||
|
├── 02_ARCHITECTURE.md
|
||||||
|
├── 03_DEPLOYMENT.md
|
||||||
|
├── 10_DATABASE.md
|
||||||
|
├── 11_API.md
|
||||||
|
├── 20_WORKFLOW-CREATION-COMPTE.md
|
||||||
|
├── 21_CONFIGURATION-SYSTEME.md
|
||||||
|
├── 22_DOCUMENTS-LEGAUX.md
|
||||||
|
├── 23_LISTE-TICKETS.md
|
||||||
|
├── 24_DECISIONS-PROJET.md (ce document)
|
||||||
|
├── 90_AUDIT.md
|
||||||
|
└── test-data/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Décisions Workflow
|
||||||
|
|
||||||
|
### 25. Gitea + Webhook + Déploiement automatique
|
||||||
|
|
||||||
|
**Décision** : ✅ **Déploiement automatique sur push master**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Productivité (pas de déploiement manuel)
|
||||||
|
- Fiabilité (script testé)
|
||||||
|
- Rapidité (feedback immédiat)
|
||||||
|
|
||||||
|
**Workflow** :
|
||||||
|
1. Push sur `master`
|
||||||
|
2. Webhook Gitea déclenché
|
||||||
|
3. Script `deploy-ptitspas.sh` exécuté
|
||||||
|
4. Git pull + Migrations + Docker rebuild + Restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 26. Branches Git
|
||||||
|
|
||||||
|
**Décision** : ✅ **Stratégie simple : master + feature branches**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Simplicité (petite équipe)
|
||||||
|
- Flexibilité
|
||||||
|
|
||||||
|
**Branches** :
|
||||||
|
- `master` : Production
|
||||||
|
- `archive/*` : Archives (ex: maquette initiale)
|
||||||
|
- `migration/*` : Migrations (ex: intégration YNOV)
|
||||||
|
- `feature/*` : Nouvelles fonctionnalités
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎫 Décisions Ticketing
|
||||||
|
|
||||||
|
### 27. Taille des tickets
|
||||||
|
|
||||||
|
**Décision** : ✅ **Tickets relativement petits (2-6h)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Granularité (suivi précis)
|
||||||
|
- Motivation (tickets terminables rapidement)
|
||||||
|
- Revue de code facilitée
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 28. Organisation tickets
|
||||||
|
|
||||||
|
**Décision** : ✅ **1 ticket = 1 fonctionnalité complète (Front + Back + BDD si nécessaire)**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Cohérence (toute la feature développée ensemble)
|
||||||
|
- Testable (end-to-end immédiatement)
|
||||||
|
- Atomique (livraison de valeur fonctionnelle)
|
||||||
|
|
||||||
|
**Alternative rejetée** :
|
||||||
|
- ❌ 1 ticket Front + 1 ticket Back + 1 ticket BDD = Dépendances complexes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Décisions Déploiement
|
||||||
|
|
||||||
|
### 29. Docker Compose vs Kubernetes
|
||||||
|
|
||||||
|
**Décision** : ✅ **Docker Compose**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Simplicité (on-premise petite/moyenne collectivité)
|
||||||
|
- Pas de besoin de scalabilité horizontale
|
||||||
|
- Maintenance facile
|
||||||
|
|
||||||
|
**Alternative rejetée** :
|
||||||
|
- ❌ Kubernetes : Overkill pour on-premise mono-instance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 30. Traefik comme reverse proxy
|
||||||
|
|
||||||
|
**Décision** : ✅ **Traefik**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Configuration automatique (labels Docker)
|
||||||
|
- SSL Let's Encrypt automatique
|
||||||
|
- Dashboard intégré
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Logs & Monitoring
|
||||||
|
|
||||||
|
### 31. Système de logs
|
||||||
|
|
||||||
|
**Décision** : ✅ **Winston avec rotation quotidienne**
|
||||||
|
|
||||||
|
**Justification** :
|
||||||
|
- Standard NestJS
|
||||||
|
- Rotation automatique (pas de disque plein)
|
||||||
|
- Niveaux de log (info, warn, error)
|
||||||
|
- Transports multiples (console + fichier)
|
||||||
|
|
||||||
|
**Logs à capturer** :
|
||||||
|
- Logs applicatifs (info, warn, error)
|
||||||
|
- Logs emails (succès/échec SMTP)
|
||||||
|
- Logs connexions (qui se connecte quand)
|
||||||
|
- Logs validations (qui valide quoi)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Résumé des décisions critiques
|
||||||
|
|
||||||
|
| # | Décision | Statut | Impact |
|
||||||
|
|---|----------|--------|--------|
|
||||||
|
| 1 | Mono-repo | ✅ Phase 1 | Architecture |
|
||||||
|
| 2 | On-premise avec config dynamique | ✅ Phase 1 | Déploiement |
|
||||||
|
| 3 | Système de fichiers pour uploads | ✅ Phase 1 | Stockage |
|
||||||
|
| 4 | Versioning documents légaux | ✅ Phase 1 | RGPD |
|
||||||
|
| 5 | Inscription sans MDP | ✅ Phase 1 | UX |
|
||||||
|
| 6 | Genre enfant obligatoire (H/F) | ✅ Phase 1 | Métier |
|
||||||
|
| 7 | Un seul champ téléphone | ✅ Phase 1 | Simplification |
|
||||||
|
| 8 | NIR obligatoire AM | ✅ Phase 1 | Conformité |
|
||||||
|
| 9 | Suppression SMS | ✅ Phase 1 | Simplification |
|
||||||
|
| 10 | Renvoyer email | ⏸️ Phase 2 | Nice-to-have |
|
||||||
|
| 11 | Sauvegarde auto | ⏸️ Phase finale | Ops |
|
||||||
|
| 12 | RGPD avancé | ⏸️ Phase 2 | Conformité |
|
||||||
|
| 13 | Statistiques | ⏸️ Phase 2 | Business |
|
||||||
|
| 14 | Migration données | ❌ Rejeté | N/A |
|
||||||
|
| 15 | Doc utilisateur | ⏸️ Phase 2 | Formation |
|
||||||
|
| 31 | Logs Winston | ✅ Phase 1 | Monitoring |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Historique des modifications
|
||||||
|
|
||||||
|
| Date | Version | Modifications |
|
||||||
|
|------|---------|---------------|
|
||||||
|
| 25/11/2025 | 1.0 | Création du document - Toutes les décisions initiales |
|
||||||
|
| 09/02/2026 | 1.1 | Configuration initiale : un seul panneau Paramètres (3 sections) dans le dashboard, plus de Setup Wizard dédié ; navigation bloquée jusqu'à sauvegarde |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 9 Février 2026
|
||||||
|
**Version** : 1.1
|
||||||
|
**Statut** : ✅ Document validé
|
||||||
|
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# 📋 Backlog Phase 2 - P'titsPas
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : 25 Novembre 2025
|
||||||
|
**Auteur** : Équipe PtitsPas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Objectif de ce document
|
||||||
|
|
||||||
|
Ce document liste toutes les **fonctionnalités reportées en Phase 2**. Ces fonctionnalités ne sont pas bloquantes pour le MVP (Phase 1), mais apportent de la valeur ajoutée pour la production intensive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Vue d'ensemble
|
||||||
|
|
||||||
|
| Catégorie | Nombre de tickets | Estimation |
|
||||||
|
|-----------|-------------------|------------|
|
||||||
|
| **RGPD avancé** | 3 tickets | ~8h |
|
||||||
|
| **Monitoring avancé** | 2 tickets | ~6h |
|
||||||
|
| **Statistiques & Reporting** | 4 tickets | ~16h |
|
||||||
|
| **Sauvegarde & Restauration** | 2 tickets | ~6h |
|
||||||
|
| **Documentation utilisateur** | 3 tickets | ~12h |
|
||||||
|
| **Améliorations UX** | 3 tickets | ~10h |
|
||||||
|
| **TOTAL** | **17 tickets** | **~58h** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 RGPD avancé
|
||||||
|
|
||||||
|
### Ticket P2-01 : [Backend] API Suppression compte (soft delete)
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `backend`, `phase-2`, `rgpd`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Implémenter le droit à l'oubli (RGPD Article 17) avec suppression logique des comptes.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Endpoint `DELETE /api/v1/users/:id` (soft delete)
|
||||||
|
- [ ] Ajout champ `supprime_le` TIMESTAMPTZ dans `utilisateurs`
|
||||||
|
- [ ] Anonymisation données personnelles (email, téléphone, adresse)
|
||||||
|
- [ ] Conservation données légales (acceptations CGU)
|
||||||
|
- [ ] Guards (super_admin only)
|
||||||
|
- [ ] Tests unitaires
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Peu de demandes en phase test
|
||||||
|
- Suppression manuelle possible en attendant
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-02 : [Backend] API Export données personnelles (RGPD)
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `backend`, `phase-2`, `rgpd`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Implémenter le droit à la portabilité (RGPD Article 20) avec export JSON des données personnelles.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Endpoint `GET /api/v1/users/:id/export` (JSON)
|
||||||
|
- [ ] Export données utilisateur + enfants + acceptations
|
||||||
|
- [ ] Format JSON structuré
|
||||||
|
- [ ] Guards (utilisateur lui-même ou admin)
|
||||||
|
- [ ] Tests unitaires
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Export manuel SQL possible en phase test
|
||||||
|
- Peu de demandes attendues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-03 : [Backend] Cron anonymisation comptes inactifs
|
||||||
|
**Estimation** : 2h
|
||||||
|
**Labels** : `backend`, `phase-2`, `rgpd`, `cron`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Anonymiser automatiquement les comptes inactifs après X mois (configurable).
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Cron job quotidien
|
||||||
|
- [ ] Détection comptes inactifs (dernière connexion > X mois)
|
||||||
|
- [ ] Anonymisation automatique
|
||||||
|
- [ ] Notification admin
|
||||||
|
- [ ] Configuration durée inactivité (table `configuration`)
|
||||||
|
- [ ] Tests
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas de comptes inactifs en phase test
|
||||||
|
- Peut être fait manuellement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring avancé
|
||||||
|
|
||||||
|
### Ticket P2-04 : [Backend] API Métriques système
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `backend`, `phase-2`, `monitoring`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Exposer des métriques système (CPU, RAM, disque, BDD) pour monitoring.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Endpoint `GET /api/v1/metrics` (super_admin only)
|
||||||
|
- [ ] Métriques système (CPU, RAM, disque)
|
||||||
|
- [ ] Métriques BDD (connexions, taille, requêtes lentes)
|
||||||
|
- [ ] Métriques application (requêtes/s, temps réponse)
|
||||||
|
- [ ] Format Prometheus (optionnel)
|
||||||
|
- [ ] Tests
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas critique pour MVP
|
||||||
|
- Logs suffisent pour debugging initial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-05 : [Frontend] Dashboard Monitoring
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `frontend`, `phase-2`, `monitoring`, `admin`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Créer un dashboard de monitoring pour le super admin.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Page `/admin/monitoring` (super_admin only)
|
||||||
|
- [ ] Graphiques temps réel (CPU, RAM, disque)
|
||||||
|
- [ ] Alertes (seuils configurables)
|
||||||
|
- [ ] Historique métriques (7 jours)
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas critique pour MVP
|
||||||
|
- Monitoring serveur possible via outils système
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Statistiques & Reporting
|
||||||
|
|
||||||
|
### Ticket P2-06 : [Backend] API Statistiques dashboard
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `backend`, `phase-2`, `statistiques`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Créer les endpoints pour récupérer les statistiques de l'application.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Endpoint `GET /api/v1/stats/overview`
|
||||||
|
- [ ] Statistiques globales (comptes, enfants, AM, validations)
|
||||||
|
- [ ] Statistiques temporelles (inscriptions par mois, validations par semaine)
|
||||||
|
- [ ] Statistiques géographiques (par ville)
|
||||||
|
- [ ] Cache (rafraîchissement toutes les heures)
|
||||||
|
- [ ] Tests
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas de données = pas de statistiques
|
||||||
|
- Nécessite application en production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-07 : [Frontend] Widgets statistiques dashboard
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `frontend`, `phase-2`, `statistiques`, `gestionnaire`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Afficher les statistiques dans le dashboard gestionnaire.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Widget "Comptes en attente" (nombre)
|
||||||
|
- [ ] Widget "Comptes validés ce mois" (graphique)
|
||||||
|
- [ ] Widget "Enfants inscrits" (nombre)
|
||||||
|
- [ ] Widget "AM disponibles" (nombre)
|
||||||
|
- [ ] Graphique évolution inscriptions (6 derniers mois)
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Besoin d'utilisateurs réels pour avoir des données
|
||||||
|
- Fonctionnalité importante pour vente, mais pas bloquante pour MVP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-08 : [Backend] API Export rapports (CSV/PDF)
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `backend`, `phase-2`, `reporting`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Permettre l'export de rapports pour les gestionnaires.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Endpoint `GET /api/v1/reports/users` (CSV)
|
||||||
|
- [ ] Endpoint `GET /api/v1/reports/validations` (CSV)
|
||||||
|
- [ ] Endpoint `GET /api/v1/reports/summary` (PDF)
|
||||||
|
- [ ] Génération PDF (librairie PDFKit)
|
||||||
|
- [ ] Filtres (date, statut, rôle)
|
||||||
|
- [ ] Tests
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Export manuel SQL possible en phase test
|
||||||
|
- Nécessite données réelles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-09 : [Frontend] Écran Rapports
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `frontend`, `phase-2`, `reporting`, `gestionnaire`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Créer un écran de génération de rapports pour les gestionnaires.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Page `/admin/rapports` (gestionnaire/admin)
|
||||||
|
- [ ] Sélection type de rapport (utilisateurs, validations, synthèse)
|
||||||
|
- [ ] Filtres (date, statut, rôle)
|
||||||
|
- [ ] Prévisualisation
|
||||||
|
- [ ] Téléchargement (CSV/PDF)
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas prioritaire pour MVP
|
||||||
|
- Nécessite données réelles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Sauvegarde & Restauration
|
||||||
|
|
||||||
|
### Ticket P2-10 : [Infra] Script backup PostgreSQL automatique
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `infra`, `phase-2`, `backup`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Créer un script de sauvegarde automatique de la base de données.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Script `backup.sh` avec `pg_dump`
|
||||||
|
- [ ] Compression (gzip)
|
||||||
|
- [ ] Rotation (garder 30 derniers jours)
|
||||||
|
- [ ] Cron job quotidien (3h du matin)
|
||||||
|
- [ ] Notification email en cas d'échec
|
||||||
|
- [ ] Tests restauration
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas bloquant pour développement
|
||||||
|
- Backup manuel possible en phase test
|
||||||
|
- À faire avant mise en production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-11 : [Doc] Guide sauvegarde & restauration
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `documentation`, `phase-2`, `backup`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Documenter les procédures de sauvegarde et restauration pour les admins sys.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Procédure sauvegarde manuelle
|
||||||
|
- [ ] Procédure restauration
|
||||||
|
- [ ] Configuration cron
|
||||||
|
- [ ] Troubleshooting
|
||||||
|
- [ ] Exemples de commandes
|
||||||
|
- [ ] Checklist pré-restauration
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Documentation technique, pas bloquante pour MVP
|
||||||
|
- À faire avant mise en production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation utilisateur
|
||||||
|
|
||||||
|
### Ticket P2-12 : [Doc] Guide utilisateur Gestionnaire
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `documentation`, `phase-2`, `formation`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Rédiger le guide utilisateur pour les gestionnaires.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Connexion et première utilisation
|
||||||
|
- [ ] Consultation des demandes
|
||||||
|
- [ ] Validation/Refus de comptes
|
||||||
|
- [ ] Gestion des paramètres
|
||||||
|
- [ ] FAQ gestionnaire
|
||||||
|
- [ ] Captures d'écran
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Formation présentiel prioritaire pour premiers utilisateurs
|
||||||
|
- Documentation nécessaire pour scalabilité
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-13 : [Doc] Guide utilisateur Parent/AM
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `documentation`, `phase-2`, `formation`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Rédiger le guide utilisateur pour les parents et assistantes maternelles.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Inscription étape par étape
|
||||||
|
- [ ] Création du mot de passe
|
||||||
|
- [ ] Connexion
|
||||||
|
- [ ] Utilisation de l'application
|
||||||
|
- [ ] FAQ parent/AM
|
||||||
|
- [ ] Captures d'écran
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Formation présentiel prioritaire
|
||||||
|
- Documentation nécessaire pour scalabilité
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-14 : [Doc] Vidéos tutoriels
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `documentation`, `phase-2`, `formation`, `video`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Créer des vidéos tutoriels pour les utilisateurs.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Vidéo "Inscription parent" (3-5 min)
|
||||||
|
- [ ] Vidéo "Inscription AM" (3-5 min)
|
||||||
|
- [ ] Vidéo "Validation comptes gestionnaire" (3-5 min)
|
||||||
|
- [ ] Vidéo "Configuration initiale admin" (5-7 min)
|
||||||
|
- [ ] Hébergement (YouTube privé ou serveur)
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Pas bloquant pour MVP
|
||||||
|
- Utile pour scalabilité et autonomie utilisateurs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Améliorations UX
|
||||||
|
|
||||||
|
### Ticket P2-15 : [Frontend] Mode sombre
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `frontend`, `phase-2`, `ux`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Ajouter un mode sombre à l'application.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Thème sombre (couleurs, contrastes)
|
||||||
|
- [ ] Toggle mode clair/sombre
|
||||||
|
- [ ] Sauvegarde préférence utilisateur
|
||||||
|
- [ ] Adaptation tous les écrans
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Nice-to-have, pas bloquant
|
||||||
|
- Améliore confort utilisateur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-16 : [Frontend] Notifications push (optionnel)
|
||||||
|
**Estimation** : 4h
|
||||||
|
**Labels** : `frontend`, `phase-2`, `ux`, `notifications`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Ajouter des notifications push pour les événements importants.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Service Worker (PWA)
|
||||||
|
- [ ] Notification "Compte validé"
|
||||||
|
- [ ] Notification "Nouveau message" (si messagerie)
|
||||||
|
- [ ] Gestion permissions
|
||||||
|
- [ ] Paramètres notifications
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Nice-to-have, pas bloquant
|
||||||
|
- Nécessite PWA ou app mobile
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ticket P2-17 : [Frontend] Accessibilité (WCAG 2.1)
|
||||||
|
**Estimation** : 3h
|
||||||
|
**Labels** : `frontend`, `phase-2`, `ux`, `a11y`
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Améliorer l'accessibilité de l'application (conformité WCAG 2.1).
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [ ] Audit accessibilité (Lighthouse, axe)
|
||||||
|
- [ ] Correction contrastes
|
||||||
|
- [ ] Attributs ARIA
|
||||||
|
- [ ] Navigation clavier
|
||||||
|
- [ ] Lecteur d'écran (test)
|
||||||
|
|
||||||
|
**Justification report Phase 2** :
|
||||||
|
- Amélioration continue
|
||||||
|
- Pas bloquant pour MVP
|
||||||
|
- Important pour collectivités (obligation légale)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Priorisation Phase 2
|
||||||
|
|
||||||
|
### Priorité Haute (à faire en premier)
|
||||||
|
1. **Sauvegarde & Restauration** (Tickets P2-10, P2-11) → Avant mise en production
|
||||||
|
2. **Statistiques** (Tickets P2-06, P2-07) → Argument de vente
|
||||||
|
3. **Documentation utilisateur** (Tickets P2-12, P2-13) → Scalabilité
|
||||||
|
|
||||||
|
### Priorité Moyenne
|
||||||
|
4. **RGPD avancé** (Tickets P2-01, P2-02, P2-03) → Conformité
|
||||||
|
5. **Reporting** (Tickets P2-08, P2-09) → Utile pour gestionnaires
|
||||||
|
|
||||||
|
### Priorité Basse
|
||||||
|
6. **Monitoring avancé** (Tickets P2-04, P2-05) → Nice-to-have
|
||||||
|
7. **Améliorations UX** (Tickets P2-15, P2-16, P2-17) → Confort
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Critères de passage en Phase 2
|
||||||
|
|
||||||
|
La Phase 2 peut commencer quand :
|
||||||
|
- ✅ Phase 1 terminée (61 tickets)
|
||||||
|
- ✅ Application déployée en production (au moins 1 collectivité)
|
||||||
|
- ✅ Utilisateurs réels (au moins 10 comptes validés)
|
||||||
|
- ✅ Feedback terrain collecté
|
||||||
|
- ✅ Bugs critiques corrigés
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Estimation globale Phase 2
|
||||||
|
|
||||||
|
**Total** : 17 tickets
|
||||||
|
**Estimation** : ~58h de développement
|
||||||
|
|
||||||
|
**Planning suggéré** :
|
||||||
|
- Sprint 1 (2 semaines) : Sauvegarde + Statistiques (26h)
|
||||||
|
- Sprint 2 (2 semaines) : Documentation + RGPD (24h)
|
||||||
|
- Sprint 3 (1 semaine) : Reporting + Améliorations UX (8h)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 25 Novembre 2025
|
||||||
|
**Version** : 1.0
|
||||||
|
**Statut** : ✅ Backlog Phase 2 défini
|
||||||
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Procédure – Utilisation de l'API Gitea
|
||||||
|
|
||||||
|
## 1. Contexte
|
||||||
|
|
||||||
|
- **Instance** : https://git.ptits-pas.fr
|
||||||
|
- **API de base** : `https://git.ptits-pas.fr/api/v1`
|
||||||
|
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
|
||||||
|
|
||||||
|
## 2. Authentification
|
||||||
|
|
||||||
|
### 2.1 Token
|
||||||
|
|
||||||
|
Le token est défini dans l'environnement (ex. `~/.bashrc`) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITEA_TOKEN="<votre_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour l'utiliser dans les commandes :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc # ou : . ~/.bashrc
|
||||||
|
# Puis utiliser $GITEA_TOKEN dans les curl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 En-tête HTTP
|
||||||
|
|
||||||
|
Toutes les requêtes API doivent envoyer le token :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-H "Authorization: token $GITEA_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Endpoints utiles
|
||||||
|
|
||||||
|
### 3.1 Dépôt (repository)
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
|
||||||
|
| Liste dépôts | GET | `/repos/search?q=petitspas` |
|
||||||
|
|
||||||
|
Exemple – infos du dépôt :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Issues (tickets)
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|------------------|---------|-----|
|
||||||
|
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
|
||||||
|
| Détail d'une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
|
||||||
|
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
|
||||||
|
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
|
||||||
|
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
|
||||||
|
|
||||||
|
**Paramètres GET utiles pour la liste :**
|
||||||
|
|
||||||
|
- `state` : `open` ou `closed`
|
||||||
|
- `labels` : filtre par label (ex. `frontend`)
|
||||||
|
- `page`, `limit` : pagination
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Toutes les issues ouvertes
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
|
||||||
|
|
||||||
|
# Issues ouvertes avec label "frontend"
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
|
||||||
|
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
|
||||||
|
|
||||||
|
# Détail de l'issue #47
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
|
||||||
|
|
||||||
|
# Fermer l'issue #31
|
||||||
|
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"state":"closed"}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
|
||||||
|
|
||||||
|
# Créer une issue
|
||||||
|
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Pull requests
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
|
||||||
|
| Détail d'une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
|
||||||
|
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
|
||||||
|
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Liste des PR ouvertes
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
|
||||||
|
|
||||||
|
# Créer une PR (head = branche source, base = branche cible)
|
||||||
|
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Branches
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Webhooks
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
|
||||||
|
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
|
||||||
|
|
||||||
|
### 3.6 Labels
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Résumé des URLs pour P'titsPas
|
||||||
|
|
||||||
|
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
|
||||||
|
|
||||||
|
| Ressource | URL |
|
||||||
|
|------------------|-----|
|
||||||
|
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
|
||||||
|
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
|
||||||
|
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
|
||||||
|
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
|
||||||
|
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
|
||||||
|
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
|
||||||
|
|
||||||
|
## 5. Documentation officielle
|
||||||
|
|
||||||
|
- Swagger / OpenAPI : https://docs.gitea.com/api
|
||||||
|
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
|
||||||
|
|
||||||
|
## 6. Dépannage
|
||||||
|
|
||||||
|
- **401 Unauthorized** : vérifier le token et l'en-tête `Authorization: token <TOKEN>`.
|
||||||
|
- **404** : vérifier owner/repo et l'URL (sensible à la casse).
|
||||||
|
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Note Backend - Activation du module Gestionnaires (Ticket #92)
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
L'endpoint `GET /api/v1/gestionnaires` renvoie une erreur **404 Not Found**.
|
||||||
|
Cela est dû au fait que le `GestionnairesModule` n'est pas importé dans l'arbre des modules de l'application (via `UserModule` ou `AppModule`).
|
||||||
|
|
||||||
|
## Solution de contournement actuelle (Frontend)
|
||||||
|
Le frontend utilise actuellement l'endpoint générique `/api/v1/users` et filtre les résultats côté client pour ne garder que les utilisateurs ayant le rôle `gestionnaire`.
|
||||||
|
*Fichier concerné : `frontend/lib/services/user_service.dart`*
|
||||||
|
|
||||||
|
## Correctif Backend à appliquer
|
||||||
|
Pour activer proprement l'endpoint dédié, il faut effectuer les modifications suivantes dans le backend :
|
||||||
|
|
||||||
|
### 1. Importer le module dans `UserModule`
|
||||||
|
Fichier : `backend/src/routes/user/user.module.ts`
|
||||||
|
|
||||||
|
Ajouter `GestionnairesModule` dans les imports.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
// ... autres imports
|
||||||
|
GestionnairesModule, // <--- AJOUTER ICI
|
||||||
|
],
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
export class UserModule { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ajouter AuthModule dans `GestionnairesModule`
|
||||||
|
Fichier : `backend/src/routes/user/gestionnaires/gestionnaires.module.ts`
|
||||||
|
|
||||||
|
Le contrôleur utilise `AuthGuard`, qui dépend de `JwtService` fourni par `AuthModule`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Users]),
|
||||||
|
AuthModule // <--- AJOUTER ICI
|
||||||
|
],
|
||||||
|
controllers: [GestionnairesController],
|
||||||
|
providers: [GestionnairesService],
|
||||||
|
})
|
||||||
|
export class GestionnairesModule { }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Après application du correctif
|
||||||
|
Une fois ces modifications backend effectuées :
|
||||||
|
1. Redémarrer le serveur backend.
|
||||||
|
2. Modifier le frontend (`frontend/lib/services/user_service.dart`) pour utiliser à nouveau l'endpoint dédié :
|
||||||
|
```dart
|
||||||
|
static Future<List<AppUser>> getGestionnaires() async {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
# 📐 Règles de Codage - Projet P'titsPas
|
||||||
|
|
||||||
|
**Version** : 1.0
|
||||||
|
**Date** : 1er Décembre 2025
|
||||||
|
**Statut** : ✅ Actif
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Langue du Code
|
||||||
|
|
||||||
|
### Principe Général
|
||||||
|
**Tout le code doit être écrit en FRANÇAIS**, sauf les termes techniques qui restent en **ANGLAIS**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Ce qui doit être en FRANÇAIS
|
||||||
|
|
||||||
|
### 1. Noms de variables
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
const utilisateurConnecte = await this.trouverUtilisateur(id);
|
||||||
|
const enfantsEnregistres = [];
|
||||||
|
const tokenCreationMotDePasse = crypto.randomUUID();
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
const loggedUser = await this.findUser(id);
|
||||||
|
const savedChildren = [];
|
||||||
|
const passwordCreationToken = crypto.randomUUID();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Noms de fonctions/méthodes
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
async inscrireParentComplet(dto: DtoInscriptionParentComplet) { }
|
||||||
|
async creerGestionnaire(dto: DtoCreationGestionnaire) { }
|
||||||
|
async validerCompte(idUtilisateur: string) { }
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
async registerParentComplete(dto: RegisterParentCompleteDto) { }
|
||||||
|
async createManager(dto: CreateManagerDto) { }
|
||||||
|
async validateAccount(userId: string) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Noms de classes/interfaces/types
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
export class DtoInscriptionParentComplet { }
|
||||||
|
export class ServiceAuthentification { }
|
||||||
|
export interface OptionsConfiguration { }
|
||||||
|
export type StatutUtilisateur = 'actif' | 'en_attente' | 'suspendu';
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
export class RegisterParentCompleteDto { }
|
||||||
|
export class AuthService { }
|
||||||
|
export interface ConfigOptions { }
|
||||||
|
export type UserStatus = 'active' | 'pending' | 'suspended';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Noms de fichiers
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
inscription-parent-complet.dto.ts
|
||||||
|
service-authentification.ts
|
||||||
|
entite-utilisateurs.ts
|
||||||
|
controleur-configuration.ts
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
register-parent-complete.dto.ts
|
||||||
|
auth.service.ts
|
||||||
|
users.entity.ts
|
||||||
|
config.controller.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Propriétés d'entités/DTOs
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
export class Enfants {
|
||||||
|
@Column({ name: 'prenom' })
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@Column({ name: 'date_naissance' })
|
||||||
|
dateNaissance: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'consentement_photo' })
|
||||||
|
consentementPhoto: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
export class Children {
|
||||||
|
@Column({ name: 'first_name' })
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Column({ name: 'birth_date' })
|
||||||
|
birthDate: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'consent_photo' })
|
||||||
|
consentPhoto: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Commentaires
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
// Créer Parent 1 + Parent 2 (si existe) + entités parents
|
||||||
|
// Vérifier que l'email n'existe pas déjà
|
||||||
|
// Transaction : Créer utilisateur + entité métier
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
// Create Parent 1 + Parent 2 (if exists) + parent entities
|
||||||
|
// Check if email already exists
|
||||||
|
// Transaction: Create user + business entity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Messages d'erreur/succès
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
return { message: 'Inscription réussie. Votre dossier est en attente de validation.' };
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
throw new ConflictException('An account with this email already exists');
|
||||||
|
return { message: 'Registration successful. Your application is pending validation.' };
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Logs
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
this.logger.log('📦 Chargement de 16 configurations en cache');
|
||||||
|
this.logger.error('Erreur lors de la création du parent');
|
||||||
|
|
||||||
|
// ❌ MAUVAIS
|
||||||
|
this.logger.log('📦 Loading 16 configurations in cache');
|
||||||
|
this.logger.error('Error creating parent');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Ce qui RESTE en ANGLAIS (Termes Techniques)
|
||||||
|
|
||||||
|
### 1. Patterns de conception
|
||||||
|
- `singleton`
|
||||||
|
- `factory`
|
||||||
|
- `repository`
|
||||||
|
- `observer`
|
||||||
|
- `decorator`
|
||||||
|
|
||||||
|
### 2. Architecture/Framework
|
||||||
|
- `backend` / `frontend`
|
||||||
|
- `controller`
|
||||||
|
- `service`
|
||||||
|
- `middleware`
|
||||||
|
- `guard`
|
||||||
|
- `interceptor`
|
||||||
|
- `pipe`
|
||||||
|
- `filter`
|
||||||
|
- `module`
|
||||||
|
- `provider`
|
||||||
|
|
||||||
|
### 3. Concepts techniques
|
||||||
|
- `entity` (TypeORM)
|
||||||
|
- `DTO` (Data Transfer Object)
|
||||||
|
- `API` / `endpoint`
|
||||||
|
- `token` (JWT)
|
||||||
|
- `hash` (bcrypt)
|
||||||
|
- `cache`
|
||||||
|
- `query`
|
||||||
|
- `transaction`
|
||||||
|
- `migration`
|
||||||
|
- `seed`
|
||||||
|
|
||||||
|
### 4. Bibliothèques/Technologies
|
||||||
|
- `NestJS`
|
||||||
|
- `TypeORM`
|
||||||
|
- `PostgreSQL`
|
||||||
|
- `Docker`
|
||||||
|
- `Git`
|
||||||
|
- `JWT`
|
||||||
|
- `bcrypt`
|
||||||
|
- `Multer`
|
||||||
|
- `Nodemailer`
|
||||||
|
|
||||||
|
### 5. Mots-clés TypeScript/JavaScript
|
||||||
|
- `async` / `await`
|
||||||
|
- `const` / `let` / `var`
|
||||||
|
- `function`
|
||||||
|
- `class`
|
||||||
|
- `interface`
|
||||||
|
- `type`
|
||||||
|
- `enum`
|
||||||
|
- `import` / `export`
|
||||||
|
- `return`
|
||||||
|
- `throw`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Exemples Complets
|
||||||
|
|
||||||
|
### Exemple 1 : Service d'authentification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceAuthentification {
|
||||||
|
constructor(
|
||||||
|
private readonly serviceUtilisateurs: ServiceUtilisateurs,
|
||||||
|
private readonly serviceJwt: JwtService,
|
||||||
|
@InjectRepository(Utilisateurs)
|
||||||
|
private readonly depotUtilisateurs: Repository<Utilisateurs>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async inscrireParentComplet(dto: DtoInscriptionParentComplet) {
|
||||||
|
// Vérifier que l'email n'existe pas
|
||||||
|
const existe = await this.serviceUtilisateurs.trouverParEmail(dto.email);
|
||||||
|
if (existe) {
|
||||||
|
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer le token de création de mot de passe
|
||||||
|
const tokenCreationMdp = crypto.randomUUID();
|
||||||
|
const dateExpiration = new Date();
|
||||||
|
dateExpiration.setDate(dateExpiration.getDate() + 7);
|
||||||
|
|
||||||
|
// Transaction : Créer parent + enfants
|
||||||
|
const resultat = await this.depotUtilisateurs.manager.transaction(async (manager) => {
|
||||||
|
const parent1 = new Utilisateurs();
|
||||||
|
parent1.email = dto.email;
|
||||||
|
parent1.prenom = dto.prenom;
|
||||||
|
parent1.nom = dto.nom;
|
||||||
|
parent1.tokenCreationMdp = tokenCreationMdp;
|
||||||
|
|
||||||
|
const parentEnregistre = await manager.save(Utilisateurs, parent1);
|
||||||
|
|
||||||
|
return { parent: parentEnregistre, token: tokenCreationMdp };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Inscription réussie. Votre dossier est en attente de validation.',
|
||||||
|
idParent: resultat.parent.id,
|
||||||
|
statut: 'en_attente',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple 2 : Entité Enfants
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ BON
|
||||||
|
@Entity('enfants')
|
||||||
|
export class Enfants {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'prenom', length: 100 })
|
||||||
|
prenom: string;
|
||||||
|
|
||||||
|
@Column({ name: 'nom', length: 100 })
|
||||||
|
nom: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: TypeGenre,
|
||||||
|
name: 'genre'
|
||||||
|
})
|
||||||
|
genre: TypeGenre;
|
||||||
|
|
||||||
|
@Column({ type: 'date', name: 'date_naissance', nullable: true })
|
||||||
|
dateNaissance?: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'date', name: 'date_prevue_naissance', nullable: true })
|
||||||
|
datePrevueNaissance?: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'photo_url', type: 'text', nullable: true })
|
||||||
|
photoUrl?: string;
|
||||||
|
|
||||||
|
@Column({ name: 'consentement_photo', type: 'boolean', default: false })
|
||||||
|
consentementPhoto: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'est_multiple', type: 'boolean', default: false })
|
||||||
|
estMultiple: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: StatutEnfantType,
|
||||||
|
name: 'statut'
|
||||||
|
})
|
||||||
|
statut: StatutEnfantType;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Migration Progressive
|
||||||
|
|
||||||
|
### Stratégie
|
||||||
|
1. ✅ **Nouveau code** : Appliquer la règle immédiatement
|
||||||
|
2. ⏳ **Code existant** : Migrer progressivement lors des modifications
|
||||||
|
3. ❌ **Ne PAS refactoriser** tout le code d'un coup
|
||||||
|
|
||||||
|
### Priorités de migration
|
||||||
|
1. **Haute priorité** : Nouveaux fichiers, nouvelles fonctionnalités
|
||||||
|
2. **Moyenne priorité** : Fichiers modifiés fréquemment
|
||||||
|
3. **Basse priorité** : Code stable non modifié
|
||||||
|
|
||||||
|
### Exemple de migration progressive
|
||||||
|
```typescript
|
||||||
|
// Avant (ancien code - OK pour l'instant)
|
||||||
|
export class Children { }
|
||||||
|
|
||||||
|
// Après modification (nouveau code - appliquer la règle)
|
||||||
|
export class Enfants { }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 Exceptions
|
||||||
|
|
||||||
|
### Cas où l'anglais est toléré
|
||||||
|
1. **Noms de colonnes en BDD** : Si la BDD existe déjà (ex: `first_name` en BDD → `prenom` en TypeScript)
|
||||||
|
2. **APIs externes** : Noms imposés par des bibliothèques tierces
|
||||||
|
3. **Standards** : `id`, `uuid`, `url`, `email`, `password` (termes universels)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist Avant Commit
|
||||||
|
|
||||||
|
- [ ] Noms de variables en français
|
||||||
|
- [ ] Noms de fonctions/méthodes en français
|
||||||
|
- [ ] Noms de classes/interfaces en français
|
||||||
|
- [ ] Noms de fichiers en français
|
||||||
|
- [ ] Propriétés d'entités/DTOs en français
|
||||||
|
- [ ] Commentaires en français
|
||||||
|
- [ ] Messages d'erreur/succès en français
|
||||||
|
- [ ] Termes techniques restent en anglais
|
||||||
|
- [ ] Pas de `console.log` (utiliser `this.logger`)
|
||||||
|
- [ ] Pas de code commenté
|
||||||
|
- [ ] Types TypeScript corrects (pas de `any`)
|
||||||
|
- [ ] Imports propres (pas d'imports inutilisés)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dernière mise à jour** : 1er Décembre 2025
|
||||||
|
**Auteur** : Équipe P'titsPas
|
||||||
|
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
# Architecture Technique - P'titsPas
|
||||||
|
## Guide d'Infrastructure et de Déploiement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
P'titsPas est une application de gestion de garde d'enfants pour les collectivités locales, basée sur une architecture **client-serveur moderne** :
|
||||||
|
|
||||||
|
- **Frontend** : Application web Flutter (Single Page Application)
|
||||||
|
- **Backend** : API REST Node.js/Express avec TypeScript
|
||||||
|
- **Base de données** : PostgreSQL avec ORM Prisma
|
||||||
|
- **Architecture** : Séparation claire frontend/backend avec API REST
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prérequis Serveur
|
||||||
|
|
||||||
|
### Environnement d'exécution
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- **Node.js** : Version 18+ (LTS recommandée : 18.19.0+)
|
||||||
|
- **npm** : Version 9+
|
||||||
|
- **TypeScript** : Inclus dans les dépendances du projet
|
||||||
|
|
||||||
|
#### Base de données
|
||||||
|
- **PostgreSQL** : Version 15+
|
||||||
|
- **Extensions** : UUID (pour les clés primaires)
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- **Serveur web statique** : nginx, Apache, ou similaire
|
||||||
|
- **Flutter Web** : Compilation en JavaScript (pas de prérequis runtime)
|
||||||
|
|
||||||
|
### Ressources recommandées
|
||||||
|
|
||||||
|
#### Environnement de développement
|
||||||
|
- **RAM** : 4GB minimum
|
||||||
|
- **CPU** : 2 vCPU
|
||||||
|
- **Storage** : 10GB
|
||||||
|
|
||||||
|
#### Environnement de production
|
||||||
|
- **RAM** : 8GB recommandé (4GB minimum)
|
||||||
|
- **CPU** : 4 vCPU recommandé (2 vCPU minimum)
|
||||||
|
- **Storage** : 50GB minimum (base de données + logs + backups)
|
||||||
|
- **Réseau** :
|
||||||
|
- Port 3000 : API Backend (interne)
|
||||||
|
- Port 80/443 : Web (externe)
|
||||||
|
- Port 5432 : PostgreSQL (interne uniquement)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack Technique Détaillée
|
||||||
|
|
||||||
|
### Backend (API)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"runtime": "Node.js 18+",
|
||||||
|
"language": "TypeScript",
|
||||||
|
"framework": "Express.js 4.18+",
|
||||||
|
"orm": "Prisma 6.7+",
|
||||||
|
"database_client": "@prisma/client",
|
||||||
|
"security": [
|
||||||
|
"helmet (sécurité headers)",
|
||||||
|
"cors (CORS policy)",
|
||||||
|
"bcrypt (hashage mots de passe)",
|
||||||
|
"jsonwebtoken (JWT auth)"
|
||||||
|
],
|
||||||
|
"logging": "morgan",
|
||||||
|
"validation": "@nestjs/common"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (Web)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"framework": "Flutter 3.2.6+",
|
||||||
|
"language": "Dart 3.0+",
|
||||||
|
"compilation": "JavaScript (Flutter Web)",
|
||||||
|
"navigation": "go_router 13.2+",
|
||||||
|
"state_management": "provider 6.1+",
|
||||||
|
"ui_framework": "Material Design",
|
||||||
|
"fonts": "Google Fonts",
|
||||||
|
"http_client": "http 1.2+"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base de données
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Structure PostgreSQL
|
||||||
|
-- Tables principales :
|
||||||
|
-- - Parent (utilisateurs parents)
|
||||||
|
-- - Child (enfants)
|
||||||
|
-- - Contract (contrats de garde)
|
||||||
|
-- - Admin (administrateurs)
|
||||||
|
-- - Theme (thèmes interface)
|
||||||
|
-- - AppSettings (paramètres app)
|
||||||
|
|
||||||
|
-- Types de données :
|
||||||
|
-- - UUID pour toutes les clés primaires
|
||||||
|
-- - Timestamps automatiques (createdAt, updatedAt)
|
||||||
|
-- - Enums pour les statuts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation et Configuration
|
||||||
|
|
||||||
|
### 1. Prérequis système
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y nodejs npm postgresql postgresql-contrib nginx
|
||||||
|
|
||||||
|
# Vérification versions
|
||||||
|
node --version # >= 18.0.0
|
||||||
|
npm --version # >= 9.0.0
|
||||||
|
psql --version # >= 15.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configuration base de données
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Se connecter en tant que postgres
|
||||||
|
sudo -u postgres psql
|
||||||
|
|
||||||
|
-- Créer la base de données et l'utilisateur
|
||||||
|
CREATE DATABASE ptitspas;
|
||||||
|
CREATE USER ptitspas_user WITH PASSWORD 'secure_password_here';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE ptitspas TO ptitspas_user;
|
||||||
|
ALTER USER ptitspas_user CREATEDB; -- Pour les migrations
|
||||||
|
|
||||||
|
-- Quitter
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Installation Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Installation des dépendances
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Configuration environnement
|
||||||
|
cp .env.example .env
|
||||||
|
# Éditer .env avec vos paramètres
|
||||||
|
|
||||||
|
# Génération du client Prisma et migrations
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma migrate deploy
|
||||||
|
|
||||||
|
# Initialisation admin (optionnel)
|
||||||
|
npm run init-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Installation Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Installation des dépendances Flutter
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Build pour production
|
||||||
|
flutter build web --release
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables d'Environnement
|
||||||
|
|
||||||
|
### Backend (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Base de données
|
||||||
|
DATABASE_URL="postgresql://ptitspas_user:secure_password_here@localhost:5432/ptitspas"
|
||||||
|
|
||||||
|
# Sécurité
|
||||||
|
JWT_SECRET="your-super-secret-jwt-key-minimum-32-characters"
|
||||||
|
JWT_EXPIRES_IN="24h"
|
||||||
|
|
||||||
|
# Serveur
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Optionnel
|
||||||
|
CORS_ORIGIN="https://ptitspas.yourdomain.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
### Option 1 : Déploiement classique (recommandé)
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# Installation production
|
||||||
|
npm ci --only=production
|
||||||
|
|
||||||
|
# Build TypeScript
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Démarrage (avec PM2 recommandé)
|
||||||
|
npm install -g pm2
|
||||||
|
pm2 start dist/index.js --name "ptitspas-api"
|
||||||
|
pm2 startup
|
||||||
|
pm2 save
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Build production
|
||||||
|
flutter build web --release
|
||||||
|
|
||||||
|
# Copier vers serveur web
|
||||||
|
sudo cp -r build/web/* /var/www/ptitspas/
|
||||||
|
sudo chown -R www-data:www-data /var/www/ptitspas/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2 : Conteneurisation Docker
|
||||||
|
|
||||||
|
#### Dockerfile Backend
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Créer répertoire app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copier package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY prisma ./prisma/
|
||||||
|
|
||||||
|
# Installer dépendances
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copier code source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Générer client Prisma
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
|
# Exposer port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Variables d'environnement
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Commande démarrage
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dockerfile Frontend
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copier build Flutter
|
||||||
|
COPY build/web /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Configuration nginx
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Exposer port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Démarrage nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ptitspas
|
||||||
|
POSTGRES_USER: ptitspas_user
|
||||||
|
POSTGRES_PASSWORD: secure_password_here
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://ptitspas_user:secure_password_here@postgres:5432/ptitspas
|
||||||
|
JWT_SECRET: your-super-secret-jwt-key
|
||||||
|
NODE_ENV: production
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Nginx
|
||||||
|
|
||||||
|
### Configuration complète
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ptitspas.yourdomain.com;
|
||||||
|
|
||||||
|
# Redirection HTTPS
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name ptitspas.yourdomain.com;
|
||||||
|
|
||||||
|
# Certificats SSL
|
||||||
|
ssl_certificate /etc/letsencrypt/live/ptitspas.yourdomain.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/ptitspas.yourdomain.com/privkey.pem;
|
||||||
|
|
||||||
|
# Configuration SSL sécurisée
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
# Frontend statique (Flutter Web)
|
||||||
|
location / {
|
||||||
|
root /var/www/ptitspas;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
# Cache statique
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Backend
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
access_log /var/log/nginx/ptitspas_access.log;
|
||||||
|
error_log /var/log/nginx/ptitspas_error.log;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Obligatoire
|
||||||
|
|
||||||
|
1. **HTTPS avec certificat SSL**
|
||||||
|
```bash
|
||||||
|
# Installation Certbot
|
||||||
|
sudo apt install certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# Génération certificat
|
||||||
|
sudo certbot --nginx -d ptitspas.yourdomain.com
|
||||||
|
|
||||||
|
# Renouvellement automatique
|
||||||
|
sudo crontab -e
|
||||||
|
# Ajouter : 0 12 * * * /usr/bin/certbot renew --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Firewall**
|
||||||
|
```bash
|
||||||
|
# UFW (Ubuntu)
|
||||||
|
sudo ufw allow 22 # SSH
|
||||||
|
sudo ufw allow 80 # HTTP
|
||||||
|
sudo ufw allow 443 # HTTPS
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Base de données sécurisée**
|
||||||
|
```bash
|
||||||
|
# PostgreSQL : accès local uniquement
|
||||||
|
sudo nano /etc/postgresql/15/main/postgresql.conf
|
||||||
|
# Commenter : #listen_addresses = 'localhost'
|
||||||
|
|
||||||
|
sudo nano /etc/postgresql/15/main/pg_hba.conf
|
||||||
|
# Vérifier que seules les connexions locales sont autorisées
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Backup automatique**
|
||||||
|
```bash
|
||||||
|
# Script backup
|
||||||
|
#!/bin/bash
|
||||||
|
BACKUP_DIR="/var/backups/ptitspas"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
pg_dump -U ptitspas_user -h localhost ptitspas > $BACKUP_DIR/ptitspas_$DATE.sql
|
||||||
|
|
||||||
|
# Nettoyer les backups > 30 jours
|
||||||
|
find $BACKUP_DIR -name "*.sql" -mtime +30 -delete
|
||||||
|
|
||||||
|
# Crontab : tous les jours à 2h
|
||||||
|
# 0 2 * * * /path/to/backup_script.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommandé
|
||||||
|
|
||||||
|
1. **Fail2Ban** (protection brute force)
|
||||||
|
```bash
|
||||||
|
sudo apt install fail2ban
|
||||||
|
sudo systemctl enable fail2ban
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Monitoring des logs**
|
||||||
|
```bash
|
||||||
|
# Logrotate pour éviter les gros fichiers
|
||||||
|
sudo nano /etc/logrotate.d/ptitspas
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Updates automatiques**
|
||||||
|
```bash
|
||||||
|
sudo apt install unattended-upgrades
|
||||||
|
sudo dpkg-reconfigure unattended-upgrades
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes de Gestion
|
||||||
|
|
||||||
|
### Démarrage des services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend (développement)
|
||||||
|
cd backend && npm run dev
|
||||||
|
|
||||||
|
# Backend (production avec PM2)
|
||||||
|
pm2 start ptitspas-api
|
||||||
|
pm2 status
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
sudo systemctl start postgresql
|
||||||
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
|
# Serveur web
|
||||||
|
sudo systemctl start nginx
|
||||||
|
sudo systemctl status nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migrations base de données
|
||||||
|
cd backend
|
||||||
|
npx prisma migrate deploy
|
||||||
|
|
||||||
|
# Logs Backend
|
||||||
|
pm2 logs ptitspas-api
|
||||||
|
|
||||||
|
# Logs Nginx
|
||||||
|
sudo tail -f /var/log/nginx/ptitspas_access.log
|
||||||
|
sudo tail -f /var/log/nginx/ptitspas_error.log
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
pm2 restart ptitspas-api
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring et Healthcheck
|
||||||
|
|
||||||
|
### Endpoints de santé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API Health (à implémenter)
|
||||||
|
curl https://ptitspas.yourdomain.com/api/health
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
psql -h localhost -U ptitspas_user -d ptitspas -c "SELECT 1;"
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
curl -I https://ptitspas.yourdomain.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs à surveiller
|
||||||
|
|
||||||
|
1. **Backend** : Via PM2 ou logs applicatifs
|
||||||
|
2. **PostgreSQL** : `/var/log/postgresql/postgresql-15-main.log`
|
||||||
|
3. **Nginx** : `/var/log/nginx/ptitspas_*.log`
|
||||||
|
4. **Système** : `/var/log/syslog`
|
||||||
|
|
||||||
|
### Métriques importantes
|
||||||
|
|
||||||
|
- **CPU/RAM** : Usage serveur
|
||||||
|
- **Espace disque** : Base de données et logs
|
||||||
|
- **Connexions DB** : Nombre de connexions actives
|
||||||
|
- **Temps de réponse** : API et frontend
|
||||||
|
- **Erreurs 5xx** : Erreurs serveur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problèmes courants
|
||||||
|
|
||||||
|
1. **Backend ne démarre pas**
|
||||||
|
```bash
|
||||||
|
# Vérifier variables d'environnement
|
||||||
|
cd backend && cat .env
|
||||||
|
|
||||||
|
# Vérifier connexion DB
|
||||||
|
npx prisma db pull
|
||||||
|
|
||||||
|
# Logs détaillés
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Frontend ne s'affiche pas**
|
||||||
|
```bash
|
||||||
|
# Vérifier build
|
||||||
|
cd frontend && flutter build web
|
||||||
|
|
||||||
|
# Vérifier nginx
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Erreurs base de données**
|
||||||
|
```bash
|
||||||
|
# Vérifier statut PostgreSQL
|
||||||
|
sudo systemctl status postgresql
|
||||||
|
|
||||||
|
# Vérifier connexions
|
||||||
|
sudo -u postgres psql -c "SELECT * FROM pg_stat_activity;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Évolutivité
|
||||||
|
|
||||||
|
### Optimisations possibles
|
||||||
|
|
||||||
|
1. **Cache Redis** : Pour les sessions et cache applicatif
|
||||||
|
2. **CDN** : Pour les assets statiques
|
||||||
|
3. **Load Balancer** : Pour haute disponibilité
|
||||||
|
4. **Clustering** : Multiple instances Node.js
|
||||||
|
5. **Database replication** : Master/Slave PostgreSQL
|
||||||
|
|
||||||
|
### Monitoring avancé
|
||||||
|
|
||||||
|
- **Prometheus + Grafana** : Métriques système et applicatif
|
||||||
|
- **ELK Stack** : Centralisation des logs
|
||||||
|
- **Uptime monitoring** : Surveillance externe
|
||||||
|
|
||||||
|
Cette architecture est conçue pour être **scalable**, **maintenable** et **sécurisée** pour un environnement de production professionnel.
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# 🎯 BRIEFING - Développement Frontend P'titsPas
|
||||||
|
|
||||||
|
## Projet
|
||||||
|
Application de gestion de crèches/assistantes maternelles - Frontend Flutter Web
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accès Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cloner le repo
|
||||||
|
git clone https://git.ptits-pas.fr/jmartin/petitspas.git
|
||||||
|
cd petitspas/frontend
|
||||||
|
|
||||||
|
# Identifiants Gitea (si demandé)
|
||||||
|
# User: jmartin
|
||||||
|
# Token: giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc
|
||||||
|
|
||||||
|
# API Gitea pour consulter les tickets frontend
|
||||||
|
curl -H "Authorization: token giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq '.[] | select(.labels[].name == "frontend") | {number, title}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend API (déjà déployé)
|
||||||
|
|
||||||
|
**URL de base** : `https://app.ptits-pas.fr/api/v1/`
|
||||||
|
|
||||||
|
### Endpoints clés disponibles
|
||||||
|
|
||||||
|
| Endpoint | Méthode | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `/configuration/setup/status` | GET | Vérifie si setup terminé (`setupCompleted: true/false`) |
|
||||||
|
| `/auth/login` | POST | Connexion (`{email, password}`) |
|
||||||
|
| `/auth/me` | GET | Profil utilisateur (inclut `changement_mdp_obligatoire`) |
|
||||||
|
| `/auth/change-password-required` | POST | Changement MDP obligatoire |
|
||||||
|
| `/auth/refresh` | POST | Rafraîchir les tokens |
|
||||||
|
| `/configuration` | GET | Liste toutes les configs (super_admin) |
|
||||||
|
| `/configuration/:category` | GET | Configs par catégorie (email, app, security) |
|
||||||
|
| `/configuration/bulk` | PATCH | Mise à jour multiple des configs |
|
||||||
|
| `/configuration/test-smtp` | POST | Test connexion SMTP |
|
||||||
|
| `/configuration/setup/complete` | POST | Marquer setup comme terminé |
|
||||||
|
| `/gestionnaires` | POST | Créer gestionnaire (super_admin only) |
|
||||||
|
| `/gestionnaires` | GET | Liste des gestionnaires |
|
||||||
|
| `/documents-legaux/actifs` | GET | CGU et Privacy actifs |
|
||||||
|
|
||||||
|
### Compte test super_admin
|
||||||
|
|
||||||
|
```
|
||||||
|
Email: admin@ptits-pas.fr
|
||||||
|
MDP: 4dm1n1strateur
|
||||||
|
changement_mdp_obligatoire: true (première connexion)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tickets Frontend prioritaires
|
||||||
|
|
||||||
|
### Workflow d'initialisation (P1 - BLOQUANT)
|
||||||
|
|
||||||
|
| # | Ticket | Description | Effort |
|
||||||
|
|---|--------|-------------|--------|
|
||||||
|
| **#12** | Panneau Paramètres / Configuration | Une page avec 3 sections (Email, Personnalisation, Avancé) ; première config = affichage direct + navigation bloquée jusqu'à sauvegarde | 5h |
|
||||||
|
| **#13** | Intégration panneau au dashboard | Onglet Configuration, accès permanent, même interface | 1h |
|
||||||
|
| **#47** | Changement MDP Obligatoire | Modale bloquante après login si flag=true | 1-2h |
|
||||||
|
| **#35** | Création Gestionnaire | Formulaire création gestionnaire | 2-3h |
|
||||||
|
|
||||||
|
### Authentification (P3)
|
||||||
|
|
||||||
|
| # | Ticket | Description | Effort |
|
||||||
|
|---|--------|-------------|--------|
|
||||||
|
| **#43** | Écran Création MDP | Page `/create-password?token=xxx` | 2h |
|
||||||
|
| **#48** | Gestion Erreurs & Messages | Snackbars, intercepteur HTTP | 2h |
|
||||||
|
|
||||||
|
### Dashboard Gestionnaire (P3)
|
||||||
|
|
||||||
|
| # | Ticket | Description | Effort |
|
||||||
|
|---|--------|-------------|--------|
|
||||||
|
| **#44** | Structure Dashboard | Layout avec onglets Parents/AM | 2h |
|
||||||
|
| **#45** | Liste Parents | Liste des parents en attente | 2h |
|
||||||
|
| **#46** | Liste AM | Liste des AM en attente | 2h |
|
||||||
|
|
||||||
|
### Inscription (P3)
|
||||||
|
|
||||||
|
| # | Ticket | Description | Effort |
|
||||||
|
|---|--------|-------------|--------|
|
||||||
|
| **#38** | Inscription Parent Étape 3 | Enfants | 3h |
|
||||||
|
| **#39** | Inscription Parent Étapes 4-6 | Finalisation | 4h |
|
||||||
|
| **#40-42** | Inscription AM | 3 panneaux | 6h |
|
||||||
|
| **#50** | Affichage CGU dynamique | Version lors inscription | 1h |
|
||||||
|
|
||||||
|
### Admin (P3)
|
||||||
|
|
||||||
|
| # | Ticket | Description | Effort |
|
||||||
|
|---|--------|-------------|--------|
|
||||||
|
| **#49** | Gestion Documents Légaux | Upload/activation CGU/Privacy | 4h |
|
||||||
|
| **#51** | Écran Logs Admin | Optionnel v1.1 | 4h |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Règles de codage
|
||||||
|
|
||||||
|
### Langue
|
||||||
|
|
||||||
|
- **Français** pour : commentaires, descriptions, noms métier
|
||||||
|
- **Anglais** pour : termes techniques (controller, service, widget, singleton, etc.)
|
||||||
|
|
||||||
|
### Exemples
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ✅ BON - Noms métier en français, technique en anglais
|
||||||
|
class EcranConnexion extends StatefulWidget { }
|
||||||
|
final utilisateurConnecte = authService.recupererUtilisateur();
|
||||||
|
final gestionnaire = await gestionnaireService.creer(donnees);
|
||||||
|
|
||||||
|
// Widget avec nom métier
|
||||||
|
class CarteEnfant extends StatelessWidget { }
|
||||||
|
class FormulaireInscriptionParent extends StatefulWidget { }
|
||||||
|
|
||||||
|
// ❌ MAUVAIS - Tout en anglais
|
||||||
|
class LoginScreen extends StatefulWidget { }
|
||||||
|
final loggedInUser = authService.getUser();
|
||||||
|
class ChildCard extends StatelessWidget { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Termes à garder en anglais
|
||||||
|
- Widget, StatefulWidget, StatelessWidget
|
||||||
|
- Controller, Service, Provider
|
||||||
|
- Singleton, Factory, Builder
|
||||||
|
- async, await, Future, Stream
|
||||||
|
- Navigator, Router, Route
|
||||||
|
- API, HTTP, JSON, DTO
|
||||||
|
|
||||||
|
### Termes métier en français
|
||||||
|
- Utilisateur, Parent, Enfant, Gestionnaire, Administrateur
|
||||||
|
- AssistanteMaternelle (ou AM)
|
||||||
|
- Inscription, Connexion, Déconnexion
|
||||||
|
- Configuration, Paramètres
|
||||||
|
- Validation, Refus, EnAttente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure frontend existante
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/lib/
|
||||||
|
├── config/
|
||||||
|
│ └── env.dart # Configuration environnement
|
||||||
|
├── controllers/
|
||||||
|
│ └── parent_dashboard_controller.dart
|
||||||
|
├── models/
|
||||||
|
│ ├── user_registration_data.dart
|
||||||
|
│ ├── parent_user_registration_data.dart
|
||||||
|
│ └── am_user_registration_data.dart
|
||||||
|
├── navigation/
|
||||||
|
│ └── app_router.dart # Routes de l'application
|
||||||
|
├── screens/
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── login_screen.dart ✅ Fait
|
||||||
|
│ │ ├── register_choice_screen.dart ✅ Fait
|
||||||
|
│ │ ├── parent_register_step1-5_screen.dart ✅ Fait
|
||||||
|
│ │ ├── am/ ✅ 4 étapes faites
|
||||||
|
│ │ └── parent/ ✅ 5 étapes faites
|
||||||
|
│ ├── administrateurs/
|
||||||
|
│ │ ├── admin_dashboardScreen.dart
|
||||||
|
│ │ └── creation/
|
||||||
|
│ │ └── gestionnaires_create.dart ⚠️ Placeholder
|
||||||
|
│ ├── home/
|
||||||
|
│ │ ├── home_screen.dart
|
||||||
|
│ │ └── parent_screen/
|
||||||
|
│ └── legal/
|
||||||
|
├── services/
|
||||||
|
│ ├── auth_service.dart
|
||||||
|
│ ├── bug_report_service.dart
|
||||||
|
│ ├── dashboardService.dart
|
||||||
|
│ ├── login_navigation_service.dart
|
||||||
|
│ └── api/
|
||||||
|
│ ├── api_config.dart
|
||||||
|
│ └── tokenService.dart
|
||||||
|
├── utils/
|
||||||
|
│ └── data_generator.dart
|
||||||
|
└── widgets/
|
||||||
|
├── admin/
|
||||||
|
│ ├── dashboard_admin.dart
|
||||||
|
│ ├── gestionnaire_card.dart
|
||||||
|
│ ├── gestionnaire_management_widget.dart
|
||||||
|
│ └── parent_managmant_widget.dart
|
||||||
|
├── dashbord_parent/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Créer une branche feature
|
||||||
|
git checkout develop
|
||||||
|
git pull origin develop
|
||||||
|
git checkout -b feature/XX-nom-ticket
|
||||||
|
|
||||||
|
# 2. Développer et commiter
|
||||||
|
git add .
|
||||||
|
git commit -m "feat(#XX): Description courte"
|
||||||
|
|
||||||
|
# 3. Pousser et créer PR
|
||||||
|
git push -u origin feature/XX-nom-ticket
|
||||||
|
|
||||||
|
# 4. Créer PR vers master via Gitea ou API
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"feat(#XX): Titre","body":"Description\n\nCloses #XX","head":"feature/XX-nom-ticket","base":"master"}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commandes Flutter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installation des dépendances
|
||||||
|
cd frontend
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# Lancer en mode développement
|
||||||
|
flutter run -d chrome
|
||||||
|
# ou
|
||||||
|
flutter run -d windows
|
||||||
|
# ou
|
||||||
|
flutter run -d macos
|
||||||
|
|
||||||
|
# Build web
|
||||||
|
flutter build web
|
||||||
|
|
||||||
|
# Analyser le code
|
||||||
|
flutter analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers de configuration importants
|
||||||
|
|
||||||
|
### `lib/config/env.dart`
|
||||||
|
```dart
|
||||||
|
class Env {
|
||||||
|
static const String apiUrl = 'https://app.ptits-pas.fr/api/v1';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `lib/services/api/api_config.dart`
|
||||||
|
Configuration des appels API avec intercepteurs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommandation de démarrage
|
||||||
|
|
||||||
|
1. **Ticket #47** - Modale changement MDP obligatoire (simple, rapide)
|
||||||
|
2. **Tickets #12 + #13** - Panneau Paramètres (3 sections) : première config + onglet Configuration dans le dashboard
|
||||||
|
3. **Ticket #35** - Formulaire création gestionnaire
|
||||||
|
|
||||||
|
Ces tickets complètent le **workflow d'initialisation** !
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : 9 février 2026*
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Charte graphique · **P'titsPas**
|
||||||
|
*Version 1.0 – avril 2025*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Essentiel de la marque
|
||||||
|
| Élément | Raison d'être |
|
||||||
|
|---------|---------------|
|
||||||
|
| **Nom** | *P'titsPas* · évoque le cheminement serein des 0-3 ans |
|
||||||
|
| **Signature** | « Grandir pas à pas, sereinement » |
|
||||||
|
| **Valeurs** | Bienveillance · Transparence · Simplicité · Modernité |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Logos officiels
|
||||||
|
|
||||||
|
| Variante | Aperçu | Usage |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| **Principal** |  | Headers, back-office, print A4+ |
|
||||||
|
| **Icône** |  | Favicon, PWA, app mobile |
|
||||||
|
| **Monochrome** |  | Sérigraphie, tampon, textile sombre |
|
||||||
|
|
||||||
|
> **Zone de protection** : laisser au minimum l'équivalent d'une pierre pastel autour du logotype.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Palette de couleurs
|
||||||
|
|
||||||
|
| Nom | Hex | Rôle |
|
||||||
|
|-----|-----|------|
|
||||||
|
| Violet Pastel | `#c6a3d8` | Accent / information |
|
||||||
|
| Turquoise | `#8ad0c8` | Actions primaires |
|
||||||
|
| Jaune Doux | `#f2d269` | Avertissements légers |
|
||||||
|
| Corail | `#f4a28c` | États d'erreur ou badges « conflit » |
|
||||||
|
| Encre | `#2f2f2f` | Texte principal |
|
||||||
|
| Ivoire BG | `#fffef9` | Fond d'application & documents |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Typographies
|
||||||
|
|
||||||
|
| Contexte | Fonte | Chargement |
|
||||||
|
|----------|-------|------------|
|
||||||
|
| Titres & accroches | **Merienda 600** | Google Fonts |
|
||||||
|
| Texte courant | **Merriweather 300/400** | Google Fonts |
|
||||||
|
| UI compact | **Inter** (fallback système) | CDN |
|
||||||
|
|
||||||
|
```css
|
||||||
|
h1, h2 { font-family: "Merienda", cursive; }
|
||||||
|
body { font-family: "Merriweather", serif; }
|
||||||
|
```
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
# Évolutions du Cahier des Charges
|
||||||
|
|
||||||
|
Ce document liste les modifications à apporter au cahier des charges original pour le rendre conforme à l'application développée.
|
||||||
|
|
||||||
|
## 1. Gestion des Enfants
|
||||||
|
|
||||||
|
### Modifications à apporter dans la section "Création de compte parent"
|
||||||
|
|
||||||
|
#### Situation actuelle dans le CDC :
|
||||||
|
- Mentionne uniquement la collecte d'informations sur l'enfant
|
||||||
|
- Ne précise pas la possibilité d'ajouter plusieurs enfants
|
||||||
|
- Ne mentionne pas la gestion des naissances multiples
|
||||||
|
- Ne mentionne pas la gestion des enfants à naître
|
||||||
|
|
||||||
|
#### Modifications proposées :
|
||||||
|
|
||||||
|
Ajouter le paragraphe suivant après la description de la collecte d'informations sur l'enfant :
|
||||||
|
|
||||||
|
```
|
||||||
|
Les parents peuvent ajouter autant d'enfants que nécessaire. Pour chaque enfant, les informations suivantes sont collectées :
|
||||||
|
- Prénom
|
||||||
|
- Date de naissance (ou date prévue pour les enfants à naître)
|
||||||
|
- Photo (optionnelle)
|
||||||
|
- Consentement pour l'utilisation de la photo
|
||||||
|
- Indication si l'enfant fait partie d'une naissance multiple (jumeaux, triplés, etc.)
|
||||||
|
|
||||||
|
Les parents peuvent :
|
||||||
|
- Ajouter un nouvel enfant à tout moment
|
||||||
|
- Supprimer un enfant ajouté
|
||||||
|
- Modifier les informations d'un enfant existant
|
||||||
|
- Indiquer si l'enfant est à naître
|
||||||
|
- Indiquer si l'enfant fait partie d'une naissance multiple
|
||||||
|
- Donner ou retirer leur consentement pour l'utilisation de la photo de l'enfant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifications à apporter dans la section "Workflow de création de compte"
|
||||||
|
|
||||||
|
#### Situation actuelle dans le CDC :
|
||||||
|
- Étape 3 : "Collecte des informations sur l'enfant"
|
||||||
|
|
||||||
|
#### Modifications proposées :
|
||||||
|
|
||||||
|
Remplacer l'étape 3 par :
|
||||||
|
```
|
||||||
|
3. Collecte des informations sur les enfants
|
||||||
|
- Ajout d'un premier enfant
|
||||||
|
- Possibilité d'ajouter d'autres enfants
|
||||||
|
- Pour chaque enfant :
|
||||||
|
* Saisie du prénom
|
||||||
|
* Saisie de la date de naissance (ou date prévue)
|
||||||
|
* Option d'ajout d'une photo
|
||||||
|
* Option de consentement photo
|
||||||
|
* Indication si naissance multiple
|
||||||
|
* Indication si enfant à naître
|
||||||
|
- Possibilité de modifier ou supprimer un enfant
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Workflow de Création de Compte
|
||||||
|
|
||||||
|
### Modifications à apporter dans la section "Workflow de création de compte"
|
||||||
|
|
||||||
|
#### Situation actuelle dans le CDC :
|
||||||
|
- Ne précise pas l'ordre exact des étapes
|
||||||
|
- Ne mentionne pas le statut du compte après création
|
||||||
|
- Ne détaille pas le processus de validation
|
||||||
|
|
||||||
|
#### Modifications proposées :
|
||||||
|
|
||||||
|
Ajouter les précisions suivantes au workflow :
|
||||||
|
|
||||||
|
```
|
||||||
|
Le processus de création de compte suit l'ordre suivant :
|
||||||
|
1. Collecte des informations du premier parent
|
||||||
|
2. Option d'ajout d'un second parent
|
||||||
|
3. Collecte des informations sur les enfants
|
||||||
|
4. Description de la situation familiale
|
||||||
|
5. Acceptation des conditions générales
|
||||||
|
6. Résumé et validation finale
|
||||||
|
|
||||||
|
Après la validation :
|
||||||
|
- Le compte est créé avec le statut "en attente"
|
||||||
|
- Un gestionnaire doit valider le compte avant son activation
|
||||||
|
- Les parents reçoivent une notification de la création de leur compte
|
||||||
|
- Une notification est envoyée aux gestionnaires pour validation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Informations Supplémentaires
|
||||||
|
|
||||||
|
### Modifications à apporter dans la section "Création de compte parent"
|
||||||
|
|
||||||
|
#### Situation actuelle dans le CDC :
|
||||||
|
- Ne mentionne pas la possibilité de présentation personnelle
|
||||||
|
- Ne mentionne pas la gestion des photos
|
||||||
|
- Ne précise pas les statuts possibles du compte
|
||||||
|
|
||||||
|
#### Modifications proposées :
|
||||||
|
|
||||||
|
Ajouter les sections suivantes :
|
||||||
|
|
||||||
|
```
|
||||||
|
### Informations complémentaires
|
||||||
|
Le premier parent peut optionnellement ajouter une présentation personnelle pour décrire sa situation et ses attentes.
|
||||||
|
|
||||||
|
### Gestion des photos
|
||||||
|
Pour chaque enfant, les parents peuvent :
|
||||||
|
- Ajouter une photo
|
||||||
|
- Donner ou retirer leur consentement pour l'utilisation de la photo
|
||||||
|
- La photo est stockée de manière sécurisée
|
||||||
|
- Le consentement est enregistré avec date et heure
|
||||||
|
|
||||||
|
### Statut du compte
|
||||||
|
Les statuts possibles du compte sont :
|
||||||
|
- En attente : compte créé, en attente de validation
|
||||||
|
- Validé : compte activé par un gestionnaire
|
||||||
|
- Rejeté : compte refusé par un gestionnaire
|
||||||
|
- Suspendu : compte temporairement désactivé
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Validation et Sécurité
|
||||||
|
|
||||||
|
### Modifications à apporter dans la section "Validation"
|
||||||
|
|
||||||
|
#### Situation actuelle dans le CDC :
|
||||||
|
- Mentionne la validation par un gestionnaire
|
||||||
|
- Ne précise pas le processus de validation
|
||||||
|
- Ne mentionne pas les notifications
|
||||||
|
|
||||||
|
#### Modifications proposées :
|
||||||
|
|
||||||
|
Ajouter la section suivante :
|
||||||
|
|
||||||
|
```
|
||||||
|
### Processus de validation
|
||||||
|
1. Création du compte avec statut "en attente"
|
||||||
|
2. Notification automatique aux gestionnaires
|
||||||
|
3. Revue des informations par un gestionnaire
|
||||||
|
4. Décision de validation ou rejet
|
||||||
|
5. Notification aux parents de la décision
|
||||||
|
6. Activation ou rejet du compte selon la décision
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- Les parents reçoivent une notification à chaque changement de statut
|
||||||
|
- Les gestionnaires reçoivent une notification pour chaque nouveau compte
|
||||||
|
- Un historique des validations est conservé
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Initialisation de l'Application
|
||||||
|
|
||||||
|
### Ajout de l'administrateur par défaut
|
||||||
|
|
||||||
|
#### Situation actuelle dans le CDC :
|
||||||
|
- Ne mentionne pas l'existence d'un administrateur par défaut
|
||||||
|
- Ne précise pas les identifiants de connexion par défaut
|
||||||
|
|
||||||
|
#### Modifications proposées :
|
||||||
|
|
||||||
|
Ajouter la section suivante :
|
||||||
|
|
||||||
|
```
|
||||||
|
### Administrateur par défaut
|
||||||
|
Lors du premier démarrage de l'application, un compte administrateur est automatiquement créé avec les identifiants suivants :
|
||||||
|
- Email : administrateur@ptitspas.fr
|
||||||
|
- Mot de passe : password
|
||||||
|
|
||||||
|
Ce compte permet d'accéder à toutes les fonctionnalités administratives de l'application.
|
||||||
|
Le changement de mot de passe est obligatoire lors de la première connexion.
|
||||||
|
L'application doit forcer ce changement avant d'autoriser l'accès aux fonctionnalités administratives.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Changement de Nom de l'Application
|
||||||
|
|
||||||
|
### Situation actuelle dans le CDC :
|
||||||
|
- L'application est nommée "SuperNounou" dans tout le document
|
||||||
|
- Les références à l'application utilisent ce nom
|
||||||
|
|
||||||
|
### Modifications proposées :
|
||||||
|
|
||||||
|
Ajouter la section suivante :
|
||||||
|
|
||||||
|
```
|
||||||
|
### Changement de nom
|
||||||
|
L'application est renommée "P'titsPas" dans toute la documentation et l'interface utilisateur.
|
||||||
|
Ce changement implique :
|
||||||
|
- Mise à jour de toutes les références à "SuperNounou" dans le CDC
|
||||||
|
- Mise à jour des mentions légales
|
||||||
|
- Mise à jour de la documentation technique
|
||||||
|
- Mise à jour des interfaces utilisateur
|
||||||
|
- Mise à jour des messages système et notifications
|
||||||
|
- Mise à jour des adresses email (ex: support@ptitspas.fr)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact sur l'application :
|
||||||
|
- Mise à jour de tous les textes statiques dans le code
|
||||||
|
- Mise à jour des templates d'email
|
||||||
|
- Mise à jour des messages de notification
|
||||||
|
- Mise à jour de la documentation utilisateur
|
||||||
|
- Mise à jour des mentions légales et CGU
|
||||||
|
|
||||||
|
## Format de présentation
|
||||||
|
|
||||||
|
Pour chaque évolution identifiée, ce document suivra la structure suivante :
|
||||||
|
1. Section concernée dans le CDC
|
||||||
|
2. Situation actuelle
|
||||||
|
3. Modifications proposées
|
||||||
|
4. Impact sur l'application
|
||||||
|
|
||||||
|
## Prochaines évolutions à documenter
|
||||||
|
|
||||||
|
- [x] Ajouter d'autres évolutions identifiées
|
||||||
|
- [ ] Mettre à jour le CDC original
|
||||||
|
- [ ] Valider les modifications avec les parties prenantes
|
||||||
|
- [ ] Modifier le texte de la checkbox de consentement photo (libellé actuel : 'J\'accepte l\'utilisation de ma photo.') sur l'écran d'inscription Nounou Étape 2 (`nanny_register_step2_screen.dart`).
|
||||||
|
|
||||||
|
# Évolutions proposées au cahier des charges
|
||||||
|
|
||||||
|
## 1. Workflow de création de compte
|
||||||
|
|
||||||
|
### 1.1 Récupération de compte
|
||||||
|
|
||||||
|
#### 1.1.1 Fonctionnalités
|
||||||
|
- Ajout d'un lien "Mot de passe oublié" sur la page de connexion
|
||||||
|
- Processus de récupération en 3 étapes :
|
||||||
|
1. Saisie de l'adresse email
|
||||||
|
2. Envoi d'un lien unique de réinitialisation (valide 24h)
|
||||||
|
3. Création d'un nouveau mot de passe
|
||||||
|
|
||||||
|
#### 1.1.2 Sécurité
|
||||||
|
- Le lien de réinitialisation doit être unique et à usage unique
|
||||||
|
- Le lien expire après 24 heures
|
||||||
|
- Le nouveau mot de passe doit respecter les mêmes critères que lors de la création de compte
|
||||||
|
- Notification par email lors de la réinitialisation du mot de passe
|
||||||
|
|
||||||
|
#### 1.1.3 Interface
|
||||||
|
- Page dédiée pour la saisie de l'email
|
||||||
|
- Page de confirmation d'envoi du lien
|
||||||
|
- Formulaire de réinitialisation du mot de passe
|
||||||
|
- Messages d'erreur clairs en cas de :
|
||||||
|
- Email non trouvé
|
||||||
|
- Lien expiré
|
||||||
|
- Mot de passe non conforme
|
||||||
|
|
||||||
|
## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition)
|
||||||
|
|
||||||
|
### X.1 Recadrage et Redimensionnement des Photos
|
||||||
|
|
||||||
|
#### X.1.1 Fonctionnalités
|
||||||
|
- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants).
|
||||||
|
- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final.
|
||||||
|
- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application.
|
||||||
|
|
||||||
|
#### X.1.2 Solution Technique Envisagée (pour discussion)
|
||||||
|
- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`.
|
||||||
|
- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise.
|
||||||
|
|
||||||
|
#### X.1.3 Impact sur l'application
|
||||||
|
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
|
||||||
|
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
|
||||||
|
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
Point tickets frontend (API Gitea) - 27/01/2026
|
||||||
|
================================================
|
||||||
|
|
||||||
|
Issues avec label "frontend" : 20 (ouvertes: 12, fermees: 8)
|
||||||
|
|
||||||
|
Num | Etat | Titre
|
||||||
|
----+--------+--------------------------------------------------------
|
||||||
|
35 | open | [Frontend] Écran Création Gestionnaire
|
||||||
|
36 | closed | [Frontend] Inscription Parent - Étape 1 (Parent 1)
|
||||||
|
37 | closed | [Frontend] Inscription Parent - Étape 2 (Parent 2)
|
||||||
|
38 | closed | [Frontend] Inscription Parent - Étape 3 (Enfants)
|
||||||
|
39 | closed | [Frontend] Inscription Parent - Étapes 4-6 (Finalisatio
|
||||||
|
40 | closed | [Frontend] Inscription AM - Panneau 1 (Identité)
|
||||||
|
41 | closed | [Frontend] Inscription AM - Panneau 2 (Infos pro)
|
||||||
|
42 | closed | [Frontend] Inscription AM - Finalisation
|
||||||
|
43 | open | [Frontend] Écran Création Mot de Passe
|
||||||
|
44 | open | [Frontend] Dashboard Gestionnaire - Structure
|
||||||
|
45 | open | [Frontend] Dashboard Gestionnaire - Liste Parents
|
||||||
|
46 | open | [Frontend] Dashboard Gestionnaire - Liste AM
|
||||||
|
47 | open | [Frontend] Écran Changement MDP Obligatoire
|
||||||
|
48 | open | [Frontend] Gestion Erreurs & Messages
|
||||||
|
49 | open | [Frontend] Écran Gestion Documents Légaux (Admin)
|
||||||
|
50 | open | [Frontend] Affichage dynamique CGU lors inscription
|
||||||
|
51 | open | [Frontend] Écran Logs Admin (optionnel v1.1)
|
||||||
|
54 | open | [Tests] Tests E2E Frontend
|
||||||
|
82 | closed | [Frontend] Adapter �cran Login pour mobile
|
||||||
|
83 | closed | [Frontend] Adapter �cran Choix Inscription pour mobile
|
||||||
|
|
||||||
|
Suivi doc 23_LISTE-TICKETS (Gitea #73,78,79,81,82,83):
|
||||||
|
#73 closed labels=[]
|
||||||
|
#78 closed labels=[]
|
||||||
|
#79 closed labels=[]
|
||||||
|
#81 closed labels=[]
|
||||||
|
#82 closed (écran Login mobile)
|
||||||
|
#83 closed labels=['frontend', 'p3', 'phase-1', 'ux']
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Procédure – Utilisation de l’API Gitea
|
||||||
|
|
||||||
|
## 1. Contexte
|
||||||
|
|
||||||
|
- **Instance** : https://git.ptits-pas.fr
|
||||||
|
- **API de base** : `https://git.ptits-pas.fr/api/v1`
|
||||||
|
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
|
||||||
|
|
||||||
|
## 2. Authentification
|
||||||
|
|
||||||
|
### 2.1 Token
|
||||||
|
|
||||||
|
Le token est défini dans l’environnement (ex. `~/.bashrc`) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITEA_TOKEN="<votre_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour l’utiliser dans les commandes :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc # ou : . ~/.bashrc
|
||||||
|
# Puis utiliser $GITEA_TOKEN dans les curl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 En-tête HTTP
|
||||||
|
|
||||||
|
Toutes les requêtes API doivent envoyer le token :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
-H "Authorization: token $GITEA_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Endpoints utiles
|
||||||
|
|
||||||
|
### 3.1 Dépôt (repository)
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
|
||||||
|
| Liste dépôts | GET | `/repos/search?q=petitspas` |
|
||||||
|
|
||||||
|
Exemple – infos du dépôt :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Issues (tickets)
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|------------------|---------|-----|
|
||||||
|
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
|
||||||
|
| Détail d’une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
|
||||||
|
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
|
||||||
|
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
|
||||||
|
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
|
||||||
|
|
||||||
|
**Paramètres GET utiles pour la liste :**
|
||||||
|
|
||||||
|
- `state` : `open` ou `closed`
|
||||||
|
- `labels` : filtre par label (ex. `frontend`)
|
||||||
|
- `page`, `limit` : pagination
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Toutes les issues ouvertes
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
|
||||||
|
|
||||||
|
# Issues ouvertes avec label "frontend"
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
|
||||||
|
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
|
||||||
|
|
||||||
|
# Détail de l’issue #47
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
|
||||||
|
|
||||||
|
# Fermer l’issue #31
|
||||||
|
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"state":"closed"}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
|
||||||
|
|
||||||
|
# Créer une issue
|
||||||
|
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Pull requests
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
|
||||||
|
| Détail d’une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
|
||||||
|
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
|
||||||
|
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Liste des PR ouvertes
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
|
||||||
|
|
||||||
|
# Créer une PR (head = branche source, base = branche cible)
|
||||||
|
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Branches
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Webhooks
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
|
||||||
|
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
|
||||||
|
|
||||||
|
### 3.6 Labels
|
||||||
|
|
||||||
|
| Action | Méthode | URL |
|
||||||
|
|---------------|---------|-----|
|
||||||
|
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Résumé des URLs pour P'titsPas
|
||||||
|
|
||||||
|
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
|
||||||
|
|
||||||
|
| Ressource | URL |
|
||||||
|
|------------------|-----|
|
||||||
|
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
|
||||||
|
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
|
||||||
|
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
|
||||||
|
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
|
||||||
|
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
|
||||||
|
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
|
||||||
|
|
||||||
|
## 5. Documentation officielle
|
||||||
|
|
||||||
|
- Swagger / OpenAPI : https://docs.gitea.com/api
|
||||||
|
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
|
||||||
|
|
||||||
|
## 6. Dépannage
|
||||||
|
|
||||||
|
- **401 Unauthorized** : vérifier le token et l’en-tête `Authorization: token <TOKEN>`.
|
||||||
|
- **404** : vérifier owner/repo et l’URL (sensible à la casse).
|
||||||
|
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Statut de l'application P'titsPas
|
||||||
|
|
||||||
|
**Date du point** : 8 février 2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Environnement de production
|
||||||
|
|
||||||
|
| Élément | Statut | Détail |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| **URL** | OK | https://app.ptits-pas.fr |
|
||||||
|
| **Frontend** | 200 | Flutter Web, Nginx |
|
||||||
|
| **API** | 200 | NestJS, préfixe `/api/v1` |
|
||||||
|
| **Base de données** | OK | PostgreSQL 17 |
|
||||||
|
| **PgAdmin** | OK | https://app.ptits-pas.fr/pgadmin |
|
||||||
|
|
||||||
|
### Conteneurs Docker
|
||||||
|
|
||||||
|
| Service | Image | État |
|
||||||
|
|---------|--------|------|
|
||||||
|
| ptitspas-frontend | ptitspas-app-frontend | Up (recréé récemment) |
|
||||||
|
| ptitspas-backend | ptitspas-app-backend | Up ~26h |
|
||||||
|
| ptitspas-postgres | postgres:17 | Up ~28h |
|
||||||
|
| ptitspas-pgadmin | dpage/pgadmin4 | Up ~28h |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Dépôt Git
|
||||||
|
|
||||||
|
- **Branche déployée** : `master`
|
||||||
|
- **Derniers commits** :
|
||||||
|
- `10bf255` – fix(ui): renforcer ombre boutons Parents/AM sur mobile
|
||||||
|
- `678f421` – docs: ticket #82 fermé (écran Login mobile)
|
||||||
|
- `5295e8e` – Merge develop: login mobile, formulaire sous slogan par ratio
|
||||||
|
- `6bf0932` – docs: Index, doc API Gitea, script fermeture issue
|
||||||
|
- `2f1740b` – docs: ticket #83 RegisterChoiceScreen Mobile (terminé)
|
||||||
|
|
||||||
|
- **Branches actives** : `master`, `develop`, diverses `feature/*` (inscription, config, documents légaux, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Déploiement (hook Gitea)
|
||||||
|
|
||||||
|
| Élément | Statut |
|
||||||
|
|--------|--------|
|
||||||
|
| **Webhook** | Opérationnel (`hooks.ptits-pas.fr/hooks/petitspas-deploy`) |
|
||||||
|
| **Déclencheur** | Push sur `master`, dépôt `petitspas` |
|
||||||
|
| **Script** | Monté depuis l’hôte (verrou + sans Prisma) |
|
||||||
|
| **Dernier déploiement** | 08/02/2026 18:18:26 – Succès |
|
||||||
|
|
||||||
|
Un seul déploiement à la fois (verrou) ; plus d’étape Prisma dans le script.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Fonctionnalités livrées
|
||||||
|
|
||||||
|
### Backend (API)
|
||||||
|
|
||||||
|
- Auth : login, refresh, profil, **changement MDP obligatoire** (first login)
|
||||||
|
- Configuration : setup status, bulk, test SMTP, catégories
|
||||||
|
- Documents légaux : actifs, versions, upload, activation, téléchargement
|
||||||
|
- Inscription : parents (workflow complet), enfants (CRUD)
|
||||||
|
- Compte super_admin par défaut (seed BDD) : `admin@ptits-pas.fr` / `4dm1n1strateur`
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **Formulaires d’inscription** : compatibles **desktop et mobile**
|
||||||
|
- Choix d’inscription (Parents / Assistante maternelle) – responsive
|
||||||
|
- Inscription Parent : étapes 1 à 5 (infos parent 1 & 2, enfants, présentation, CGU, récap)
|
||||||
|
- Inscription AM : étapes 1 à 4 (identité, pro, présentation, récap)
|
||||||
|
- **Login** : écran adapté mobile (formulaire sous slogan selon ratio)
|
||||||
|
- Modale **changement de mot de passe obligatoire** après première connexion si `changement_mdp_obligatoire`
|
||||||
|
- CORS configuré (localhost + prod)
|
||||||
|
|
||||||
|
### Base de données
|
||||||
|
|
||||||
|
- Schéma database-first (BDD.sql)
|
||||||
|
- Tables : utilisateurs, configuration, documents_legaux, acceptations_documents, enfants, etc.
|
||||||
|
- Champs tokens création MDP, genre enfants, configuration système
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Tickets / Priorités (résumé)
|
||||||
|
|
||||||
|
- **Liste détaillée** : `docs/23_LISTE-TICKETS.md`
|
||||||
|
- **Récent fermé** : #82 (Login mobile), #83 (RegisterChoiceScreen mobile), #73, #78, #79, #81
|
||||||
|
- **P0 (BDD)** : quelques amendements ouverts (champs CDC, présentation dossier, etc.)
|
||||||
|
- **P1** : configuration système (panneau Paramètres, 3 sections, première config + accès permanent)
|
||||||
|
- **P2/P3** : backend métier et frontend (dashboards, écrans création MDP, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Documentation utile
|
||||||
|
|
||||||
|
| Fichier | Usage |
|
||||||
|
|---------|--------|
|
||||||
|
| `00_INDEX.md` | Index de la doc |
|
||||||
|
| `01_CAHIER-DES-CHARGES.md` | CDC v1.3 |
|
||||||
|
| `11_API.md` | Endpoints API |
|
||||||
|
| `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte |
|
||||||
|
| `23_LISTE-TICKETS.md` | Liste des tickets |
|
||||||
|
| `BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires |
|
||||||
|
| `PROCEDURE-API-GITEA.md` | Utilisation API Gitea (issues, PR, token) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Synthèse
|
||||||
|
|
||||||
|
L’application est **en production** sur https://app.ptits-pas.fr avec :
|
||||||
|
|
||||||
|
- Frontend et API accessibles et répondant en 200.
|
||||||
|
- Déploiement automatique sur push `master` avec script à jour (verrou, sans Prisma).
|
||||||
|
- Formulaires d’inscription (Parents et AM) **responsive desktop et mobile**.
|
||||||
|
- Login et changement de mot de passe obligatoire opérationnels.
|
||||||
|
- Prochaines priorités : P0 BDD si besoin, P1 panneau Paramètres / Configuration (tickets #12, #13), puis dashboards et workflows métier (P2/P3).
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# SuperNounou – SSS-001
|
||||||
|
## Spécification technique & opérationnelle unifiée
|
||||||
|
_Version 0.2 – 24 avril 2025_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
Centraliser tous les aspects **techniques et opérationnels** de la plateforme SuperNounou :
|
||||||
|
- Sauvegarde & Plan de Reprise d’Activité (PRA)
|
||||||
|
- Spécifications des API & intégrations
|
||||||
|
- Directives de déploiement, d’observabilité, de CI/CD
|
||||||
|
|
||||||
|
## 2. Portée
|
||||||
|
Instances de production, pré-production et recette (Frontend, Backend, PostgreSQL, stockage objets), scripts d’installation, pipelines CI/CD, journaux et métriques.
|
||||||
|
|
||||||
|
## 3. Références
|
||||||
|
- CDC SuperNounou V1.1
|
||||||
|
- ISO 27001 / ISO 22301 bonnes pratiques
|
||||||
|
- Politique sécurité DSI Enedis #SEC-POL-2024
|
||||||
|
- RGPD (2016/679)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# A – Sauvegarde & Plan de Reprise d’Activité
|
||||||
|
|
||||||
|
### A.1 Architecture de sauvegarde
|
||||||
|
Schéma bloc, chiffrement AES-256 (KMS), réplication hors-site « Object Storage B ».
|
||||||
|
|
||||||
|
### A.2 Stratégie de sauvegarde
|
||||||
|
|
||||||
|
| Type | Fréquence | Rétention | Support |
|
||||||
|
|-------------------|-------------|-----------|--------------------|
|
||||||
|
| Incrémentale | Quotidienne | 30 j | Object Storage A |
|
||||||
|
| Complète | Hebdomadaire| 6 mois | Object Storage B |
|
||||||
|
| Export logique DB | Mensuelle | 5 ans | Stockage Glacier |
|
||||||
|
|
||||||
|
### A.3 PRA
|
||||||
|
RPO 24 h / RTO 4 h – scénarios : panne VM, corruption DB, sinistre DC – procédure détaillée + escalade.
|
||||||
|
|
||||||
|
### A.4 Tests de restauration
|
||||||
|
Intégrale semestrielle, partielle trimestrielle – rapport d’audit et actions correctives.
|
||||||
|
|
||||||
|
### A.5 Monitoring & alertes
|
||||||
|
Endpoint Prometheus `/metrics`, tableau Grafana « Backup status », alerte > 26 h sans backup.
|
||||||
|
|
||||||
|
### A.6 Rôles
|
||||||
|
DevOps Lead (implémentation), DBA (tests restore), RSSI (audit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# B – API & Intégrations
|
||||||
|
|
||||||
|
### B.1 Conventions
|
||||||
|
OpenAPI 3 livré (`openapi.yaml`), version URL `/api/v1`, ISO 8601 dates.
|
||||||
|
|
||||||
|
### B.2 Sécurité API
|
||||||
|
JWT Bearer (ou OAuth 2), TLS 1.3, rate-limit 100 req/min/IP, signature HMAC pour webhooks.
|
||||||
|
|
||||||
|
### B.3 Exemples
|
||||||
|
Collection Postman, scripts cURL, guide « Appeler l’API ».
|
||||||
|
|
||||||
|
### B.4 Intégrations futures
|
||||||
|
SSO LDAP/SAML, webhook `contract.validated`, export statistiques CSV.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# C – Déploiement, CI/CD et Observabilité *(nouveau)*
|
||||||
|
|
||||||
|
### C.1 Déploiement communal (on-premise)
|
||||||
|
- **Objectif** : installation complète sur un serveur Linux ou VM en moins d’1 h.
|
||||||
|
- **Livrable** : solution de packaging **au choix** (Docker Compose, image VM, paquet .deb/.rpm).
|
||||||
|
- **Script update / rollback** : `update.sh` ou équivalent (backup ➜ pull ➜ migrate ➜ vérif ; rollback ≤ 5 min).
|
||||||
|
- **Config** : fichier `.env.sample` décrivant toutes les variables.
|
||||||
|
|
||||||
|
### C.2 Environnements
|
||||||
|
- Developpement local (Docker Compose).
|
||||||
|
- Recette & Production (serveur communal).
|
||||||
|
- Les étudiants doivent décrire la procédure de bascule Recette → Prod.
|
||||||
|
|
||||||
|
### C.3 Pipeline CI/CD
|
||||||
|
- Pipeline automatisé (tests unitaires + build image + scan CVE) déclenché à chaque merge.
|
||||||
|
- L’école fournit son propre dépôt Git/runner.
|
||||||
|
- Artifacts : images taguées, notes de version (`CHANGELOG.md`).
|
||||||
|
|
||||||
|
### C.4 Observabilité & logs
|
||||||
|
- Journaux applicatifs JSON (timestamp UTC, level, traceId).
|
||||||
|
- Rotation/retention : 7 jours sur disque, 30 jours sur archive compressée.
|
||||||
|
- Export métriques Prometheus (`/metrics`) : latence API, nombre de sessions, files d’attente hors-ligne.
|
||||||
|
- Tableaux Grafana d’exemple inclus (`grafana_dashboard.json`).
|
||||||
|
|
||||||
|
### C.5 SLA et performances (indicatifs)
|
||||||
|
- Disponibilité mensuelle cible : **≥ 98 %**.
|
||||||
|
- Temps de réponse P95 des opérations courantes : **< 500 ms**.
|
||||||
|
- Capacité test : **≈ 50 sessions simultanées** sans dégradation (> 1 s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# D – Glossaire
|
||||||
|
AES-256, JWT, KMS, OpenAPI, RPO, RTO, rate-limit, HMAC, Compose, CI/CD…
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# E – Historique des versions
|
||||||
|
|
||||||
|
| Version | Date | Auteur | Commentaire |
|
||||||
|
|---------|------------|------------------|---------------------------------|
|
||||||
|
| 0.1-draft | 2025-04-24 | Équipe projet | Création du SSS unifié |
|
||||||
|
| 0.2 | 2025-04-24 | ChatGPT & Julien | Ajout déploiement / CI/CD / logs |
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package io.flutter.plugins;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import io.flutter.Log;
|
|
||||||
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated file. Do not edit.
|
|
||||||
* This file is generated by the Flutter tool based on the
|
|
||||||
* plugins that support the Android platform.
|
|
||||||
*/
|
|
||||||
@Keep
|
|
||||||
public final class GeneratedPluginRegistrant {
|
|
||||||
private static final String TAG = "GeneratedPluginRegistrant";
|
|
||||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
flutter.sdk=C:\\Users\\myhan\\flutter
|
flutter.sdk=C:\\Users\\marti\\dev\\flutter
|
||||||
sdk.dir=C:\\Users\\myhan\\AppData\\Local\\Android\\Sdk
|
sdk.dir=C:\\Users\\myhan\\AppData\\Local\\Android\\Sdk
|
||||||
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 510 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect x="271" y="17" width="193" height="96" rx="19.2" fill="#f9bc8e" />
|
||||||
|
<rect x="168" y="108" width="206" height="115" rx="23.0" fill="#f7db75" />
|
||||||
|
<rect x="131" y="229" width="223" height="132" rx="26.4" fill="#aadac2" />
|
||||||
|
<rect x="47" y="349" width="238" height="144" rx="28.8" fill="#dfc2cf" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 374 B |
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<!-- Water‑color like noise -->
|
||||||
|
<filter id="wcTexture" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" seed="12" result="noise"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="noise" mode="multiply"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- Gradients -->
|
||||||
|
<radialGradient id="gradCoral" cx="50%" cy="40%" r="70%">
|
||||||
|
<stop offset="0%" stop-color="#ffddc9"/>
|
||||||
|
<stop offset="100%" stop-color="#f49c6e"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<radialGradient id="gradYellow" cx="45%" cy="35%" r="70%">
|
||||||
|
<stop offset="0%" stop-color="#fff6c9"/>
|
||||||
|
<stop offset="100%" stop-color="#e9c833"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<radialGradient id="gradMint" cx="40%" cy="30%" r="70%">
|
||||||
|
<stop offset="0%" stop-color="#d4f4ec"/>
|
||||||
|
<stop offset="100%" stop-color="#6bbda3"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<radialGradient id="gradLavender" cx="35%" cy="25%" r="70%">
|
||||||
|
<stop offset="0%" stop-color="#f5e6ff"/>
|
||||||
|
<stop offset="100%" stop-color="#b289c9"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- CORAL -->
|
||||||
|
<path filter="url(#wcTexture)" fill="url(#gradCoral)" d="
|
||||||
|
M 360 40
|
||||||
|
Q 380 15 420 22
|
||||||
|
L 450 28
|
||||||
|
Q 480 35 490 60
|
||||||
|
L 495 80
|
||||||
|
Q 500 105 470 120
|
||||||
|
L 440 135
|
||||||
|
Q 410 150 370 135
|
||||||
|
L 345 120
|
||||||
|
Q 315 105 325 75
|
||||||
|
L 330 55
|
||||||
|
Q 335 50 360 40 Z"/>
|
||||||
|
|
||||||
|
<!-- YELLOW -->
|
||||||
|
<path filter="url(#wcTexture)" fill="url(#gradYellow)" d="
|
||||||
|
M 280 190
|
||||||
|
Q 300 170 340 175
|
||||||
|
L 370 180
|
||||||
|
Q 405 185 410 210
|
||||||
|
L 415 230
|
||||||
|
Q 420 255 390 270
|
||||||
|
L 355 285
|
||||||
|
Q 320 300 290 285
|
||||||
|
L 265 270
|
||||||
|
Q 235 255 245 225
|
||||||
|
L 250 205
|
||||||
|
Q 255 200 280 190 Z"/>
|
||||||
|
|
||||||
|
<!-- MINT -->
|
||||||
|
<path filter="url(#wcTexture)" fill="url(#gradMint)" d="
|
||||||
|
M 180 330
|
||||||
|
Q 205 310 255 315
|
||||||
|
L 285 320
|
||||||
|
Q 325 325 330 350
|
||||||
|
L 335 370
|
||||||
|
Q 340 395 305 410
|
||||||
|
L 275 425
|
||||||
|
Q 235 440 200 425
|
||||||
|
L 175 410
|
||||||
|
Q 145 395 155 365
|
||||||
|
L 160 345
|
||||||
|
Q 165 340 180 330 Z"/>
|
||||||
|
|
||||||
|
<!-- LAVENDER -->
|
||||||
|
<path filter="url(#wcTexture)" fill="url(#gradLavender)" d="
|
||||||
|
M 80 460
|
||||||
|
Q 100 440 160 445
|
||||||
|
L 190 450
|
||||||
|
Q 235 455 240 480
|
||||||
|
L 245 500
|
||||||
|
Q 250 525 210 540
|
||||||
|
L 170 555
|
||||||
|
Q 130 570 95 555
|
||||||
|
L 65 540
|
||||||
|
Q 35 525 45 495
|
||||||
|
L 50 475
|
||||||
|
Q 55 470 80 460 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 304 KiB |
@@ -0,0 +1,126 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
// Models
|
||||||
|
import '../models/user_registration_data.dart';
|
||||||
|
import '../models/am_registration_data.dart';
|
||||||
|
|
||||||
|
// Screens
|
||||||
|
import '../screens/auth/login_screen.dart';
|
||||||
|
import '../screens/auth/register_choice_screen.dart';
|
||||||
|
import '../screens/auth/parent_register_step1_screen.dart';
|
||||||
|
import '../screens/auth/parent_register_step2_screen.dart';
|
||||||
|
import '../screens/auth/parent_register_step3_screen.dart';
|
||||||
|
import '../screens/auth/parent_register_step4_screen.dart';
|
||||||
|
import '../screens/auth/parent_register_step5_screen.dart';
|
||||||
|
import '../screens/auth/am_register_step1_screen.dart';
|
||||||
|
import '../screens/auth/am_register_step2_screen.dart';
|
||||||
|
import '../screens/auth/am_register_step3_screen.dart';
|
||||||
|
import '../screens/auth/am_register_step4_screen.dart';
|
||||||
|
import '../screens/home/home_screen.dart';
|
||||||
|
import '../screens/administrateurs/admin_dashboardScreen.dart';
|
||||||
|
import '../screens/home/parent_screen/ParentDashboardScreen.dart';
|
||||||
|
import '../screens/unknown_screen.dart';
|
||||||
|
|
||||||
|
// --- Provider Instances ---
|
||||||
|
// It's generally better to provide these higher up the widget tree if possible,
|
||||||
|
// or ensure they are created only once.
|
||||||
|
// For ShellRoute, creating them here and passing via .value is common.
|
||||||
|
|
||||||
|
final userRegistrationDataNotifier = UserRegistrationData();
|
||||||
|
final amRegistrationDataNotifier = AmRegistrationData();
|
||||||
|
|
||||||
|
class AppRouter {
|
||||||
|
static final GoRouter router = GoRouter(
|
||||||
|
initialLocation: '/login',
|
||||||
|
errorBuilder: (context, state) => const UnknownScreen(),
|
||||||
|
debugLogDiagnostics: true,
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/login',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const LoginScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/register-choice',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const RegisterChoiceScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/home',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/admin-dashboard',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const AdminDashboardScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/parent-dashboard',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const ParentDashboardScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/am-dashboard',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- Parent Registration Flow ---
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) {
|
||||||
|
return ChangeNotifierProvider<UserRegistrationData>.value(
|
||||||
|
value: userRegistrationDataNotifier,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/parent-register-step1',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep1Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/parent-register-step2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep2Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/parent-register-step3',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep3Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/parent-register-step4',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep4Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/parent-register-step5',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- AM (Assistante Maternelle) Registration Flow ---
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) {
|
||||||
|
return ChangeNotifierProvider<AmRegistrationData>.value(
|
||||||
|
value: amRegistrationDataNotifier,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/am-register-step1',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep1Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/am-register-step2',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep2Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/am-register-step3',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep3Screen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/am-register-step4',
|
||||||
|
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep4Screen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Mode d'affichage d'un formulaire
|
||||||
|
enum DisplayMode {
|
||||||
|
/// Mode éditable (formulaire d'inscription)
|
||||||
|
editable,
|
||||||
|
|
||||||
|
/// Mode lecture seule (récapitulatif)
|
||||||
|
readonly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration d'affichage pour les widgets de formulaire
|
||||||
|
class DisplayConfig {
|
||||||
|
/// Mode d'affichage (editable/readonly)
|
||||||
|
final DisplayMode mode;
|
||||||
|
|
||||||
|
/// Type de layout détecté (mobile/desktop)
|
||||||
|
final LayoutType layoutType;
|
||||||
|
|
||||||
|
const DisplayConfig({
|
||||||
|
required this.mode,
|
||||||
|
required this.layoutType,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Crée une config à partir du contexte
|
||||||
|
factory DisplayConfig.fromContext(
|
||||||
|
BuildContext context, {
|
||||||
|
DisplayMode mode = DisplayMode.editable,
|
||||||
|
}) {
|
||||||
|
return DisplayConfig(
|
||||||
|
mode: mode,
|
||||||
|
layoutType: LayoutHelper.getLayoutType(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Est en mode éditable
|
||||||
|
bool get isEditable => mode == DisplayMode.editable;
|
||||||
|
|
||||||
|
/// Est en mode lecture seule
|
||||||
|
bool get isReadonly => mode == DisplayMode.readonly;
|
||||||
|
|
||||||
|
/// Est en layout mobile
|
||||||
|
bool get isMobile => layoutType == LayoutType.mobile;
|
||||||
|
|
||||||
|
/// Est en layout desktop
|
||||||
|
bool get isDesktop => layoutType == LayoutType.desktop;
|
||||||
|
|
||||||
|
/// Layout vertical (mobile)
|
||||||
|
bool get isVerticalLayout => isMobile;
|
||||||
|
|
||||||
|
/// Layout horizontal (desktop)
|
||||||
|
bool get isHorizontalLayout => isDesktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type de layout
|
||||||
|
enum LayoutType {
|
||||||
|
/// Mobile (< 600px) - toujours vertical
|
||||||
|
mobile,
|
||||||
|
|
||||||
|
/// Desktop/Tablette (≥ 600px) - horizontal
|
||||||
|
desktop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utilitaires pour la détection de layout
|
||||||
|
class LayoutHelper {
|
||||||
|
/// Seuil de largeur pour mobile/desktop (en pixels)
|
||||||
|
static const double mobileBreakpoint = 600.0;
|
||||||
|
|
||||||
|
/// Détermine le type de layout selon la largeur d'écran
|
||||||
|
static LayoutType getLayoutType(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
return width < mobileBreakpoint
|
||||||
|
? LayoutType.mobile
|
||||||
|
: LayoutType.desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si on est sur mobile
|
||||||
|
static bool isMobile(BuildContext context) {
|
||||||
|
return getLayoutType(context) == LayoutType.mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si on est sur desktop
|
||||||
|
static bool isDesktop(BuildContext context) {
|
||||||
|
return getLayoutType(context) == LayoutType.desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne un espacement adapté au layout
|
||||||
|
static double getSpacing(BuildContext context, {
|
||||||
|
double mobileSpacing = 12.0,
|
||||||
|
double desktopSpacing = 20.0,
|
||||||
|
}) {
|
||||||
|
return isMobile(context) ? mobileSpacing : desktopSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne une largeur max adaptée au layout
|
||||||
|
static double getMaxWidth(BuildContext context, {
|
||||||
|
double? mobileMaxWidth,
|
||||||
|
double? desktopMaxWidth,
|
||||||
|
}) {
|
||||||
|
if (isMobile(context)) {
|
||||||
|
return mobileMaxWidth ?? double.infinity;
|
||||||
|
} else {
|
||||||
|
return desktopMaxWidth ?? 1200.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,11 @@ class Env {
|
|||||||
// Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL
|
// Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL
|
||||||
static const String apiBaseUrl = String.fromEnvironment(
|
static const String apiBaseUrl = String.fromEnvironment(
|
||||||
'API_BASE_URL',
|
'API_BASE_URL',
|
||||||
defaultValue: 'https://ynov.ptits-pas.fr',
|
defaultValue: 'https://app.ptits-pas.fr',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
|
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
|
||||||
static String apiV1(String path) => "${apiBaseUrl}/api/v1$path";
|
static String apiV1(String path) => '$apiBaseUrl/api/v1$path';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
|
||||||
import 'navigation/app_router.dart';
|
// import 'package:provider/provider.dart'; // Supprimer Provider
|
||||||
|
import 'config/app_router.dart'; // <-- Importer le bon routeur (GoRouter)
|
||||||
|
// import 'theme/app_theme.dart'; // Supprimer AppTheme
|
||||||
|
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp()); // Exécution simple
|
runApp(const MyApp()); // Exécution simple
|
||||||
@@ -14,7 +17,7 @@ class MyApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Pas besoin de Provider.of ici
|
// Pas besoin de Provider.of ici
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp.router( // <-- Utilisation de MaterialApp.router
|
||||||
title: 'P\'titsPas',
|
title: 'P\'titsPas',
|
||||||
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
|
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
|
||||||
textTheme: GoogleFonts.meriendaTextTheme(
|
textTheme: GoogleFonts.meriendaTextTheme(
|
||||||
@@ -32,8 +35,7 @@ class MyApp extends StatelessWidget {
|
|||||||
// Locale('en', 'US'), // Anglais, si besoin
|
// Locale('en', 'US'), // Anglais, si besoin
|
||||||
],
|
],
|
||||||
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
|
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
|
||||||
initialRoute: AppRouter.login,
|
routerConfig: AppRouter.router, // <-- Passer la configuration du GoRouter
|
||||||
onGenerateRoute: AppRouter.generateRoute,
|
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class AmRegistrationData extends ChangeNotifier {
|
||||||
|
// Step 1: Identity Info
|
||||||
|
String firstName = '';
|
||||||
|
String lastName = '';
|
||||||
|
String streetAddress = ''; // Nouveau pour N° et Rue
|
||||||
|
String postalCode = ''; // Nouveau
|
||||||
|
String city = ''; // Nouveau
|
||||||
|
String phone = '';
|
||||||
|
String email = '';
|
||||||
|
String password = '';
|
||||||
|
// String? photoPath; // Déplacé ou géré à l'étape 2
|
||||||
|
// bool photoConsent = false; // Déplacé ou géré à l'étape 2
|
||||||
|
|
||||||
|
// Step 2: Professional Info
|
||||||
|
String? photoPath; // Ajouté pour l'étape 2
|
||||||
|
bool photoConsent = false; // Ajouté pour l'étape 2
|
||||||
|
DateTime? dateOfBirth;
|
||||||
|
String birthCity = ''; // Nouveau
|
||||||
|
String birthCountry = ''; // Nouveau
|
||||||
|
// String placeOfBirth = ''; // Remplacé par birthCity et birthCountry
|
||||||
|
String nir = ''; // Numéro de Sécurité Sociale
|
||||||
|
String agrementNumber = ''; // Numéro d'agrément
|
||||||
|
int? capacity; // Number of children the AM can look after
|
||||||
|
|
||||||
|
// Step 3: Presentation & CGU
|
||||||
|
String presentationText = '';
|
||||||
|
bool cguAccepted = false;
|
||||||
|
|
||||||
|
// --- Methods to update data and notify listeners ---
|
||||||
|
|
||||||
|
void updateIdentityInfo({
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? streetAddress, // Modifié
|
||||||
|
String? postalCode, // Nouveau
|
||||||
|
String? city, // Nouveau
|
||||||
|
String? phone,
|
||||||
|
String? email,
|
||||||
|
String? password,
|
||||||
|
}) {
|
||||||
|
this.firstName = firstName ?? this.firstName;
|
||||||
|
this.lastName = lastName ?? this.lastName;
|
||||||
|
this.streetAddress = streetAddress ?? this.streetAddress; // Modifié
|
||||||
|
this.postalCode = postalCode ?? this.postalCode; // Nouveau
|
||||||
|
this.city = city ?? this.city; // Nouveau
|
||||||
|
this.phone = phone ?? this.phone;
|
||||||
|
this.email = email ?? this.email;
|
||||||
|
this.password = password ?? this.password;
|
||||||
|
// if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1
|
||||||
|
// this.photoPath = photoPath;
|
||||||
|
// }
|
||||||
|
// this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProfessionalInfo({
|
||||||
|
String? photoPath,
|
||||||
|
bool? photoConsent,
|
||||||
|
DateTime? dateOfBirth,
|
||||||
|
String? birthCity, // Nouveau
|
||||||
|
String? birthCountry, // Nouveau
|
||||||
|
// String? placeOfBirth, // Remplacé
|
||||||
|
String? nir,
|
||||||
|
String? agrementNumber,
|
||||||
|
int? capacity,
|
||||||
|
}) {
|
||||||
|
// Allow setting photoPath to null explicitly
|
||||||
|
if (photoPath != null || this.photoPath != null) {
|
||||||
|
this.photoPath = photoPath;
|
||||||
|
}
|
||||||
|
this.photoConsent = photoConsent ?? this.photoConsent;
|
||||||
|
this.dateOfBirth = dateOfBirth ?? this.dateOfBirth;
|
||||||
|
this.birthCity = birthCity ?? this.birthCity; // Nouveau
|
||||||
|
this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau
|
||||||
|
// this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé
|
||||||
|
this.nir = nir ?? this.nir;
|
||||||
|
this.agrementNumber = agrementNumber ?? this.agrementNumber;
|
||||||
|
this.capacity = capacity ?? this.capacity;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePresentationAndCgu({
|
||||||
|
String? presentationText,
|
||||||
|
bool? cguAccepted,
|
||||||
|
}) {
|
||||||
|
this.presentationText = presentationText ?? this.presentationText;
|
||||||
|
this.cguAccepted = cguAccepted ?? this.cguAccepted;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Getters for validation or display ---
|
||||||
|
bool get isStep1Complete =>
|
||||||
|
firstName.isNotEmpty &&
|
||||||
|
lastName.isNotEmpty &&
|
||||||
|
streetAddress.isNotEmpty && // Modifié
|
||||||
|
postalCode.isNotEmpty && // Nouveau
|
||||||
|
city.isNotEmpty && // Nouveau
|
||||||
|
phone.isNotEmpty &&
|
||||||
|
email.isNotEmpty;
|
||||||
|
// password n'est pas requis à l'inscription (défini après validation par lien email)
|
||||||
|
|
||||||
|
bool get isStep2Complete =>
|
||||||
|
// photoConsent is mandatory if a photo is system-required, otherwise optional.
|
||||||
|
// For now, let's assume if photoPath is present, consent should ideally be true.
|
||||||
|
// Or, make consent always mandatory if photo section exists.
|
||||||
|
// Based on new mockup, photo is present, so consent might be implicitly or explicitly needed.
|
||||||
|
(photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée
|
||||||
|
dateOfBirth != null &&
|
||||||
|
birthCity.isNotEmpty &&
|
||||||
|
birthCountry.isNotEmpty &&
|
||||||
|
nir.isNotEmpty && // Basic check, could add validation
|
||||||
|
agrementNumber.isNotEmpty &&
|
||||||
|
capacity != null && capacity! > 0;
|
||||||
|
|
||||||
|
bool get isStep3Complete =>
|
||||||
|
// presentationText is optional as per CDC (message au gestionnaire)
|
||||||
|
cguAccepted;
|
||||||
|
|
||||||
|
bool get isRegistrationComplete =>
|
||||||
|
isStep1Complete && isStep2Complete && isStep3Complete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AmRegistrationData('
|
||||||
|
'firstName: $firstName, lastName: $lastName, '
|
||||||
|
'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, '
|
||||||
|
'phone: $phone, email: $email, '
|
||||||
|
// 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié
|
||||||
|
'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, '
|
||||||
|
'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, '
|
||||||
|
'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, '
|
||||||
|
'presentationText: $presentationText, cguAccepted: $cguAccepted)';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
|
|
||||||
|
class AssistanteMaternelleModel {
|
||||||
|
final AppUser user;
|
||||||
|
final String? approvalNumber;
|
||||||
|
final String? residenceCity;
|
||||||
|
final int? maxChildren;
|
||||||
|
final int? placesAvailable;
|
||||||
|
|
||||||
|
AssistanteMaternelleModel({
|
||||||
|
required this.user,
|
||||||
|
this.approvalNumber,
|
||||||
|
this.residenceCity,
|
||||||
|
this.maxChildren,
|
||||||
|
this.placesAvailable,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AssistanteMaternelleModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final userJson = json['user'] ?? json;
|
||||||
|
final user = AppUser.fromJson(userJson);
|
||||||
|
|
||||||
|
return AssistanteMaternelleModel(
|
||||||
|
user: user,
|
||||||
|
approvalNumber: json['numero_agrement'] as String?,
|
||||||
|
residenceCity: json['ville_residence'] as String?,
|
||||||
|
maxChildren: json['nb_max_enfants'] as int?,
|
||||||
|
placesAvailable: json['place_disponible'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
|
|
||||||
|
class ParentModel {
|
||||||
|
final AppUser user;
|
||||||
|
final int childrenCount;
|
||||||
|
|
||||||
|
ParentModel({required this.user, this.childrenCount = 0});
|
||||||
|
|
||||||
|
factory ParentModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final userJson = json['user'] ?? json;
|
||||||
|
final user = AppUser.fromJson(userJson);
|
||||||
|
final children = json['parentChildren'] as List?;
|
||||||
|
return ParentModel(
|
||||||
|
user: user,
|
||||||
|
childrenCount: children?.length ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,15 @@ class AppUser {
|
|||||||
final String role;
|
final String role;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
|
final bool changementMdpObligatoire;
|
||||||
|
final String? nom;
|
||||||
|
final String? prenom;
|
||||||
|
final String? statut;
|
||||||
|
final String? telephone;
|
||||||
|
final String? photoUrl;
|
||||||
|
final String? adresse;
|
||||||
|
final String? ville;
|
||||||
|
final String? codePostal;
|
||||||
|
|
||||||
AppUser({
|
AppUser({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -11,15 +20,54 @@ class AppUser {
|
|||||||
required this.role,
|
required this.role,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
this.changementMdpObligatoire = false,
|
||||||
|
this.nom,
|
||||||
|
this.prenom,
|
||||||
|
this.statut,
|
||||||
|
this.telephone,
|
||||||
|
this.photoUrl,
|
||||||
|
this.adresse,
|
||||||
|
this.ville,
|
||||||
|
this.codePostal,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AppUser.fromJson(Map<String, dynamic> json) {
|
factory AppUser.fromJson(Map<String, dynamic> json) {
|
||||||
|
final id = json['id']?.toString();
|
||||||
|
final email = json['email']?.toString();
|
||||||
|
final role = json['role']?.toString();
|
||||||
|
if (id == null || id.isEmpty) {
|
||||||
|
throw Exception('Profil invalide: id manquant');
|
||||||
|
}
|
||||||
|
if (email == null || email.isEmpty) {
|
||||||
|
throw Exception('Profil invalide: email manquant');
|
||||||
|
}
|
||||||
|
if (role == null || role.isEmpty) {
|
||||||
|
throw Exception('Profil invalide: rôle manquant');
|
||||||
|
}
|
||||||
return AppUser(
|
return AppUser(
|
||||||
id: json['id'] as String,
|
id: id,
|
||||||
email: json['email'] as String,
|
email: email,
|
||||||
role: json['role'] as String,
|
role: role,
|
||||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
createdAt: json['cree_le'] != null
|
||||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
? DateTime.tryParse(json['cree_le'].toString()) ?? DateTime.now()
|
||||||
|
: (json['createdAt'] != null
|
||||||
|
? DateTime.tryParse(json['createdAt'].toString()) ?? DateTime.now()
|
||||||
|
: DateTime.now()),
|
||||||
|
updatedAt: json['modifie_le'] != null
|
||||||
|
? DateTime.tryParse(json['modifie_le'].toString()) ?? DateTime.now()
|
||||||
|
: (json['updatedAt'] != null
|
||||||
|
? DateTime.tryParse(json['updatedAt'].toString()) ?? DateTime.now()
|
||||||
|
: DateTime.now()),
|
||||||
|
changementMdpObligatoire:
|
||||||
|
json['changement_mdp_obligatoire'] == true,
|
||||||
|
nom: json['nom']?.toString(),
|
||||||
|
prenom: json['prenom']?.toString(),
|
||||||
|
statut: json['statut']?.toString(),
|
||||||
|
telephone: json['telephone']?.toString(),
|
||||||
|
photoUrl: json['photo_url']?.toString(),
|
||||||
|
adresse: json['adresse']?.toString(),
|
||||||
|
ville: json['ville']?.toString(),
|
||||||
|
codePostal: json['code_postal']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +78,17 @@ class AppUser {
|
|||||||
'role': role,
|
'role': role,
|
||||||
'createdAt': createdAt.toIso8601String(),
|
'createdAt': createdAt.toIso8601String(),
|
||||||
'updatedAt': updatedAt.toIso8601String(),
|
'updatedAt': updatedAt.toIso8601String(),
|
||||||
|
'changement_mdp_obligatoire': changementMdpObligatoire,
|
||||||
|
'nom': nom,
|
||||||
|
'prenom': prenom,
|
||||||
|
'statut': statut,
|
||||||
|
'telephone': telephone,
|
||||||
|
'photo_url': photoUrl,
|
||||||
|
'adresse': adresse,
|
||||||
|
'ville': ville,
|
||||||
|
'code_postal': codePostal,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
String get fullName => '${prenom ?? ''} ${nom ?? ''}'.trim();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:io'; // Pour File
|
||||||
|
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
// import 'package:p_tits_pas/models/child.dart'; // Commenté car fichier non trouvé
|
||||||
|
|
||||||
|
class ParentData {
|
||||||
|
String firstName;
|
||||||
|
String lastName;
|
||||||
|
String address; // Rue et numéro
|
||||||
|
String postalCode; // Ajout
|
||||||
|
String city; // Ajout
|
||||||
|
String phone;
|
||||||
|
String email;
|
||||||
|
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
|
||||||
|
File? profilePicture; // Chemin ou objet File
|
||||||
|
|
||||||
|
ParentData({
|
||||||
|
this.firstName = '',
|
||||||
|
this.lastName = '',
|
||||||
|
this.address = '', // Rue
|
||||||
|
this.postalCode = '', // Ajout
|
||||||
|
this.city = '', // Ajout
|
||||||
|
this.phone = '',
|
||||||
|
this.email = '',
|
||||||
|
this.password = '',
|
||||||
|
this.profilePicture,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChildData {
|
||||||
|
String firstName;
|
||||||
|
String lastName;
|
||||||
|
String dob; // Date de naissance ou prévisionnelle
|
||||||
|
bool photoConsent;
|
||||||
|
bool multipleBirth;
|
||||||
|
bool isUnbornChild;
|
||||||
|
File? imageFile;
|
||||||
|
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
|
||||||
|
|
||||||
|
ChildData({
|
||||||
|
this.firstName = '',
|
||||||
|
this.lastName = '',
|
||||||
|
this.dob = '',
|
||||||
|
this.photoConsent = false,
|
||||||
|
this.multipleBirth = false,
|
||||||
|
this.isUnbornChild = false,
|
||||||
|
this.imageFile,
|
||||||
|
required this.cardColor, // Rendre requis dans le constructeur
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nouvelle classe pour les détails bancaires
|
||||||
|
class BankDetails {
|
||||||
|
String bankName;
|
||||||
|
String iban;
|
||||||
|
String bic;
|
||||||
|
|
||||||
|
BankDetails({
|
||||||
|
this.bankName = '',
|
||||||
|
this.iban = '',
|
||||||
|
this.bic = '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserRegistrationData extends ChangeNotifier {
|
||||||
|
ParentData parent1;
|
||||||
|
ParentData? parent2; // Optionnel
|
||||||
|
List<ChildData> children;
|
||||||
|
String motivationText;
|
||||||
|
bool cguAccepted;
|
||||||
|
BankDetails? bankDetails; // Ajouté
|
||||||
|
String attestationCafNumber; // Ajouté
|
||||||
|
bool consentQuotientFamilial; // Ajouté
|
||||||
|
|
||||||
|
UserRegistrationData({
|
||||||
|
ParentData? parent1Data,
|
||||||
|
this.parent2,
|
||||||
|
List<ChildData>? childrenData,
|
||||||
|
this.motivationText = '',
|
||||||
|
this.cguAccepted = false,
|
||||||
|
this.bankDetails, // Ajouté
|
||||||
|
this.attestationCafNumber = '', // Ajouté
|
||||||
|
this.consentQuotientFamilial = false, // Ajouté
|
||||||
|
}) : parent1 = parent1Data ?? ParentData(),
|
||||||
|
children = childrenData ?? [];
|
||||||
|
|
||||||
|
// Méthode pour ajouter/mettre à jour le parent 1
|
||||||
|
void updateParent1(ParentData data) {
|
||||||
|
parent1 = data;
|
||||||
|
notifyListeners(); // Notifier les changements
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour ajouter/mettre à jour le parent 2
|
||||||
|
void updateParent2(ParentData? data) {
|
||||||
|
parent2 = data;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour ajouter un enfant
|
||||||
|
void addChild(ChildData child) {
|
||||||
|
children.add(child);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
|
||||||
|
void updateChild(int index, ChildData child) {
|
||||||
|
if (index >= 0 && index < children.length) {
|
||||||
|
children[index] = child;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour supprimer un enfant
|
||||||
|
void removeChild(int index) {
|
||||||
|
if (index >= 0 && index < children.length) {
|
||||||
|
children.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour la motivation
|
||||||
|
void updateMotivation(String text) {
|
||||||
|
motivationText = text;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour les informations bancaires et CAF
|
||||||
|
void updateFinancialInfo({
|
||||||
|
BankDetails? bankDetails,
|
||||||
|
String? attestationCafNumber,
|
||||||
|
bool? consentQuotientFamilial,
|
||||||
|
}) {
|
||||||
|
if (bankDetails != null) this.bankDetails = bankDetails;
|
||||||
|
if (attestationCafNumber != null) this.attestationCafNumber = attestationCafNumber;
|
||||||
|
if (consentQuotientFamilial != null) this.consentQuotientFamilial = consentQuotientFamilial;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepter les CGU
|
||||||
|
void acceptCGU(bool accepted) { // Prend un booléen
|
||||||
|
cguAccepted = accepted;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthode pour vérifier si toutes les données requises sont là (simplifié)
|
||||||
|
bool isRegistrationComplete() {
|
||||||
|
// Ajouter ici les validations nécessaires
|
||||||
|
// Exemple : parent1 doit avoir des champs remplis, au moins un enfant, CGU acceptées
|
||||||
|
return parent1.firstName.isNotEmpty &&
|
||||||
|
parent1.lastName.isNotEmpty &&
|
||||||
|
children.isNotEmpty &&
|
||||||
|
cguAccepted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
|
||||||
import 'package:p_tits_pas/screens/administrateurs/admin_dashboardScreen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/am/am_register_step4_sceen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/home/parent_screen/ParentDashboardScreen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/home/parent_screen/find_nanny.dart';
|
|
||||||
import 'package:p_tits_pas/screens/legal/legal_page.dart';
|
|
||||||
import 'package:p_tits_pas/screens/legal/privacy_page.dart';
|
|
||||||
import '../screens/auth/login_screen.dart';
|
|
||||||
import '../screens/auth/register_choice_screen.dart';
|
|
||||||
import '../screens/auth/parent/parent_register_step1_screen.dart';
|
|
||||||
import '../screens/auth/parent/parent_register_step2_screen.dart';
|
|
||||||
import '../screens/auth/parent/parent_register_step3_screen.dart';
|
|
||||||
import '../screens/auth/parent/parent_register_step4_screen.dart';
|
|
||||||
import '../screens/auth/parent/parent_register_step5_screen.dart';
|
|
||||||
import '../models/parent_user_registration_data.dart';
|
|
||||||
|
|
||||||
class AppRouter {
|
|
||||||
static const String login = '/login';
|
|
||||||
static const String registerChoice = '/register-choice';
|
|
||||||
static const String legal = '/legal';
|
|
||||||
static const String privacy = '/privacy';
|
|
||||||
static const String parentRegisterStep1 = '/parent-register/step1';
|
|
||||||
static const String parentRegisterStep2 = '/parent-register/step2';
|
|
||||||
static const String parentRegisterStep3 = '/parent-register/step3';
|
|
||||||
static const String parentRegisterStep4 = '/parent-register/step4';
|
|
||||||
static const String parentRegisterStep5 = '/parent-register/step5';
|
|
||||||
|
|
||||||
static const String amRegisterStep1 = '/am-register/step1';
|
|
||||||
static const String amRegisterStep2 = '/am-register/step2';
|
|
||||||
static const String amRegisterStep3 = '/am-register/step3';
|
|
||||||
static const String amRegisterStep4 = '/am-register/step4';
|
|
||||||
static const String parentDashboard = '/parent-dashboard';
|
|
||||||
static const String admin_dashboard = '/admin_dashboard';
|
|
||||||
static const String findNanny = '/find-nanny';
|
|
||||||
|
|
||||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
|
||||||
Widget screen;
|
|
||||||
bool slideTransition = false;
|
|
||||||
Object? args = settings.arguments;
|
|
||||||
|
|
||||||
Widget buildErrorScreen(String step) {
|
|
||||||
print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step");
|
|
||||||
return const ParentRegisterStep1Screen();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (settings.name) {
|
|
||||||
case login:
|
|
||||||
screen = const LoginPage();
|
|
||||||
break;
|
|
||||||
case registerChoice:
|
|
||||||
screen = const RegisterChoiceScreen();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case legal:
|
|
||||||
screen = const LegalPage();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case privacy:
|
|
||||||
screen = const PrivacyPage();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentRegisterStep1:
|
|
||||||
screen = ParentRegisterStep1Screen();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentRegisterStep2:
|
|
||||||
if (args is UserRegistrationData) {
|
|
||||||
screen = ParentRegisterStep2Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = buildErrorScreen('2');
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentRegisterStep3:
|
|
||||||
if (args is UserRegistrationData) {
|
|
||||||
screen = ParentRegisterStep3Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = buildErrorScreen('3');
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentRegisterStep4:
|
|
||||||
if (args is UserRegistrationData) {
|
|
||||||
screen = ParentRegisterStep4Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = buildErrorScreen('4');
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentRegisterStep5:
|
|
||||||
if (args is UserRegistrationData) {
|
|
||||||
screen = ParentRegisterStep5Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = buildErrorScreen('5');
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case amRegisterStep1:
|
|
||||||
screen = const AmRegisterStep1Screen();
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case amRegisterStep2:
|
|
||||||
if (args is ChildminderRegistrationData) {
|
|
||||||
screen = AmRegisterStep2Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = AmRegisterStep2Screen(registrationData: ChildminderRegistrationData());
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case amRegisterStep3:
|
|
||||||
if (args is ChildminderRegistrationData) {
|
|
||||||
screen = AmRegisterStep3Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = AmRegisterStep3Screen(registrationData: ChildminderRegistrationData());
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case amRegisterStep4:
|
|
||||||
if (args is ChildminderRegistrationData) {
|
|
||||||
screen = AmRegisterStep4Screen(registrationData: args);
|
|
||||||
} else {
|
|
||||||
screen = AmRegisterStep4Screen(registrationData: ChildminderRegistrationData());
|
|
||||||
}
|
|
||||||
slideTransition = true;
|
|
||||||
break;
|
|
||||||
case parentDashboard:
|
|
||||||
screen = const ParentDashboardScreen();
|
|
||||||
break;
|
|
||||||
case admin_dashboard:
|
|
||||||
screen = const AdminDashboardScreen();
|
|
||||||
break;
|
|
||||||
case findNanny:
|
|
||||||
screen = const FindNannyScreen();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
screen = Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text('Route non définie : ${settings.name}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slideTransition) {
|
|
||||||
return PageRouteBuilder(
|
|
||||||
pageBuilder: (context, animation, secondaryAnimation) => screen,
|
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
const begin = Offset(1.0, 0.0);
|
|
||||||
const end = Offset.zero;
|
|
||||||
const curve = Curves.easeInOut;
|
|
||||||
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
|
||||||
var offsetAnimation = animation.drive(tween);
|
|
||||||
return SlideTransition(position: offsetAnimation, child: child);
|
|
||||||
},
|
|
||||||
transitionDuration: const Duration(milliseconds: 400),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return MaterialPageRoute(builder: (_) => screen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/parametres_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||||
|
|
||||||
@@ -9,20 +12,55 @@ class AdminDashboardScreen extends StatefulWidget {
|
|||||||
const AdminDashboardScreen({super.key});
|
const AdminDashboardScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_AdminDashboardScreenState createState() => _AdminDashboardScreenState();
|
State<AdminDashboardScreen> createState() => _AdminDashboardScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||||
int selectedIndex = 0;
|
bool? _setupCompleted;
|
||||||
|
int mainTabIndex = 0;
|
||||||
|
int subIndex = 0;
|
||||||
|
|
||||||
void onTabChange(int index) {
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSetupStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSetupStatus() async {
|
||||||
|
try {
|
||||||
|
final completed = await ConfigurationService.getSetupStatus();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_setupCompleted = completed;
|
||||||
|
if (!completed) mainTabIndex = 1;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) setState(() {
|
||||||
|
_setupCompleted = false;
|
||||||
|
mainTabIndex = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onMainTabChange(int index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedIndex = index;
|
mainTabIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSubTabChange(int index) {
|
||||||
|
setState(() {
|
||||||
|
subIndex = index;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_setupCompleted == null) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(60.0),
|
preferredSize: const Size.fromHeight(60.0),
|
||||||
@@ -33,13 +71,19 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: DashboardAppBarAdmin(
|
child: DashboardAppBarAdmin(
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: mainTabIndex,
|
||||||
onTabChange: onTabChange,
|
onTabChange: onMainTabChange,
|
||||||
|
setupCompleted: _setupCompleted!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (mainTabIndex == 0)
|
||||||
|
DashboardUserManagementSubBar(
|
||||||
|
selectedSubIndex: subIndex,
|
||||||
|
onSubTabChange: onSubTabChange,
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _getBody(),
|
child: _getBody(),
|
||||||
),
|
),
|
||||||
@@ -50,17 +94,20 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _getBody() {
|
Widget _getBody() {
|
||||||
switch (selectedIndex) {
|
if (mainTabIndex == 1) {
|
||||||
|
return ParametresPanel(redirectToLoginAfterSave: !_setupCompleted!);
|
||||||
|
}
|
||||||
|
switch (subIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
return const GestionnaireManagementWidget();
|
return const GestionnaireManagementWidget();
|
||||||
case 1:
|
case 1:
|
||||||
return const ParentManagementWidget();
|
return const ParentManagementWidget();
|
||||||
case 2:
|
case 2:
|
||||||
return const AssistanteMaternelleManagementWidget();
|
return const AssistanteMaternelleManagementWidget();
|
||||||
case 3:
|
case 3:
|
||||||
return const Center(child: Text("👨💼 Administrateurs"));
|
return const AdminManagementWidget();
|
||||||
default:
|
default:
|
||||||
return const Center(child: Text("Page non trouvée"));
|
return const Center(child: Text('Page non trouvée'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||