Files
siop2/apps/api/prisma/seed.ts
pr-daaif 8adb561b63 feat(r0.10): apps/api — auth fermée par défaut, matrice en base, démo-login ADR-002, seed
- packages/shared : rôles/catégories, schémas Zod, contrat d'API ;
  pnpm contract → docs/openapi.json committée (règle d'or ADR-001)
- apps/api : NestJS 11 + Prisma 6, migration r0_identity (Role/Permission/User) ;
  guard JWT global + @Public() ; PermissionsGuard (@RequirePermission,
  matrice relue en base, cache 60 s) ; FileStorage (seul import MinIO) ; /health
- démo-login ADR-002 : module conditionnel DEMO_MODE (404 sinon, testé e2e),
  double verrou production, refus des comptes isDemo=false
- seed idempotent : 7 rôles, matrice complète (70 lignes), 7 comptes démo
- 19 tests Jest (unit + e2e) ; smoke test sur build de prod

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:55:04 +01:00

169 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Seed R0 — idempotent (upserts) : 7 rôles, matrice COMPLÈTE (une ligne par
* rôle × catégorie, invariant R0) et 7 comptes de démonstration (ADR-002).
* La matrice ci-dessous est le point de DÉPART pédagogique : elle vit en base
* et sera administrable (R1) — la modifier ici ne change pas une base déjà
* seedée (les lignes existantes ne sont pas écrasées, voir plus bas).
* Usage : pnpm seed (ou prisma db seed)
*/
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import {
OBJECT_CATEGORIES,
ROLE_NAMES,
type ObjectCategory,
type RoleName,
} from '@siop/shared';
import * as argon2 from 'argon2';
type Grant = Partial<{
view: boolean;
viewOther: boolean;
create: boolean;
edit: boolean;
delete: boolean;
}>;
const FULL: Grant = { view: true, viewOther: true, create: true, edit: true, delete: true };
const READ: Grant = { view: true, viewOther: true };
/** Matrice de départ (rôle → catégorie → droits ; absent = tout à false). */
const MATRIX: Record<RoleName, Partial<Record<ObjectCategory, Grant>>> = {
Administrateur: Object.fromEntries(
OBJECT_CATEGORIES.map((c) => [c, FULL]),
) as Record<ObjectCategory, Grant>,
Gestionnaire: {
WORK_ORDERS: FULL,
REQUESTS: FULL,
ASSETS: FULL,
LOCATIONS: FULL,
METERS: FULL,
PARTS: FULL,
PURCHASE_ORDERS: FULL,
PEOPLE_TEAMS: { view: true, viewOther: true, create: true, edit: true },
ANALYTICS: READ,
SETTINGS: { view: true },
},
Dispatcher: {
WORK_ORDERS: { view: true, viewOther: true, create: true, edit: true },
REQUESTS: { view: true, viewOther: true, create: true, edit: true },
ASSETS: READ,
LOCATIONS: READ,
METERS: READ,
PARTS: { view: true },
PEOPLE_TEAMS: READ,
ANALYTICS: READ,
},
Technicien: {
WORK_ORDERS: { view: true, edit: true }, // ses OT uniquement (viewOther=false)
REQUESTS: { view: true },
ASSETS: READ,
LOCATIONS: READ,
METERS: { view: true, viewOther: true, create: true }, // relevés
PARTS: { view: true },
},
'Technicien limité': {
WORK_ORDERS: { view: true, edit: true }, // ses OT, sans consultation du parc
ASSETS: { view: true },
},
Demandeur: {
REQUESTS: { view: true, create: true }, // ses demandes uniquement
},
'Vue seule': {
WORK_ORDERS: READ,
REQUESTS: READ,
ASSETS: READ,
LOCATIONS: READ,
METERS: READ,
PARTS: READ,
PURCHASE_ORDERS: READ,
PEOPLE_TEAMS: READ,
ANALYTICS: READ,
},
};
/** 7 comptes démo — un par rôle (personas des maquettes 02-design). */
const DEMO_USERS: { email: string; displayName: string; role: RoleName }[] = [
{ email: 'admin@demo.siop.ma', displayName: 'Amina Benali', role: 'Administrateur' },
{ email: 'dispatcher@demo.siop.ma', displayName: 'Salma Radi', role: 'Dispatcher' },
{ email: 'technicien@demo.siop.ma', displayName: 'Ahmed Meskini', role: 'Technicien' },
{ email: 'technicien-limite@demo.siop.ma', displayName: 'Yassine Bouzid', role: 'Technicien limité' },
{ email: 'gestionnaire@demo.siop.ma', displayName: 'Nadia Cherkaoui', role: 'Gestionnaire' },
{ email: 'demandeur@demo.siop.ma', displayName: 'Karim El Fassi', role: 'Demandeur' },
{ email: 'vue-seule@demo.siop.ma', displayName: 'Omar Senhaji', role: 'Vue seule' },
];
export async function seed(prisma: PrismaClient): Promise<void> {
const roleIds = new Map<RoleName, string>();
for (const name of ROLE_NAMES) {
const role = await prisma.role.upsert({
where: { name },
update: {},
create: { name },
});
roleIds.set(name, role.id);
}
// Matrice complète : une ligne par rôle × catégorie. Les lignes existantes
// ne sont PAS écrasées (la base est la source de vérité, pas ce fichier).
for (const name of ROLE_NAMES) {
const roleId = roleIds.get(name)!;
for (const category of OBJECT_CATEGORIES) {
const g = MATRIX[name][category] ?? {};
await prisma.permission.upsert({
where: { roleId_objectCategory: { roleId, objectCategory: category } },
update: {},
create: {
roleId,
objectCategory: category,
canView: g.view ?? false,
canViewOther: g.viewOther ?? false,
canCreate: g.create ?? false,
canEdit: g.edit ?? false,
canDelete: g.delete ?? false,
},
});
}
}
// Mot de passe commun des comptes démo (la connexion classique reste testable).
const passwordHash = await argon2.hash(
process.env.SEED_DEMO_PASSWORD ?? 'Demo!2026',
);
for (const u of DEMO_USERS) {
await prisma.user.upsert({
where: { email: u.email },
update: {},
create: {
email: u.email,
displayName: u.displayName,
passwordHash,
roleId: roleIds.get(u.role)!,
isDemo: true,
},
});
}
}
/* c8 ignore start — wrapper CLI */
if (require.main === module) {
const prisma = new PrismaClient();
seed(prisma)
.then(async () => {
const [roles, permissions, users] = await Promise.all([
prisma.role.count(),
prisma.permission.count(),
prisma.user.count({ where: { isDemo: true } }),
]);
console.log(
`Seed OK — ${roles} rôles, ${permissions} lignes de matrice, ${users} comptes démo.`,
);
})
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());
}
/* c8 ignore stop */