diff --git a/src/features/admin/pages.js b/src/features/admin/AdminPage.client.js similarity index 100% rename from src/features/admin/pages.js rename to src/features/admin/AdminPage.client.js diff --git a/src/features/admin/page.js b/src/features/admin/AdminPage.server.js similarity index 100% rename from src/features/admin/page.js rename to src/features/admin/AdminPage.server.js diff --git a/src/features/admin/actions.js b/src/features/admin/actions.js deleted file mode 100644 index da1e4b0..0000000 --- a/src/features/admin/actions.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Admin Server Actions - * - * Exported separately from admin/index.js to avoid bundling - * server-side code (which includes database imports) into client components. - * - * Usage: import { getDashboardStats } from '@zen/core/features/admin/actions'; - */ - -export { getDashboardStats } from './actions/statsActions.js'; diff --git a/src/features/admin/actions/statsActions.js b/src/features/admin/actions/statsActions.js deleted file mode 100644 index f6832a2..0000000 --- a/src/features/admin/actions/statsActions.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Admin Stats Actions - * - * @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place. - * Ce fichier est conservé pour la rétrocompatibilité avec les projets qui - * appellent getDashboardStats() directement depuis leur page admin. - */ - -'use server'; - -import { query } from '@zen/core/database'; -import { fail } from '@zen/core/shared/logger'; - -/** - * @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place. - * @returns {Promise<{ success: boolean, stats?: { totalUsers: number }, error?: string }>} - */ -export async function getDashboardStats() { - try { - const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`); - const totalUsers = parseInt(result.rows[0].count) || 0; - return { success: true, stats: { totalUsers } }; - } catch (error) { - fail(`Error getting dashboard stats: ${error.message}`); - return { success: false, error: error.message || 'Failed to get dashboard statistics' }; - } -} diff --git a/src/features/admin/components/AdminPages.js b/src/features/admin/components/AdminPages.js deleted file mode 100644 index 6772d25..0000000 --- a/src/features/admin/components/AdminPages.js +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import DashboardPage from './pages/DashboardPage.js'; -import UsersPage from './pages/UsersPage.js'; -import UserEditPage from './pages/UserEditPage.js'; -import ProfilePage from './pages/ProfilePage.js'; -import RolesPage from './pages/RolesPage.js'; -import RoleEditPage from './pages/RoleEditPage.js'; - -export default function AdminPagesClient({ params, user, dashboardStats = null }) { - const parts = params?.admin || []; - const page = parts[0] || 'dashboard'; - - if (page === 'users' && parts[1] === 'edit' && parts[2]) { - return ; - } - - if (page === 'roles' && parts[1] === 'edit' && parts[2]) { - return ; - } - - if (page === 'roles' && parts[1] === 'new') { - return ; - } - - const corePages = { - dashboard: () => , - users: () => , - profile: () => , - roles: () => , - }; - - const CorePageComponent = corePages[page]; - return CorePageComponent ? : ; -} diff --git a/src/features/admin/components/AdminPagesLayout.js b/src/features/admin/components/AdminShell.js similarity index 100% rename from src/features/admin/components/AdminPagesLayout.js rename to src/features/admin/components/AdminShell.js diff --git a/src/features/admin/dashboard/clientRegistry.js b/src/features/admin/dashboard/clientRegistry.js deleted file mode 100644 index 4b0f806..0000000 --- a/src/features/admin/dashboard/clientRegistry.js +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -/** - * Dashboard Client Registry — API générique - * - * Les features s'enregistrent ici via registerClientWidget(). - * Ce fichier ne doit jamais importer une feature directement. - * Les features dépendent de ce fichier, pas l'inverse. - */ - -const widgets = []; - -/** - * Enregistre un composant React pour le tableau de bord. - * Appelé en side effect par chaque feature (ex: auth/dashboard.widget.js). - * @param {string} id - Identifiant unique de la feature (doit correspondre à l'id serveur) - * @param {React.ComponentType} Component - Composant avec props { data, loading } - * @param {number} order - Ordre d'affichage (croissant). Utiliser des intervalles de 10. - */ -export function registerClientWidget(id, Component, order) { - widgets.push({ id, Component, order }); -} - -/** - * Retourne tous les widgets enregistrés, triés par order croissant. - * @returns {Array<{ id: string, Component: React.ComponentType, order: number }>} - */ -export function getClientWidgets() { - return [...widgets].sort((a, b) => a.order - b.order); -} diff --git a/src/features/admin/dashboard/registry.js b/src/features/admin/dashboard/registry.js deleted file mode 100644 index af41447..0000000 --- a/src/features/admin/dashboard/registry.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Dashboard Server Registry — API générique - * - * Les features s'enregistrent ici via registerServerWidget(). - * Ce fichier ne doit jamais importer une feature directement. - * Les features dépendent de ce fichier, pas l'inverse. - */ - -const widgets = new Map(); // id → fetcher - -/** - * Enregistre une fonction de fetch pour le tableau de bord. - * Appelé en side effect par chaque feature (ex: auth/dashboard.server.js). - * @param {string} id - Identifiant unique de la feature - * @param {() => Promise} fetcher - Fonction async retournant des données sérialisables - */ -export function registerServerWidget(id, fetcher) { - widgets.set(id, fetcher); -} - -/** - * Exécute tous les fetchers enregistrés en parallèle. - * Un fetcher qui échoue n'empêche pas les autres (Promise.allSettled). - * @returns {Promise>} Map featureId → données - */ -export async function collectAllDashboardData() { - const results = await Promise.allSettled( - Array.from(widgets.entries()).map(([id, fetcher]) => - fetcher().then(data => ({ id, data })) - ) - ); - - return results.reduce((acc, result) => { - if (result.status === 'fulfilled') { - acc[result.value.id] = result.value.data; - } - return acc; - }, {}); -} diff --git a/src/features/admin/dashboard/serverRegistry.js b/src/features/admin/dashboard/serverRegistry.js deleted file mode 100644 index 427244c..0000000 --- a/src/features/admin/dashboard/serverRegistry.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Dashboard Server Registry — Point d'entrée - * - * Ré-exporte l'API du registre pur et déclenche le câblage des features - * via un import side-effect de features/dashboard.server.js. - * - * Ne jamais importer une feature directement ici. - * Pour ajouter une feature : éditer src/features/dashboard.server.js. - */ - -export { collectAllDashboardData } from './registry.js'; - -// Side effect : initialise le registre, enregistre les widgets core, puis les features -import './registry.js'; -import './widgets/index.server.js'; -import '../../dashboard.server.js'; diff --git a/src/features/admin/dashboard/widgets/index.client.js b/src/features/admin/dashboard/widgets/index.client.js deleted file mode 100644 index 3077164..0000000 --- a/src/features/admin/dashboard/widgets/index.client.js +++ /dev/null @@ -1,10 +0,0 @@ -'use client'; - -/** - * Widgets core — Câblage client - * - * Ce fichier est le SEUL à modifier pour ajouter un widget core côté client. - * Les widgets core sont auto-enregistrés au démarrage de l'admin, sans module externe. - */ - -import './users.widget.js'; diff --git a/src/features/admin/dashboard/widgets/index.server.js b/src/features/admin/dashboard/widgets/index.server.js deleted file mode 100644 index f84abc1..0000000 --- a/src/features/admin/dashboard/widgets/index.server.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Widgets core — Câblage serveur - * - * Ce fichier est le SEUL à modifier pour ajouter un widget core côté serveur. - * Les widgets core sont auto-enregistrés au démarrage de l'admin, sans module externe. - */ - -import './users.server.js'; diff --git a/src/features/admin/navigation.server.js b/src/features/admin/navigation.server.js deleted file mode 100644 index dc6c0c4..0000000 --- a/src/features/admin/navigation.server.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Admin Navigation Builder (Server-Only) - * - * IMPORTANT: Navigation data must be serializable (no functions/components). - * Icons are passed as string names and resolved on the client. - */ - -/** - * Build complete navigation sections - * @param {string} pathname - Current pathname - * @returns {Array} Navigation sections (serializable, icons as strings) - */ -export function buildNavigationSections(pathname) { - const coreNavigation = [ - { - id: 'Dashboard', - title: 'Tableau de bord', - icon: 'DashboardSquare03Icon', - items: [ - { - name: 'Tableau de bord', - href: '/admin/dashboard', - icon: 'DashboardSquare03Icon', - current: pathname === '/admin/dashboard' - }, - ] - } - ]; - - const systemNavigation = [ - { - id: 'users', - title: 'Utilisateurs', - icon: 'UserMultiple02Icon', - items: [ - { - name: 'Utilisateurs', - href: '/admin/users', - icon: 'UserMultiple02Icon', - current: pathname.startsWith('/admin/users') - }, - { - name: 'Rôles', - href: '/admin/roles', - icon: 'Crown03Icon', - current: pathname.startsWith('/admin/roles') - }, - ] - } - ]; - - return [...coreNavigation, ...systemNavigation]; -} diff --git a/src/features/admin/components/pages/DashboardPage.js b/src/features/admin/pages/DashboardPage.client.js similarity index 100% rename from src/features/admin/components/pages/DashboardPage.js rename to src/features/admin/pages/DashboardPage.client.js diff --git a/src/features/admin/components/pages/ProfilePage.js b/src/features/admin/pages/ProfilePage.client.js similarity index 100% rename from src/features/admin/components/pages/ProfilePage.js rename to src/features/admin/pages/ProfilePage.client.js diff --git a/src/features/admin/components/pages/RoleEditPage.js b/src/features/admin/pages/RoleEditPage.client.js similarity index 100% rename from src/features/admin/components/pages/RoleEditPage.js rename to src/features/admin/pages/RoleEditPage.client.js diff --git a/src/features/admin/components/pages/RolesPage.js b/src/features/admin/pages/RolesPage.client.js similarity index 100% rename from src/features/admin/components/pages/RolesPage.js rename to src/features/admin/pages/RolesPage.client.js diff --git a/src/features/admin/components/pages/UserEditPage.js b/src/features/admin/pages/UserEditPage.client.js similarity index 100% rename from src/features/admin/components/pages/UserEditPage.js rename to src/features/admin/pages/UserEditPage.client.js diff --git a/src/features/admin/components/pages/UsersPage.js b/src/features/admin/pages/UsersPage.client.js similarity index 100% rename from src/features/admin/components/pages/UsersPage.js rename to src/features/admin/pages/UsersPage.client.js diff --git a/src/features/admin/middleware/protect.js b/src/features/admin/protect.js similarity index 100% rename from src/features/admin/middleware/protect.js rename to src/features/admin/protect.js diff --git a/src/features/admin/dashboard/widgets/users.widget.js b/src/features/admin/widgets/users.client.js similarity index 100% rename from src/features/admin/dashboard/widgets/users.widget.js rename to src/features/admin/widgets/users.client.js diff --git a/src/features/admin/dashboard/widgets/users.server.js b/src/features/admin/widgets/users.server.js similarity index 100% rename from src/features/admin/dashboard/widgets/users.server.js rename to src/features/admin/widgets/users.server.js diff --git a/src/features/auth/pages.js b/src/features/auth/AuthPage.client.js similarity index 100% rename from src/features/auth/pages.js rename to src/features/auth/AuthPage.client.js diff --git a/src/features/auth/page.js b/src/features/auth/AuthPage.server.js similarity index 100% rename from src/features/auth/page.js rename to src/features/auth/AuthPage.server.js diff --git a/src/features/auth/actions/authActions.js b/src/features/auth/actions/authActions.js deleted file mode 100644 index 0b22439..0000000 --- a/src/features/auth/actions/authActions.js +++ /dev/null @@ -1,434 +0,0 @@ -/** - * Server Actions for Next.js - * Authentication actions for login, register, password reset, etc. - */ - -'use server'; - -import { register, login, requestPasswordReset, resetPassword, verifyUserEmail } from '../lib/auth.js'; -import { validateSession, deleteSession } from '../lib/session.js'; -import { verifyEmailToken, verifyResetToken, sendVerificationEmail, sendPasswordResetEmail } from '../lib/email.js'; -import { fail } from '@zen/core/shared/logger'; -import { cookies, headers } from 'next/headers'; -import { getSessionCookieName, getPublicBaseUrl } from '@zen/core/shared/config'; -import { checkRateLimit, getIpFromHeaders, formatRetryAfter } from '@zen/core/shared/rate-limit'; - -/** - * Errors that are safe to surface verbatim to the client (e.g. "token expired"). - * All other errors — including library-layer and database errors — must be caught, - * logged server-side only, and replaced with a generic message to prevent internal - * detail disclosure. - */ -class UserFacingError extends Error { - constructor(message) { - super(message); - this.name = 'UserFacingError'; - } -} - -/** - * Get the client IP from the current server action context. - */ -async function getClientIp() { - const h = await headers(); - return getIpFromHeaders(h); -} - -// Emitted at most once per process lifetime to avoid log flooding while still -// alerting operators that per-IP rate limiting is inactive for server actions. -let _rateLimitUnavailableWarned = false; - -/** - * Apply per-IP rate limiting only when a real IP address is available. - * - * When IP resolves to 'unknown' (no trusted proxy configured), every caller - * shares the single bucket keyed ':unknown'. A single attacker can - * exhaust that bucket in 5 requests and impose a 30-minute denial-of-service - * on every legitimate user. Rate limiting is therefore suspended for the - * 'unknown' case and a one-time operator warning is emitted instead, - * mirroring the identical policy applied to API routes in router.js. - * - * Set ZEN_TRUST_PROXY=true only behind a verified reverse proxy (Nginx, - * Cloudflare, AWS ALB, …) that strips and rewrites forwarding headers. - * - * @param {string} ip - * @param {string} action - * @returns {{ allowed: boolean, retryAfterMs?: number } | null} null = suspended - */ -function enforceRateLimit(ip, action) { - if (ip === 'unknown') { - if (!_rateLimitUnavailableWarned) { - _rateLimitUnavailableWarned = true; - fail( - 'Rate limiting inactive (server actions): client IP cannot be determined. ' + - 'Set ZEN_TRUST_PROXY=true behind a verified reverse proxy to enable per-IP rate limiting.' - ); - } - return null; - } - return checkRateLimit(ip, action); -} - -/** - * Validate anti-bot fields submitted with forms. - * - _hp : honeypot field — must be empty - * - _t : form load timestamp (ms) — submission must be at least 1.5 s after page - * load AND no more than MAX_FORM_AGE_MS in the past. Both a lower bound - * (prevents instant automated submission) and an upper bound (prevents the - * trivial bypass of supplying an arbitrary past timestamp such as _t=1) are - * enforced. Future timestamps are also rejected. - * - * @param {FormData} formData - * @returns {{ valid: boolean, error?: string }} - */ -function validateAntiBotFields(formData) { - const honeypot = formData.get('_hp'); - if (honeypot && honeypot.length > 0) { - return { valid: false, error: 'Requête invalide' }; - } - - const MIN_ELAPSED_MS = 1_500; - const MAX_FORM_AGE_MS = 10 * 60 * 1_000; // 10 minutes — rejects epoch-era timestamps - const now = Date.now(); - const t = parseInt(formData.get('_t') || '0', 10); - const elapsed = now - t; - - if (t === 0 || t > now || elapsed < MIN_ELAPSED_MS || elapsed > MAX_FORM_AGE_MS) { - return { valid: false, error: 'Requête invalide' }; - } - - return { valid: true }; -} - -// Get cookie name from environment or use default -export const COOKIE_NAME = getSessionCookieName(); - -/** - * Register a new user - * @param {FormData} formData - Form data with email, password, name - * @returns {Promise} Result object - */ -export async function registerAction(formData) { - try { - const botCheck = validateAntiBotFields(formData); - if (!botCheck.valid) return { success: false, error: botCheck.error }; - - const ip = await getClientIp(); - const rl = enforceRateLimit(ip, 'register'); - if (rl && !rl.allowed) { - return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` }; - } - - const email = formData.get('email'); - const password = formData.get('password'); - const name = formData.get('name'); - - const result = await register({ email, password, name }); - - // Send verification email - await sendVerificationEmail(result.user.email, result.verificationToken, getPublicBaseUrl()); - - return { - success: true, - message: 'Compte créé avec succès. Consultez votre e-mail pour vérifier votre compte.', - user: result.user - }; - } catch (error) { - // Never return raw error.message to the client — library and database errors - // (e.g. unique-constraint violations) expose internal table names and schema. - fail(`Auth: registerAction error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' }; - } -} - -/** - * Login a user - * @param {FormData} formData - Form data with email and password - * @returns {Promise} Result object - */ -export async function loginAction(formData) { - try { - const botCheck = validateAntiBotFields(formData); - if (!botCheck.valid) return { success: false, error: botCheck.error }; - - const ip = await getClientIp(); - const rl = enforceRateLimit(ip, 'login'); - if (rl && !rl.allowed) { - return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` }; - } - - const email = formData.get('email'); - const password = formData.get('password'); - - const result = await login({ email, password }); - - // Set the session cookie directly inside this server action so the token - // never travels through JavaScript-readable response payload. - // An HttpOnly cookie is the only safe transport for session tokens. - const cookieStore = await cookies(); - cookieStore.set(COOKIE_NAME, result.session.token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 30, - path: '/' - }); - - return { - success: true, - message: 'Connexion réussie', - user: result.user - // sessionToken intentionally omitted — it must never appear in a - // JavaScript-accessible response body. - }; - } catch (error) { - fail(`Auth: loginAction error: ${error.message}`); - return { success: false, error: 'Identifiants invalides ou erreur interne. Veuillez réessayer.' }; - } -} - -/** - * Set session cookie after verifying the token is a genuine live session. - * - * This server action is client-callable. Without server-side token validation an - * attacker could supply any arbitrary string (including a stolen token for another - * user) and have it written as the HttpOnly session cookie, completely bypassing - * the protection that HttpOnly is intended to provide. The token is therefore - * validated against the session store before the cookie is written. - * - * @param {string} token - Session token obtained from a trusted server-side flow - * @returns {Promise} Result object - */ -export async function setSessionCookie(token) { - try { - if (!token || typeof token !== 'string' || token.trim() === '') { - return { success: false, error: 'Jeton de session invalide' }; - } - - const session = await validateSession(token); - if (!session) { - return { success: false, error: 'Session invalide ou expirée' }; - } - - const cookieStore = await cookies(); - cookieStore.set(COOKIE_NAME, token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 30, // 30 days - path: '/' - }); - - return { success: true }; - } catch (error) { - fail(`Auth: setSessionCookie error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue' }; - } -} - -/** - * Refresh session cookie (extend expiration). - * - * Re-validates the token before extending its cookie lifetime so that expired - * or revoked tokens cannot have their cookie window reopened by replaying this - * server action. - * - * @param {string} token - Session token - * @returns {Promise} Result object - */ -export async function refreshSessionCookie(token) { - try { - if (!token || typeof token !== 'string' || token.trim() === '') { - return { success: false, error: 'Jeton de session invalide' }; - } - - const session = await validateSession(token); - if (!session) { - return { success: false, error: 'Session invalide ou expirée' }; - } - - const cookieStore = await cookies(); - cookieStore.set(COOKIE_NAME, token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 30, // 30 days - path: '/' - }); - - return { success: true }; - } catch (error) { - fail(`Auth: refreshSessionCookie error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue' }; - } -} - -/** - * Logout a user - * @returns {Promise} Result object - */ -export async function logoutAction() { - try { - const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; - - if (token) { - await deleteSession(token); - } - - cookieStore.delete(COOKIE_NAME); - - return { - success: true, - message: 'Déconnexion réussie' - }; - } catch (error) { - fail(`Auth: logoutAction error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' }; - } -} - -/** - * Get current user session - * @returns {Promise} Session and user data or null - */ -export async function getSession() { - try { - const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; - - if (!token) return null; - - const result = await validateSession(token); - - // If session was refreshed, also refresh the cookie - if (result && result.sessionRefreshed) { - await refreshSessionCookie(token); - } - - return result; - } catch (error) { - fail(`Auth: session validation error: ${error.message}`); - return null; - } -} - -/** - * Request password reset - * @param {FormData} formData - Form data with email - * @returns {Promise} Result object - */ -export async function forgotPasswordAction(formData) { - try { - const botCheck = validateAntiBotFields(formData); - if (!botCheck.valid) return { success: false, error: botCheck.error }; - - const ip = await getClientIp(); - const rl = enforceRateLimit(ip, 'forgot_password'); - if (rl && !rl.allowed) { - return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` }; - } - - const email = formData.get('email'); - - const result = await requestPasswordReset(email); - - if (result.token) { - await sendPasswordResetEmail(email, result.token, getPublicBaseUrl()); - } - - return { - success: true, - message: 'Si un compte existe avec cet e-mail, vous recevrez un lien pour réinitialiser votre mot de passe.' - }; - } catch (error) { - fail(`Auth: forgotPasswordAction error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' }; - } -} - -/** - * Reset password with token - * @param {FormData} formData - Form data with email, token, and newPassword - * @returns {Promise} Result object - */ -export async function resetPasswordAction(formData) { - try { - const ip = await getClientIp(); - const rl = enforceRateLimit(ip, 'reset_password'); - if (rl && !rl.allowed) { - return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` }; - } - - const email = formData.get('email'); - const token = formData.get('token'); - const newPassword = formData.get('newPassword'); - - // Verify token first — throw UserFacingError so the specific message reaches - // the client while unexpected system errors are sanitized in the catch below. - const isValid = await verifyResetToken(email, token); - if (!isValid) { - throw new UserFacingError('Jeton de réinitialisation invalide ou expiré'); - } - - await resetPassword({ email, token, newPassword }); - - return { - success: true, - message: 'Mot de passe réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.' - }; - } catch (error) { - if (error instanceof UserFacingError) { - return { success: false, error: error.message }; - } - fail(`Auth: resetPasswordAction error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' }; - } -} - -/** - * Verify email with token - * @param {FormData} formData - Form data with email and token - * @returns {Promise} Result object - */ -export async function verifyEmailAction(formData) { - try { - const ip = await getClientIp(); - const rl = enforceRateLimit(ip, 'verify_email'); - if (rl && !rl.allowed) { - return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` }; - } - - const email = formData.get('email'); - const token = formData.get('token'); - - // Verify token — throw UserFacingError so validation messages surface to the - // client while unexpected system errors remain sanitized in the catch below. - const isValid = await verifyEmailToken(email, token); - if (!isValid) { - throw new UserFacingError('Jeton de vérification invalide ou expiré'); - } - - // Find user and verify - const { findOne } = await import('../../../core/database/crud.js'); - const user = await findOne('zen_auth_users', { email }); - - if (!user) { - throw new UserFacingError('Utilisateur introuvable'); - } - - await verifyUserEmail(user.id); - - return { - success: true, - message: 'E-mail vérifié avec succès. Vous pouvez maintenant vous connecter.' - }; - } catch (error) { - if (error instanceof UserFacingError) { - return { success: false, error: error.message }; - } - fail(`Auth: verifyEmailAction error: ${error.message}`); - return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' }; - } -} - diff --git a/src/features/auth/lib/auth.js b/src/features/auth/auth.js similarity index 100% rename from src/features/auth/lib/auth.js rename to src/features/auth/auth.js diff --git a/src/features/auth/components/AuthPages.js b/src/features/auth/components/AuthPages.js deleted file mode 100644 index be0ecaa..0000000 --- a/src/features/auth/components/AuthPages.js +++ /dev/null @@ -1,104 +0,0 @@ -'use client'; - -/** - * Auth Pages Component - Catch-all route for Next.js App Router - * This component handles all authentication routes: login, register, forgot, reset, confirm - */ - -import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import LoginPage from './pages/LoginPage.js'; -import RegisterPage from './pages/RegisterPage.js'; -import ForgotPasswordPage from './pages/ForgotPasswordPage.js'; -import ResetPasswordPage from './pages/ResetPasswordPage.js'; -import ConfirmEmailPage from './pages/ConfirmEmailPage.js'; -import LogoutPage from './pages/LogoutPage.js'; - -export default function AuthPagesClient({ - params, - searchParams, - registerAction, - loginAction, - forgotPasswordAction, - resetPasswordAction, - verifyEmailAction, - logoutAction, - setSessionCookieAction, - redirectAfterLogin = '/', - currentUser = null -}) { - const router = useRouter(); - const [currentPage, setCurrentPage] = useState(null); // null = loading - const [isLoading, setIsLoading] = useState(true); - const [email, setEmail] = useState(''); - const [token, setToken] = useState(''); - - useEffect(() => { - // Get page from params or URL - const getPageFromParams = () => { - if (params?.auth?.[0]) { - return params.auth[0]; - } - - // Fallback: read from URL - if (typeof window !== 'undefined') { - const pathname = window.location.pathname; - const match = pathname.match(/\/auth\/([^\/\?]+)/); - return match ? match[1] : 'login'; - } - - return 'login'; - }; - - const page = getPageFromParams(); - setCurrentPage(page); - setIsLoading(false); - }, [params]); - - // Extract email and token from searchParams (handles both Promise and regular object) - useEffect(() => { - const extractSearchParams = async () => { - let resolvedParams = searchParams; - - // Check if searchParams is a Promise (Next.js 15+) - if (searchParams && typeof searchParams.then === 'function') { - resolvedParams = await searchParams; - } - - // Extract email and token from URL if not in searchParams - if (typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.search); - setEmail(resolvedParams?.email || urlParams.get('email') || ''); - setToken(resolvedParams?.token || urlParams.get('token') || ''); - } else { - setEmail(resolvedParams?.email || ''); - setToken(resolvedParams?.token || ''); - } - }; - - extractSearchParams(); - }, [searchParams]); - - const navigate = (page) => { - router.push(`/auth/${page}`); - }; - - // Don't render anything while determining the correct page - if (isLoading || !currentPage) { - return null; - } - - // Page components mapping - const pageComponents = { - login: () => , - register: () => , - forgot: () => , - reset: () => , - confirm: () => , - logout: () => - }; - - // Render the appropriate page - const PageComponent = pageComponents[currentPage]; - return PageComponent ? : ; -} diff --git a/src/features/auth/components/AuthPagesLayout.js b/src/features/auth/components/AuthPagesLayout.js deleted file mode 100644 index ed8f27f..0000000 --- a/src/features/auth/components/AuthPagesLayout.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Auth Pages Layout - Server Component - * Provides the layout structure for authentication pages - * - * Usage: - * - * - * - */ - -export default function AuthPagesLayout({ children }) { - return ( -
-
- {children} -
-
- ); -} diff --git a/src/features/auth/lib/email.js b/src/features/auth/email.js similarity index 100% rename from src/features/auth/lib/email.js rename to src/features/auth/email.js diff --git a/src/features/auth/components/pages/ConfirmEmailPage.js b/src/features/auth/pages/ConfirmEmailPage.client.js similarity index 100% rename from src/features/auth/components/pages/ConfirmEmailPage.js rename to src/features/auth/pages/ConfirmEmailPage.client.js diff --git a/src/features/auth/components/pages/ForgotPasswordPage.js b/src/features/auth/pages/ForgotPasswordPage.client.js similarity index 100% rename from src/features/auth/components/pages/ForgotPasswordPage.js rename to src/features/auth/pages/ForgotPasswordPage.client.js diff --git a/src/features/auth/components/pages/LoginPage.js b/src/features/auth/pages/LoginPage.client.js similarity index 100% rename from src/features/auth/components/pages/LoginPage.js rename to src/features/auth/pages/LoginPage.client.js diff --git a/src/features/auth/components/pages/LogoutPage.js b/src/features/auth/pages/LogoutPage.client.js similarity index 100% rename from src/features/auth/components/pages/LogoutPage.js rename to src/features/auth/pages/LogoutPage.client.js diff --git a/src/features/auth/components/pages/RegisterPage.js b/src/features/auth/pages/RegisterPage.client.js similarity index 100% rename from src/features/auth/components/pages/RegisterPage.js rename to src/features/auth/pages/RegisterPage.client.js diff --git a/src/features/auth/components/pages/ResetPasswordPage.js b/src/features/auth/pages/ResetPasswordPage.client.js similarity index 100% rename from src/features/auth/components/pages/ResetPasswordPage.js rename to src/features/auth/pages/ResetPasswordPage.client.js diff --git a/src/features/auth/lib/password.js b/src/features/auth/password.js similarity index 100% rename from src/features/auth/lib/password.js rename to src/features/auth/password.js diff --git a/src/features/auth/middleware/protect.js b/src/features/auth/protect.js similarity index 100% rename from src/features/auth/middleware/protect.js rename to src/features/auth/protect.js diff --git a/src/features/auth/lib/session.js b/src/features/auth/session.js similarity index 100% rename from src/features/auth/lib/session.js rename to src/features/auth/session.js diff --git a/src/features/dashboard.client.js b/src/features/dashboard.client.js deleted file mode 100644 index f4c47c3..0000000 --- a/src/features/dashboard.client.js +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; - -/** - * Câblage client des widgets du tableau de bord — Features externes - * - * Ce fichier est le SEUL à modifier pour ajouter le widget client - * d'une nouvelle feature au tableau de bord. - * - * L'import suffit : chaque module s'enregistre lui-même en side effect - * via registerClientWidget() de admin/dashboard/clientRegistry.js. - * - * Exemple pour une feature 'blog' : - * import './blog/dashboard.widget.js'; - */ diff --git a/src/features/dashboard.server.js b/src/features/dashboard.server.js deleted file mode 100644 index b6fca16..0000000 --- a/src/features/dashboard.server.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Câblage serveur des widgets du tableau de bord — Features externes - * - * Ce fichier est le SEUL à modifier pour ajouter la contribution serveur - * d'une nouvelle feature au tableau de bord. - * - * L'import suffit : chaque module s'enregistre lui-même en side effect - * via registerServerWidget() de admin/dashboard/registry.js. - * - * Exemple pour une feature 'blog' : - * import './blog/dashboard.server.js'; - */ diff --git a/src/features/features.registry.js b/src/features/features.registry.js deleted file mode 100644 index 4678f43..0000000 --- a/src/features/features.registry.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Core Features Registry - * - * Lists all built-in features that are always initialized when running `zen-db init`. - * Unlike optional modules (src/modules), core features are not gated by env vars — - * they are required for the application to function. - * - * Each name must correspond to a directory under src/features/ that exposes a db.js - * with createTables() and optionally dropTables(). - */ -export const CORE_FEATURES = [ - 'auth', -]; diff --git a/src/shared/Icons.js b/src/shared/icons/index.js similarity index 100% rename from src/shared/Icons.js rename to src/shared/icons/index.js