feat(users): refactor users system

This commit is contained in:
2026-04-19 16:42:33 -04:00
parent af8da2aa86
commit f08376d979
24 changed files with 1531 additions and 669 deletions
+148 -7
View File
@@ -9,6 +9,7 @@
import { query, updateById } from '@zen/core/database';
import { updateUser } from './lib/auth.js';
import { listRoles, getRoleById, createRole, updateRole, deleteRole, getUserRoles, assignUserRole, revokeUserRole } from '@zen/core/users';
import { uploadImage, deleteFile, generateUniqueFilename, getFileExtension, FILE_TYPE_PRESETS, FILE_SIZE_LIMITS, validateUpload } from '@zen/core/storage';
const generateUserFilePath = (userId, category, filename) => `users/${userId}/${category}/${filename}`;
@@ -339,6 +340,138 @@ async function handleDeleteProfilePicture(_request, _params, { session }) {
}
}
// ---------------------------------------------------------------------------
// GET /zen/api/users/:id/roles (admin only)
// ---------------------------------------------------------------------------
async function handleGetUserRoles(_request, { id: userId }) {
const roles = await getUserRoles(userId);
return apiSuccess({ roles });
}
// ---------------------------------------------------------------------------
// POST /zen/api/users/:id/roles (admin only)
// ---------------------------------------------------------------------------
async function handleAssignUserRole(request, { id: userId }) {
try {
const body = await request.json();
const { roleId } = body;
if (!roleId) return apiError('Bad Request', 'roleId is required');
const roleCheck = await query(`SELECT id FROM zen_auth_roles WHERE id = $1`, [roleId]);
if (roleCheck.rows.length === 0) return apiError('Not Found', 'Role not found');
await assignUserRole(userId, roleId);
return apiSuccess({ success: true });
} catch (error) {
logAndObscureError(error, null);
return apiError('Internal Server Error', 'Failed to assign role');
}
}
// ---------------------------------------------------------------------------
// DELETE /zen/api/users/:id/roles/:roleId (admin only)
// ---------------------------------------------------------------------------
async function handleRevokeUserRole(_request, { id: userId, roleId }) {
try {
await revokeUserRole(userId, roleId);
return apiSuccess({ success: true });
} catch (error) {
logAndObscureError(error, null);
return apiError('Internal Server Error', 'Failed to revoke role');
}
}
// ---------------------------------------------------------------------------
// GET /zen/api/roles (admin only)
// ---------------------------------------------------------------------------
async function handleListRoles() {
const roles = await listRoles();
return apiSuccess({ roles });
}
// ---------------------------------------------------------------------------
// POST /zen/api/roles (admin only)
// ---------------------------------------------------------------------------
async function handleCreateRole(request) {
try {
const body = await request.json();
const { name, description, color } = body;
if (!name || !String(name).trim()) {
return apiError('Bad Request', 'Role name is required');
}
const role = await createRole({
name: String(name).trim(),
description: description ? String(description).trim() : null,
color: color ? String(color) : '#6b7280'
});
return apiSuccess({ role });
} catch (error) {
if (error.message === 'Role name is required') return apiError('Bad Request', error.message);
logAndObscureError(error, null);
return apiError('Internal Server Error', 'Failed to create role');
}
}
// ---------------------------------------------------------------------------
// GET /zen/api/roles/:id (admin only)
// ---------------------------------------------------------------------------
async function handleGetRole(_request, { id: roleId }) {
const role = await getRoleById(roleId);
if (!role) return apiError('Not Found', 'Role not found');
return apiSuccess({ role });
}
// ---------------------------------------------------------------------------
// PUT /zen/api/roles/:id (admin only)
// ---------------------------------------------------------------------------
async function handleUpdateRole(request, { id: roleId }) {
try {
const body = await request.json();
const { name, description, color, permissionKeys } = body;
const role = await updateRole(roleId, {
...(name !== undefined && { name: String(name).trim() }),
...(description !== undefined && { description: description ? String(description).trim() : null }),
...(color !== undefined && { color: String(color) }),
...(permissionKeys !== undefined && { permissionKeys: Array.isArray(permissionKeys) ? permissionKeys : [] })
});
return apiSuccess({ role });
} catch (error) {
if (error.message === 'Role not found') return apiError('Not Found', error.message);
if (error.message === 'Role name cannot be empty') return apiError('Bad Request', error.message);
logAndObscureError(error, null);
return apiError('Internal Server Error', 'Failed to update role');
}
}
// ---------------------------------------------------------------------------
// DELETE /zen/api/roles/:id (admin only)
// ---------------------------------------------------------------------------
async function handleDeleteRole(_request, { id: roleId }) {
try {
await deleteRole(roleId);
return apiSuccess({ success: true });
} catch (error) {
if (error.message === 'Cannot delete a system role') return apiError('Bad Request', error.message);
if (error.message === 'Role not found') return apiError('Not Found', error.message);
logAndObscureError(error, null);
return apiError('Internal Server Error', 'Failed to delete role');
}
}
// ---------------------------------------------------------------------------
// Route definitions
// ---------------------------------------------------------------------------
@@ -347,11 +480,19 @@ async function handleDeleteProfilePicture(_request, _params, { session }) {
// parameterised paths (/users/:id) so they match first.
export const routes = defineApiRoutes([
{ path: '/users', method: 'GET', handler: handleListUsers, auth: 'admin' },
{ path: '/users/me', method: 'GET', handler: handleGetCurrentUser, auth: 'user' },
{ path: '/users/profile', method: 'PUT', handler: handleUpdateProfile, auth: 'user' },
{ path: '/users/profile/picture', method: 'POST', handler: handleUploadProfilePicture, auth: 'user' },
{ path: '/users/profile/picture', method: 'DELETE', handler: handleDeleteProfilePicture, auth: 'user' },
{ path: '/users/:id', method: 'GET', handler: handleGetUserById, auth: 'admin' },
{ path: '/users/:id', method: 'PUT', handler: handleUpdateUserById, auth: 'admin' },
{ path: '/users', method: 'GET', handler: handleListUsers, auth: 'admin' },
{ path: '/users/me', method: 'GET', handler: handleGetCurrentUser, auth: 'user' },
{ path: '/users/profile', method: 'PUT', handler: handleUpdateProfile, auth: 'user' },
{ path: '/users/profile/picture', method: 'POST', handler: handleUploadProfilePicture, auth: 'user' },
{ path: '/users/profile/picture', method: 'DELETE', handler: handleDeleteProfilePicture, auth: 'user' },
{ path: '/users/:id/roles', method: 'GET', handler: handleGetUserRoles, auth: 'admin' },
{ path: '/users/:id/roles', method: 'POST', handler: handleAssignUserRole, auth: 'admin' },
{ path: '/users/:id/roles/:roleId', method: 'DELETE', handler: handleRevokeUserRole, auth: 'admin' },
{ path: '/users/:id', method: 'GET', handler: handleGetUserById, auth: 'admin' },
{ path: '/users/:id', method: 'PUT', handler: handleUpdateUserById, auth: 'admin' },
{ path: '/roles', method: 'GET', handler: handleListRoles, auth: 'admin' },
{ path: '/roles', method: 'POST', handler: handleCreateRole, auth: 'admin' },
{ path: '/roles/:id', method: 'GET', handler: handleGetRole, auth: 'admin' },
{ path: '/roles/:id', method: 'PUT', handler: handleUpdateRole, auth: 'admin' },
{ path: '/roles/:id', method: 'DELETE', handler: handleDeleteRole, auth: 'admin' },
]);