import { objectSchema } from '../../kernel/input-schema.mjs'; import { getUsersService } from './index.mjs'; const emailRule = { type: 'string', required: true, minLength: 3, maxLength: 254, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }; const roles = ['admin','manager','editor','publisher','viewer']; export function registerUsersRoutes({ app, routes, deps }) { routes.register(app, { id: 'users.me.read', method: 'GET', path: '/api/me', owner: 'vendoo.users', policy: 'users.self', csrf: false, stability: 'stable', handler: async (req, res) => res.json(getUsersService().currentIdentity(req.user.id, req.cookies?.csrf_token)), }); routes.register(app, { id: 'users.me.update', method: 'PUT', path: '/api/me', owner: 'vendoo.users', policy: 'users.self', csrf: true, stability: 'stable', input: objectSchema({ name: { type: 'string', required: true, minLength: 1, maxLength: 120 } }), handler: async (req, res) => { const user = getUsersService().updateProfile(req.user.id, req.validatedBody); deps.auditLog(req.user.id, req.user.email, 'user.profile_updated', 'user', String(req.user.id), null, deps.getClientIp(req)); res.json(user); }, }); routes.register(app, { id: 'users.admin.list', method: 'GET', path: '/api/admin/users', owner: 'vendoo.users', policy: 'users.manage', csrf: false, stability: 'stable', handler: async (_req, res) => res.json(getUsersService().list()), }); routes.register(app, { id: 'users.admin.invite', method: 'POST', path: '/api/admin/users/invite', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', input: objectSchema({ email: emailRule, name: { type: 'string', maxLength: 120 }, role: { type: 'string', enum: roles }, method: { type: 'string', enum: ['magic_link','auto_password'] } }), handler: async (req, res) => { const prepared = getUsersService().prepareInvite({ ...req.validatedBody, createdBy: req.user.id }); const baseUrl = `${req.protocol}://${req.get('host')}`; if (prepared.mode === 'auto_password') { const mailResult = await deps.sendCredentials(prepared.user.email, prepared.password, prepared.user.name || '', baseUrl); deps.auditLog(req.user.id, req.user.email, 'user.invited', 'user', prepared.user.email, `Rolle: ${prepared.role}, Auto-Passwort`, deps.getClientIp(req)); return res.json({ ok: true, emailSent: mailResult.sent, consoleFallback: mailResult.consoleFallback || false, password: prepared.password }); } const result = await deps.sendMagicLink(prepared.email, prepared.invite.token, baseUrl, 'invite'); deps.auditLog(req.user.id, req.user.email, 'user.invited', 'user', prepared.email, `Rolle: ${prepared.role}`, deps.getClientIp(req)); res.json({ ok: true, emailSent: result.sent, consoleFallback: result.consoleFallback || false }); }, }); routes.register(app, { id: 'users.admin.invite-resend', method: 'POST', path: '/api/admin/users/resend-invite', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', input: objectSchema({ user_id: { type: 'integer', required: true, min: 1 } }), handler: async (req, res) => { const prepared = getUsersService().resendInvite({ userId: req.validatedBody.user_id, createdBy: req.user.id }); const result = await deps.sendMagicLink(prepared.user.email, prepared.invite.token, `${req.protocol}://${req.get('host')}`, 'invite'); deps.auditLog(req.user.id, req.user.email, 'user.invite_resent', 'user', String(prepared.user.id), null, deps.getClientIp(req)); res.json({ ok: true, emailSent: result.sent, consoleFallback: result.consoleFallback || false }); }, }); routes.register(app, { id: 'users.admin.invitations.list', method: 'GET', path: '/api/admin/users/invitations', owner: 'vendoo.users', policy: 'users.manage', csrf: false, stability: 'stable', handler: async (req, res) => res.json({ invitations: getUsersService().invitations(Number(req.query.limit) || 100) }), }); routes.register(app, { id: 'users.admin.invitations.resend', method: 'POST', path: '/api/admin/users/invitations/:id/resend', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', handler: async (req, res) => { const prepared = getUsersService().resendPendingInvitation({ invitationId: Number(req.params.id), createdBy: req.user.id }); const result = await deps.sendMagicLink(prepared.invitation.email, prepared.invite.token, `${req.protocol}://${req.get('host')}`, 'invite'); deps.auditLog(req.user.id, req.user.email, 'user.invitation_resent', 'invitation', String(req.params.id), JSON.stringify({ email: prepared.invitation.email, role: prepared.role, expires_at: prepared.invite.expiresAt }), deps.getClientIp(req)); res.json({ ok: true, emailSent: result.sent, consoleFallback: result.consoleFallback || false, expiresAt: prepared.invite.expiresAt }); }, }); routes.register(app, { id: 'users.admin.invitations.revoke', method: 'DELETE', path: '/api/admin/users/invitations/:id', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', handler: async (req, res) => { const invitation = getUsersService().revokePendingInvitation(Number(req.params.id)); deps.auditLog(req.user.id, req.user.email, 'user.invitation_revoked', 'invitation', String(req.params.id), JSON.stringify({ email: invitation.email, role: invitation.role }), deps.getClientIp(req)); res.json({ ok: true, invitation }); }, }); routes.register(app, { id: 'users.admin.update', method: 'PUT', path: '/api/admin/users/:id', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', input: objectSchema({ name: { type: 'string', maxLength: 120 }, role: { type: 'string', enum: roles }, active: { type: 'integer', min: 0, max: 1 }, avatar_color: { type: 'string', maxLength: 32 } }), handler: async (req, res) => { const updated = getUsersService().updateManagedUser({ actor: req.user, userId: Number(req.params.id), patch: req.validatedBody }); deps.auditLog(req.user.id, req.user.email, 'user.updated', 'user', String(req.params.id), JSON.stringify(req.validatedBody), deps.getClientIp(req)); res.json(updated); }, }); routes.register(app, { id: 'users.admin.delete', method: 'DELETE', path: '/api/admin/users/:id', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', handler: async (req, res) => { const deleted = getUsersService().deleteManagedUser({ actor: req.user, userId: Number(req.params.id) }); deps.auditLog(req.user.id, req.user.email, 'user.deleted', 'user', String(req.params.id), deleted.email, deps.getClientIp(req)); res.json({ ok: true }); }, }); routes.register(app, { id: 'users.admin.password-reset', method: 'POST', path: '/api/admin/users/:id/reset-password', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable', input: objectSchema({ password: { type: 'string', required: true, maxLength: 512, trim: false } }), handler: async (req, res) => { const user = getUsersService().resetPassword({ userId: Number(req.params.id), password: req.validatedBody.password }); deps.auditLog(req.user.id, req.user.email, 'user.password_reset', 'user', String(req.params.id), `für ${user.email}`, deps.getClientIp(req)); res.json({ ok: true, message: `Passwort für ${user.email} zurückgesetzt` }); }, }); routes.register(app, { id: 'users.admin.login-history', method: 'GET', path: '/api/admin/users/:id/login-history', owner: 'vendoo.users', policy: 'users.manage', csrf: false, stability: 'stable', handler: async (req, res) => res.json(getUsersService().loginHistory(Number(req.params.id), 30)), }); routes.register(app, { id: 'users.admin.login-history-compat', method: 'GET', path: '/api/admin/login-history/:userId', owner: 'vendoo.users', policy: 'users.manage', csrf: false, stability: 'deprecated', handler: async (req, res) => res.json(getUsersService().loginHistory(Number(req.params.userId), 20)), }); }