Mise en place des workspaces

This commit is contained in:
ocroguennec committed 2026-09-08 12:33:04 +02:00
1 parent be3c93897b
commit 6b6cad7cd6
19 files changed
+1273 -56

No files matched your search

+46 -2
View File
@@ -6,7 +6,7 @@ import path from 'node:path';
import os from 'node:os';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import db from '../db/index.js';
import db, { grantDefaultWorkspace } from '../db/index.js';
import Database from 'better-sqlite3';
import { HttpError } from '../middleware/errorHandler.js';
import { checkStatutsRetard } from '../jobs/autoStatut.js';
@@ -79,10 +79,17 @@ router.get('/users', (req, res) => {
JOIN investisseurs inv ON inv.id = i.investisseur_id
WHERE inv.user_id = users.id) AS nb_investissements,
(SELECT COUNT(*) FROM documents d WHERE d.user_id = users.id) AS nb_documents,
(SELECT COALESCE(SUM(d.taille_octets), 0) FROM documents d WHERE d.user_id = users.id) AS taille_documents
(SELECT COALESCE(SUM(d.taille_octets), 0) FROM documents d WHERE d.user_id = users.id) AS taille_documents,
(SELECT GROUP_CONCAT(workspace_id) FROM user_workspaces uw WHERE uw.user_id = users.id AND uw.granted_by_admin = 1) AS workspace_ids
FROM users
ORDER BY id ASC
`).all();
// workspace_ids : GROUP_CONCAT renvoie une chaîne "1,2" ou null — converti ici
// en tableau de nombres pour rester cohérent avec le format attendu côté
// frontend (ManageRightsModal), plutôt que de parser la chaîne à chaque endroit.
for (const u of users) {
u.workspace_ids = u.workspace_ids ? u.workspace_ids.split(',').map(Number) : [];
}
res.json(users);
});
@@ -131,6 +138,10 @@ router.post('/users', (req, res, next) => {
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
).run(userId, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
// Accorde l'accès au workspace socle (crowdlending), comme tout nouveau compte
// — cf. project_workspaces_transformation.md.
grantDefaultWorkspace(userId);
audit(req, { action: 'user_created', category: 'account', actorId: req.user.id, targetUserId: userId, details: { email: body.email, role: body.role, created_by_admin: true } });
res.status(201).json({ id: userId, email: body.email, display_name: body.displayName || null, role: body.role });
} catch (e) { next(e); }
@@ -173,6 +184,39 @@ router.patch('/users/:id/ai-access', (req, res, next) => {
} catch (e) { next(e); }
});
/** Définit les workspaces accordés à un utilisateur (octroi admin) — remplace
* intégralement l'ensemble accordé (granted_by_admin) par la liste fournie.
* L'activation personnelle (active_by_user) n'est jamais touchée pour un
* workspace déjà accordé, pour ne pas écraser un choix déjà fait par
* l'utilisateur ; elle est mise à 1 par défaut pour un nouvel octroi. */
const PatchWorkspacesSchema = z.object({
workspaceIds: z.array(z.number().int()),
});
router.patch('/users/:id/workspaces', (req, res, next) => {
try {
const { workspaceIds } = PatchWorkspacesSchema.parse(req.body);
const targetId = Number(req.params.id);
const target = db.prepare('SELECT id FROM users WHERE id = ?').get(targetId);
if (!target) throw new HttpError(404, 'Utilisateur introuvable');
const applyGrants = db.transaction(() => {
db.prepare('UPDATE user_workspaces SET granted_by_admin = 0 WHERE user_id = ?').run(targetId);
for (const wsId of workspaceIds) {
db.prepare(`
INSERT INTO user_workspaces (user_id, workspace_id, granted_by_admin, active_by_user, granted_at)
VALUES (?, ?, 1, 1, datetime('now'))
ON CONFLICT(user_id, workspace_id) DO UPDATE SET granted_by_admin = 1, granted_at = datetime('now')
`).run(targetId, wsId);
}
});
applyGrants();
audit(req, { action: 'workspaces_changed', category: 'workspaces', actorId: req.user.id, targetUserId: targetId, details: { workspace_ids: workspaceIds } });
res.json({ id: targetId, workspaceIds });
} catch (e) { next(e); }
});
/** Modifie le rôle d'un utilisateur */
const PatchRoleSchema = z.object({
role: z.enum(['user', 'admin']),