relv-dev commited on
Commit
7707ce7
·
verified ·
1 Parent(s): 23dd5ee

Add admin dashboard account provisioning

Browse files
src/server/app.ts CHANGED
@@ -6,6 +6,7 @@ import submitRouter from './routes/submit';
6
  import internalRouter from './routes/internal';
7
  import reportsRouter from './routes/reports';
8
  import accountsRouter from './routes/accounts';
 
9
 
10
  /**
11
  * Create and configure the Express application with all routes and middleware.
@@ -47,6 +48,7 @@ export function createApp(): Express {
47
  app.use(submitRouter);
48
  app.use(reportsRouter);
49
  app.use(accountsRouter);
 
50
  app.use(internalRouter);
51
 
52
  app.use((req: Request, res: Response) => {
 
6
  import internalRouter from './routes/internal';
7
  import reportsRouter from './routes/reports';
8
  import accountsRouter from './routes/accounts';
9
+ import adminUsersRouter from './routes/admin-users';
10
 
11
  /**
12
  * Create and configure the Express application with all routes and middleware.
 
48
  app.use(submitRouter);
49
  app.use(reportsRouter);
50
  app.use(accountsRouter);
51
+ app.use(adminUsersRouter);
52
  app.use(internalRouter);
53
 
54
  app.use((req: Request, res: Response) => {
src/server/routes/admin-users.ts ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router, Response } from 'express';
2
+ import { supabase } from '../../db/client';
3
+ import { logger } from '../../utils/logger';
4
+ import { authenticateUser, AuthenticatedRequest } from '../middleware/auth';
5
+
6
+ const router = Router();
7
+
8
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
+ const ALLOWED_ROLES = new Set(['user', 'admin']);
10
+
11
+ async function isAdmin(userId: string): Promise<boolean> {
12
+ const { data, error } = await supabase
13
+ .from('user_profiles')
14
+ .select('role')
15
+ .eq('id', userId)
16
+ .single();
17
+
18
+ if (error) {
19
+ logger.warn('Failed to verify admin for user management', {
20
+ userId,
21
+ error: error.message,
22
+ });
23
+ return false;
24
+ }
25
+
26
+ return data?.role === 'admin';
27
+ }
28
+
29
+ router.post(
30
+ '/api/admin/users',
31
+ authenticateUser,
32
+ async (req, res: Response): Promise<void> => {
33
+ const authReq = req as AuthenticatedRequest;
34
+
35
+ try {
36
+ if (!(await isAdmin(authReq.userId))) {
37
+ res.status(403).json({ error: 'Admin access required' });
38
+ return;
39
+ }
40
+
41
+ const email = String(authReq.body.email || '').trim().toLowerCase();
42
+ const password = String(authReq.body.password || '');
43
+ const displayName = String(authReq.body.displayName || '').trim();
44
+ const role = String(authReq.body.role || 'user').trim().toLowerCase();
45
+ const ticketBalance = Number(authReq.body.ticketBalance ?? 0);
46
+
47
+ if (!EMAIL_PATTERN.test(email)) {
48
+ res.status(400).json({ error: 'A valid email address is required' });
49
+ return;
50
+ }
51
+ if (password.length < 8 || password.length > 128) {
52
+ res.status(400).json({ error: 'Password must contain 8 to 128 characters' });
53
+ return;
54
+ }
55
+ if (displayName.length > 80) {
56
+ res.status(400).json({ error: 'Display name must not exceed 80 characters' });
57
+ return;
58
+ }
59
+ if (!ALLOWED_ROLES.has(role)) {
60
+ res.status(400).json({ error: 'Role must be user or admin' });
61
+ return;
62
+ }
63
+ if (!Number.isInteger(ticketBalance) || ticketBalance < 0 || ticketBalance > 1_000_000) {
64
+ res.status(400).json({ error: 'Ticket balance must be an integer from 0 to 1,000,000' });
65
+ return;
66
+ }
67
+
68
+ const { data: created, error: createError } = await supabase.auth.admin.createUser({
69
+ email,
70
+ password,
71
+ email_confirm: true,
72
+ user_metadata: {
73
+ display_name: displayName || email.split('@')[0],
74
+ },
75
+ });
76
+
77
+ if (createError || !created.user) {
78
+ const message = createError?.message || 'Supabase did not return the created user';
79
+ const status = /already|registered|exists/i.test(message) ? 409 : 400;
80
+ res.status(status).json({ error: message });
81
+ return;
82
+ }
83
+
84
+ const { data: profile, error: profileError } = await supabase
85
+ .from('user_profiles')
86
+ .update({
87
+ display_name: displayName || email.split('@')[0],
88
+ role,
89
+ ticket_balance: ticketBalance,
90
+ updated_at: new Date().toISOString(),
91
+ })
92
+ .eq('id', created.user.id)
93
+ .select('id, email, display_name, role, ticket_balance, created_at')
94
+ .single();
95
+
96
+ if (profileError || !profile) {
97
+ await supabase.auth.admin.deleteUser(created.user.id).catch(() => undefined);
98
+ throw profileError || new Error('User profile was not created');
99
+ }
100
+
101
+ logger.info('Dashboard account created by admin', {
102
+ adminUserId: authReq.userId,
103
+ createdUserId: created.user.id,
104
+ role,
105
+ ticketBalance,
106
+ });
107
+
108
+ res.status(201).json({ user: profile });
109
+ } catch (error: unknown) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ logger.error('Admin user creation failed', {
112
+ adminUserId: authReq.userId,
113
+ error: message,
114
+ });
115
+ res.status(500).json({ error: 'Failed to create dashboard account' });
116
+ }
117
+ },
118
+ );
119
+
120
+ export default router;