fix: conformité stricte skill itinova-user-management

- Rôles : remplacement admin/approbateur/validateur/operateur → admin/standard/readonly
- schema.ts, migrate.ts : ENUM MySQL mis à jour (3 rôles skill)
- routes/auth.ts : rôle par défaut standard, validRoles, modèle CSV corrigé
- routes/invoices.ts : permissions readonly/standard/admin
- routes/dashboard.ts : compteurs dashboard selon standard/admin
- frontend/types/index.ts : type User role mis à jour
- frontend/utils/helpers.ts : roleLabels admin/standard/readonly
- frontend/pages/InvoiceDetail.tsx : actions disponibles selon standard/readonly/admin
- frontend/pages/UserList.tsx : rôle par défaut standard, labels import corrigés
This commit is contained in:
Manus Agent
2026-04-28 04:27:46 -04:00
parent 8d20df5646
commit 1fb8328fe1
9 changed files with 37 additions and 28 deletions

View File

@@ -31,7 +31,7 @@ async function migrate() {
password VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role ENUM('admin', 'approbateur', 'validateur', 'operateur') NOT NULL DEFAULT 'operateur',
role ENUM('admin', 'standard', 'readonly') NOT NULL DEFAULT 'standard',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL

View File

@@ -22,7 +22,7 @@ export const users = mysqlTable('users', {
password: varchar('password', { length: 255 }).notNull(),
firstName: varchar('first_name', { length: 100 }).notNull(),
lastName: varchar('last_name', { length: 100 }).notNull(),
role: mysqlEnum('role', ['admin', 'approbateur', 'validateur', 'operateur']).notNull().default('operateur'),
role: mysqlEnum('role', ['admin', 'standard', 'readonly']).notNull().default('standard'),
isActive: boolean('is_active').notNull().default(true),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(),

View File

@@ -153,7 +153,7 @@ router.post('/users', authenticate, authorize('admin'), async (req: AuthRequest,
const [result]: any = await pool.execute(
'INSERT INTO users (email, password, first_name, last_name, role) VALUES (?, ?, ?, ?, ?)',
[email, hashedPassword, firstName, lastName, role || 'operateur']
[email, hashedPassword, firstName, lastName, role || 'standard']
);
res.status(201).json({
@@ -161,7 +161,7 @@ router.post('/users', authenticate, authorize('admin'), async (req: AuthRequest,
email,
firstName,
lastName,
role: role || 'operateur',
role: role || 'standard',
});
} catch (error: any) {
if (error.code === 'ER_DUP_ENTRY') {
@@ -218,7 +218,7 @@ router.delete('/users/:id', authenticate, authorize('admin'), async (req: AuthRe
// GET /api/auth/users/import-template - Télécharger le modèle CSV
router.get('/users/import-template', authenticate, authorize('admin'), (_req: AuthRequest, res: Response) => {
const csvContent = 'email,prenom,nom,role\nadmin@exemple.com,Jean,Dupont,admin\nutilisateur@exemple.com,Marie,Martin,operateur\n';
const csvContent = 'email,prenom,nom,role\nadmin@exemple.com,Jean,Dupont,admin\nstandard@exemple.com,Marie,Martin,standard\nreadonly@exemple.com,Paul,Durand,readonly\n';
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="modele_import_utilisateurs.csv"');
res.send('\uFEFF' + csvContent); // BOM UTF-8 pour Excel
@@ -232,7 +232,7 @@ router.post('/users/import', authenticate, authorize('admin'), uploadMemory.sing
}
const pool = getPool();
const validRoles = ['admin', 'approbateur', 'validateur', 'operateur'];
const validRoles = ['admin', 'standard', 'readonly'];
const results = { created: 0, updated: 0, errors: [] as string[] };
// Parsing CSV ou Excel
@@ -265,7 +265,7 @@ router.post('/users/import', authenticate, authorize('admin'), uploadMemory.sing
const email = String(row[0] || '').trim().toLowerCase();
const prenom = String(row[1] || '').trim();
const nom = String(row[2] || '').trim();
const role = String(row[3] || 'operateur').trim().toLowerCase();
const role = String(row[3] || 'standard').trim().toLowerCase();
// Validation
if (!email || !email.includes('@')) {
@@ -276,7 +276,7 @@ router.post('/users/import', authenticate, authorize('admin'), uploadMemory.sing
results.errors.push(`Ligne ${lineNum} : prénom ou nom manquant`);
continue;
}
const finalRole = validRoles.includes(role) ? role : 'operateur';
const finalRole = validRoles.includes(role) ? role : 'standard';
// Vérifier si l'utilisateur existe
const [existing]: any = await pool.execute(

View File

@@ -67,28 +67,32 @@ router.get('/', authenticate, async (req: AuthRequest, res: Response) => {
'SELECT source, COUNT(*) as count FROM invoices GROUP BY source'
);
// Factures en attente d'action selon le rôle
// Factures en attente d'action selon le rôle (skill : admin / standard / readonly)
let pendingForUser: any[] = [];
const role = req.user!.role;
if (role === 'operateur' || role === 'admin') {
// standard et admin voient les factures reçues à vérifier
if (role === 'standard' || role === 'admin') {
const [rows]: any = await pool.execute(
`SELECT COUNT(*) as count FROM invoices WHERE status = 'recue'`
);
pendingForUser.push({ action: 'À vérifier', count: rows[0].count });
}
if (role === 'validateur' || role === 'admin') {
// standard et admin voient les factures en vérification à valider
if (role === 'standard' || role === 'admin') {
const [rows]: any = await pool.execute(
`SELECT COUNT(*) as count FROM invoices WHERE status = 'en_verification'`
);
pendingForUser.push({ action: 'À valider', count: rows[0].count });
}
if (role === 'approbateur' || role === 'admin') {
// standard et admin voient les factures validées à approuver
if (role === 'standard' || role === 'admin') {
const [rows]: any = await pool.execute(
`SELECT COUNT(*) as count FROM invoices WHERE status = 'validee'`
);
pendingForUser.push({ action: 'À approuver', count: rows[0].count });
}
// admin uniquement voit les factures approuvées à payer
if (role === 'admin') {
const [rows]: any = await pool.execute(
`SELECT COUNT(*) as count FROM invoices WHERE status = 'approuvee'`

View File

@@ -401,12 +401,14 @@ router.post('/:id/status', authenticate, async (req: AuthRequest, res: Response)
});
}
// Vérifier les permissions
// Vérifier les permissions (skill itinova-user-management : admin / standard / readonly)
const role = req.user!.role;
const rolePermissions: Record<string, string[]> = {
'operateur': ['en_verification', 'recue'],
'validateur': ['en_verification', 'validee', 'rejetee', 'recue'],
'approbateur': ['en_verification', 'validee', 'approuvee', 'rejetee', 'recue'],
// readonly : consultation uniquement, aucune modification de statut
'readonly': [],
// standard : droits métiers complets (validation, approbation, rejet)
'standard': ['en_verification', 'validee', 'approuvee', 'rejetee', 'recue'],
// admin : tous les droits y compris paiement et archivage
'admin': ['en_verification', 'validee', 'approuvee', 'payee', 'rejetee', 'recue', 'archivee'],
};