116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
|
|
import { requireAdmin, requireManager } from '../_shared/admin-auth.ts'
|
|
import { createServiceRoleClient } from '../_shared/quota.ts'
|
|
import { verifyCurrentAdminActor } from '../_shared/admin-current-role.ts'
|
|
import {
|
|
AdminPublicError,
|
|
actorEmail,
|
|
adminErrorResponse,
|
|
adminJsonResponse,
|
|
adminRpcError,
|
|
managedRoleFilter,
|
|
parsePagination,
|
|
parseRoleChangeRequest,
|
|
parseSearch,
|
|
readAdminJson,
|
|
requireIdempotencyKey,
|
|
requireUuid,
|
|
validateQueryKeys,
|
|
validateRoleRpcResult,
|
|
} from '../_shared/admin-contract.ts'
|
|
|
|
async function readUsers(req: Request): Promise<Response> {
|
|
const admin = await requireManager(req)
|
|
const url = new URL(req.url)
|
|
validateQueryKeys(url, ['userId', 'page', 'limit', 'search', 'role'])
|
|
const serviceClient = createServiceRoleClient()
|
|
await verifyCurrentAdminActor(serviceClient, admin)
|
|
const userIdValue = url.searchParams.get('userId')
|
|
|
|
if (userIdValue) {
|
|
const userId = requireUuid(userIdValue, 'invalid_userId')
|
|
const [{ data: profile, error: profileError }, { data: subscription, error: subscriptionError }, accountResult] =
|
|
await Promise.all([
|
|
serviceClient
|
|
.from('profiles')
|
|
.select('id, name, avatar_url, locale, tier, role, created_at, updated_at')
|
|
.eq('id', userId)
|
|
.maybeSingle(),
|
|
serviceClient
|
|
.from('subscriptions')
|
|
.select('id, user_id, tier, status, provider, payment_provider, current_period_start, current_period_end, overage_credits, admin_note, cancel_at, created_at, updated_at')
|
|
.eq('user_id', userId)
|
|
.maybeSingle(),
|
|
serviceClient.auth.admin.getUserById(userId),
|
|
])
|
|
|
|
if (profileError || subscriptionError || accountResult.error) {
|
|
throw new AdminPublicError(500, 'admin_user_read_failed')
|
|
}
|
|
if (!profile || !accountResult.data.user) throw new AdminPublicError(404, 'user_not_found')
|
|
|
|
const account = accountResult.data.user
|
|
return adminJsonResponse({
|
|
profile,
|
|
subscription: subscription ?? null,
|
|
account: {
|
|
id: account.id,
|
|
email: account.email ?? null,
|
|
createdAt: account.created_at,
|
|
lastSignInAt: account.last_sign_in_at ?? null,
|
|
},
|
|
}, corsHeaders)
|
|
}
|
|
|
|
const { page, limit, from, to } = parsePagination(url)
|
|
const search = parseSearch(url.searchParams.get('search'))
|
|
const role = managedRoleFilter(url.searchParams.get('role'))
|
|
let query = serviceClient
|
|
.from('profiles')
|
|
.select('id, name, avatar_url, tier, role, created_at', { count: 'exact' })
|
|
|
|
if (search) {
|
|
const literalPattern = search.replace(/[\\%_]/g, '\\$&')
|
|
query = query.ilike('name', `%${literalPattern}%`)
|
|
}
|
|
if (role) query = query.eq('role', role)
|
|
|
|
const { data, count, error } = await query
|
|
.order('created_at', { ascending: false })
|
|
.range(from, to)
|
|
if (error) throw new AdminPublicError(500, 'admin_user_read_failed')
|
|
|
|
return adminJsonResponse({ profiles: data ?? [], total: count ?? 0, page, limit }, corsHeaders)
|
|
}
|
|
|
|
async function changeRole(req: Request): Promise<Response> {
|
|
const admin = await requireAdmin(req)
|
|
const body = await readAdminJson(req)
|
|
const mutation = parseRoleChangeRequest(body, requireIdempotencyKey(req))
|
|
const serviceClient = createServiceRoleClient()
|
|
const { data, error } = await serviceClient.rpc('admin_change_user_role_v1', {
|
|
p_actor_email: actorEmail(admin),
|
|
p_idempotency_key: mutation.idempotencyKey,
|
|
p_target_user_id: mutation.userId,
|
|
p_new_role: mutation.newRole,
|
|
p_memo: mutation.memo,
|
|
})
|
|
if (error) throw adminRpcError(error)
|
|
return adminJsonResponse(validateRoleRpcResult(data, mutation), corsHeaders)
|
|
}
|
|
|
|
export async function handleAdminUsers(req: Request): Promise<Response> {
|
|
const preflight = handleCorsPreflightRequest(req)
|
|
if (preflight) return preflight
|
|
|
|
try {
|
|
if (req.method === 'GET') return await readUsers(req)
|
|
if (req.method === 'PATCH') return await changeRole(req)
|
|
return adminJsonResponse({ error: 'method_not_allowed' }, corsHeaders, 405)
|
|
} catch (error) {
|
|
console.error('admin-users request failed', error)
|
|
return adminErrorResponse(error, corsHeaders)
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) Deno.serve(handleAdminUsers)
|