-
-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy path[id].js
More file actions
105 lines (95 loc) · 3.98 KB
/
Copy path[id].js
File metadata and controls
105 lines (95 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// GET /api/admin/user/:id — full user detail
// PATCH /api/admin/user/:id — update plan, is_admin, or soft-delete
import { z } from 'zod';
import { sql } from '../../_lib/db.js';
import { cors, json, method, readJson, wrap, error } from '../../_lib/http.js';
import { requireAdmin } from '../../_lib/admin.js';
import { requireCsrf } from '../../_lib/csrf.js';
import { parse } from '../../_lib/validate.js';
const patchSchema = z
.object({
plan: z.enum(['free', 'pro', 'team', 'enterprise']).optional(),
is_admin: z.boolean().optional(),
deleted: z.boolean().optional(),
// Account tier ("mode") grant. null / 'none' clears it to the default
// 'user' tier; 'holder' is derived on-chain and isn't grantable here.
// See api/_lib/account-tier.js.
account_tier: z.enum(['beta', 'pro', 'three-dimensional', 'none']).nullable().optional(),
})
.refine((d) => Object.keys(d).length > 0, { message: 'nothing to update' });
export default wrap(async (req, res) => {
if (cors(req, res, { methods: 'GET,PATCH,OPTIONS', credentials: true })) return;
if (!method(req, res, ['GET', 'PATCH'])) return;
const admin = await requireAdmin(req, res);
if (!admin) return;
if (req.method === 'PATCH') {
if (!(await requireCsrf(req, res, admin.id))) return;
}
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const id = req.url.split('/').pop().split('?')[0];
if (!id) return error(res, 400, 'bad_request', 'missing user id');
if (!UUID_REGEX.test(id)) return error(res, 400, 'invalid_id', 'user id must be a valid UUID');
if (req.method === 'GET') {
const [user] = await sql`
select
u.id, u.email, u.display_name, u.plan, u.is_admin, u.account_tier,
u.wallet_address, u.email_verified, u.created_at, u.updated_at, u.deleted_at,
(
select json_agg(json_build_object(
'address', w.address, 'chain_type', w.chain_type,
'is_primary', w.is_primary, 'created_at', w.created_at
) order by w.created_at)
from user_wallets w where w.user_id = u.id
) as wallets,
(select count(*)::int from avatars a where a.owner_id = u.id and a.deleted_at is null) as avatar_count,
(select count(*)::int from agent_identities ai where ai.user_id = u.id and ai.deleted_at is null) as agent_count,
(select count(*)::int from sessions s where s.user_id = u.id and s.revoked_at is null and s.expires_at > now()) as active_sessions,
(select json_agg(json_build_object('plan', sub.plan, 'chain_type', sub.chain_type, 'status', sub.status, 'active_until', sub.active_until) order by sub.created_at desc)
from subscriptions sub where sub.user_id = u.id) as subscriptions
from users u
where u.id = ${id}
limit 1
`;
if (!user) return error(res, 404, 'not_found', 'user not found');
return json(res, 200, { user });
}
// PATCH
const body = parse(patchSchema, await readJson(req));
const setFrags = [];
const params = [];
if (body.plan !== undefined) {
params.push(body.plan);
setFrags.push(`plan = $${params.length}`);
}
if (body.is_admin !== undefined) {
params.push(body.is_admin);
setFrags.push(`is_admin = $${params.length}`);
}
if (body.account_tier !== undefined) {
// 'none' / null clears the grant; the resolver treats NULL as the default
// 'user' tier.
const grant = body.account_tier && body.account_tier !== 'none' ? body.account_tier : null;
params.push(grant);
setFrags.push(`account_tier = $${params.length}`);
}
if (body.deleted !== undefined) {
if (body.deleted) {
setFrags.push(`deleted_at = now()`);
} else {
setFrags.push(`deleted_at = NULL`);
}
}
if (setFrags.length === 0) return error(res, 400, 'validation_error', 'nothing to update');
params.push(id);
const idIdx = params.length;
const [updated] = await sql(
`
update users set ${setFrags.join(', ')}
where id = $${idIdx}
returning id, email, display_name, plan, is_admin, account_tier, deleted_at
`,
params,
);
if (!updated) return error(res, 404, 'not_found', 'user not found');
return json(res, 200, { user: updated });
});