Spaces:
Sleeping
Sleeping
| import { Router, Response } from 'express'; | |
| import { supabase } from '../../db/client'; | |
| import { logger } from '../../utils/logger'; | |
| import { authenticateUser, AuthenticatedRequest } from '../middleware/auth'; | |
| const router = Router(); | |
| const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | |
| const ALLOWED_ROLES = new Set(['user', 'admin']); | |
| async function isAdmin(userId: string): Promise<boolean> { | |
| const { data, error } = await supabase | |
| .from('user_profiles') | |
| .select('role') | |
| .eq('id', userId) | |
| .single(); | |
| if (error) { | |
| logger.warn('Failed to verify admin for user management', { | |
| userId, | |
| error: error.message, | |
| }); | |
| return false; | |
| } | |
| return data?.role === 'admin'; | |
| } | |
| router.post( | |
| '/api/admin/users', | |
| authenticateUser, | |
| async (req, res: Response): Promise<void> => { | |
| const authReq = req as AuthenticatedRequest; | |
| try { | |
| if (!(await isAdmin(authReq.userId))) { | |
| res.status(403).json({ error: 'Admin access required' }); | |
| return; | |
| } | |
| const email = String(authReq.body.email || '').trim().toLowerCase(); | |
| const password = String(authReq.body.password || ''); | |
| const displayName = String(authReq.body.displayName || '').trim(); | |
| const role = String(authReq.body.role || 'user').trim().toLowerCase(); | |
| const ticketBalance = Number(authReq.body.ticketBalance ?? 0); | |
| if (!EMAIL_PATTERN.test(email)) { | |
| res.status(400).json({ error: 'A valid email address is required' }); | |
| return; | |
| } | |
| if (password.length < 8 || password.length > 128) { | |
| res.status(400).json({ error: 'Password must contain 8 to 128 characters' }); | |
| return; | |
| } | |
| if (displayName.length > 80) { | |
| res.status(400).json({ error: 'Display name must not exceed 80 characters' }); | |
| return; | |
| } | |
| if (!ALLOWED_ROLES.has(role)) { | |
| res.status(400).json({ error: 'Role must be user or admin' }); | |
| return; | |
| } | |
| if (!Number.isInteger(ticketBalance) || ticketBalance < 0 || ticketBalance > 1_000_000) { | |
| res.status(400).json({ error: 'Ticket balance must be an integer from 0 to 1,000,000' }); | |
| return; | |
| } | |
| const { data: created, error: createError } = await supabase.auth.admin.createUser({ | |
| email, | |
| password, | |
| email_confirm: true, | |
| app_metadata: { | |
| dashboard_provisioned: true, | |
| }, | |
| user_metadata: { | |
| display_name: displayName || email.split('@')[0], | |
| }, | |
| }); | |
| if (createError || !created.user) { | |
| const message = createError?.message || 'Supabase did not return the created user'; | |
| const status = /already|registered|exists/i.test(message) ? 409 : 400; | |
| res.status(status).json({ error: message }); | |
| return; | |
| } | |
| const { data: profile, error: profileError } = await supabase | |
| .from('user_profiles') | |
| .update({ | |
| display_name: displayName || email.split('@')[0], | |
| role, | |
| ticket_balance: ticketBalance, | |
| updated_at: new Date().toISOString(), | |
| }) | |
| .eq('id', created.user.id) | |
| .select('id, email, display_name, role, ticket_balance, created_at') | |
| .single(); | |
| if (profileError || !profile) { | |
| await supabase.auth.admin.deleteUser(created.user.id).catch(() => undefined); | |
| throw profileError || new Error('User profile was not created'); | |
| } | |
| logger.info('Dashboard account created by admin', { | |
| adminUserId: authReq.userId, | |
| createdUserId: created.user.id, | |
| role, | |
| ticketBalance, | |
| }); | |
| res.status(201).json({ user: profile }); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| logger.error('Admin user creation failed', { | |
| adminUserId: authReq.userId, | |
| error: message, | |
| }); | |
| res.status(500).json({ error: 'Failed to create dashboard account' }); | |
| } | |
| }, | |
| ); | |
| export default router; | |