feat(admin): add session management tab to profile page
- add sessions tab with active session list in ProfilePage - fetch and display sessions with current session highlight - implement single and bulk session revocation with redirect on self-revoke - add session-related api helpers in auth api
This commit is contained in:
@@ -11,7 +11,7 @@ import { query, updateById, findOne } from '@zen/core/database';
|
||||
import { updateUser, requestPasswordReset } from './auth.js';
|
||||
import { hashPassword, verifyPassword } from '../../core/users/password.js';
|
||||
import { createEmailChangeToken, verifyEmailChangeToken, applyEmailChange, sendEmailChangeConfirmEmail, sendEmailChangeOldNotifyEmail, sendEmailChangeNewNotifyEmail, sendPasswordChangedEmail, sendPasswordResetEmail } from './email.js';
|
||||
import { listRoles, getRoleById, createRole, updateRole, deleteRole, getUserRoles, assignUserRole, revokeUserRole } from '@zen/core/users';
|
||||
import { listRoles, getRoleById, createRole, updateRole, deleteRole, getUserRoles, assignUserRole, revokeUserRole, deleteUserSessions } from '@zen/core/users';
|
||||
import { uploadImage, deleteFile, generateUniqueFilename, getFileExtension, FILE_TYPE_PRESETS, FILE_SIZE_LIMITS, validateUpload } from '@zen/core/storage';
|
||||
import { getPublicBaseUrl } from '@zen/core/shared/config';
|
||||
|
||||
@@ -627,6 +627,81 @@ async function handleDeleteRole(_request, { id: roleId }) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /zen/api/users/profile/sessions (user — list own sessions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseUserAgent(ua) {
|
||||
if (!ua) return { browser: 'Navigateur inconnu', os: 'Système inconnu', device: 'desktop' };
|
||||
const device = /Mobile|Android|iPhone|iPad/i.test(ua) ? 'mobile' : 'desktop';
|
||||
let os = 'Système inconnu';
|
||||
if (/Windows/i.test(ua)) os = 'Windows';
|
||||
else if (/Android/i.test(ua)) os = 'Android';
|
||||
else if (/iPhone|iPad/i.test(ua)) os = 'iOS';
|
||||
else if (/Mac OS X/i.test(ua)) os = 'macOS';
|
||||
else if (/Linux/i.test(ua)) os = 'Linux';
|
||||
let browser = 'Navigateur inconnu';
|
||||
if (/Edg\//i.test(ua)) browser = 'Edge';
|
||||
else if (/Chrome\//i.test(ua)) browser = 'Chrome';
|
||||
else if (/Firefox\//i.test(ua)) browser = 'Firefox';
|
||||
else if (/Safari\//i.test(ua)) browser = 'Safari';
|
||||
return { browser, os, device };
|
||||
}
|
||||
|
||||
async function handleListSessions(_request, _params, { session }) {
|
||||
try {
|
||||
const result = await query(
|
||||
'SELECT id, ip_address, user_agent, created_at, expires_at FROM zen_auth_sessions WHERE user_id = $1 ORDER BY created_at DESC',
|
||||
[session.user.id]
|
||||
);
|
||||
const sessions = result.rows.map(s => ({
|
||||
id: s.id,
|
||||
ip_address: s.ip_address,
|
||||
created_at: s.created_at,
|
||||
expires_at: s.expires_at,
|
||||
...parseUserAgent(s.user_agent),
|
||||
}));
|
||||
return apiSuccess({ sessions, currentSessionId: session.session.id });
|
||||
} catch (error) {
|
||||
logAndObscureError(error, null);
|
||||
return apiError('Internal Server Error', 'Impossible de récupérer les sessions');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /zen/api/users/profile/sessions (user — revoke all own sessions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function handleDeleteAllSessions(_request, _params, { session }) {
|
||||
try {
|
||||
await deleteUserSessions(session.user.id);
|
||||
return apiSuccess({ success: true });
|
||||
} catch (error) {
|
||||
logAndObscureError(error, null);
|
||||
return apiError('Internal Server Error', 'Impossible de révoquer les sessions');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /zen/api/users/profile/sessions/:sessionId (user — revoke one session)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function handleDeleteSession(_request, { sessionId }, { session }) {
|
||||
try {
|
||||
const result = await query(
|
||||
'DELETE FROM zen_auth_sessions WHERE id = $1 AND user_id = $2 RETURNING id',
|
||||
[sessionId, session.user.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return apiError('Not Found', 'Session introuvable');
|
||||
}
|
||||
return apiSuccess({ success: true, isCurrent: sessionId === session.session.id });
|
||||
} catch (error) {
|
||||
logAndObscureError(error, null);
|
||||
return apiError('Internal Server Error', 'Impossible de révoquer la session');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /zen/api/users/profile/password (user — change own password)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -746,6 +821,9 @@ export const routes = defineApiRoutes([
|
||||
{ path: '/users/profile/password', method: 'POST', handler: handleChangeOwnPassword, auth: 'user' },
|
||||
{ path: '/users/profile/picture', method: 'POST', handler: handleUploadProfilePicture, auth: 'user' },
|
||||
{ path: '/users/profile/picture', method: 'DELETE', handler: handleDeleteProfilePicture, auth: 'user' },
|
||||
{ path: '/users/profile/sessions', method: 'GET', handler: handleListSessions, auth: 'user' },
|
||||
{ path: '/users/profile/sessions', method: 'DELETE', handler: handleDeleteAllSessions, auth: 'user' },
|
||||
{ path: '/users/profile/sessions/:sessionId', method: 'DELETE', handler: handleDeleteSession, auth: 'user' },
|
||||
{ path: '/users/email/confirm', method: 'GET', handler: handleConfirmEmailChange, auth: 'user' },
|
||||
{ path: '/users/:id/roles', method: 'GET', handler: handleGetUserRoles, auth: 'admin' },
|
||||
{ path: '/users/:id/roles', method: 'POST', handler: handleAssignUserRole, auth: 'admin' },
|
||||
|
||||
Reference in New Issue
Block a user