caterium-app/supabase/functions/caterium-platform-auth-admin/index.ts

104 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'
const PROD_ORIGINS = new Set([
'https://app.caterium.ru',
'https://caterium.ru',
'https://www.caterium.ru',
])
const WORKERS_DEV_ORIGIN = /^https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*\.workers\.dev$/i
const LOCAL_ORIGIN = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d{1,5})?$/i
function allowedOrigin(origin: string) {
if (!origin) return true
return PROD_ORIGINS.has(origin) || WORKERS_DEV_ORIGIN.test(origin) || LOCAL_ORIGIN.test(origin)
}
function corsHeaders(req: Request) {
const origin = req.headers.get('Origin') || ''
const headers: Record<string, string> = {
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Content-Type': 'application/json',
'Vary': 'Origin',
}
if (origin && allowedOrigin(origin)) headers['Access-Control-Allow-Origin'] = origin
return headers
}
const json = (req: Request, body: unknown, status = 200) => new Response(JSON.stringify(body), {status, headers: corsHeaders(req)})
Deno.serve(async (req) => {
const origin = req.headers.get('Origin') || ''
if (origin && !allowedOrigin(origin)) return json(req, {error: 'Origin is not allowed', code: 'origin_not_allowed'}, 403)
if (req.method === 'OPTIONS') return new Response('ok', {headers: corsHeaders(req)})
if (req.method !== 'POST') return json(req, {error:'Method not allowed'},405)
try {
const url = Deno.env.get('SUPABASE_URL')!
const anon = Deno.env.get('SUPABASE_ANON_KEY')!
const service = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const authHeader = req.headers.get('Authorization') || ''
const userClient = createClient(url, anon, {global:{headers:{Authorization:authHeader}},auth:{persistSession:false,autoRefreshToken:false}})
const {data:{user},error:userError} = await userClient.auth.getUser()
if (userError || !user) return json(req, {error:'Unauthorized'},401)
const {data:isAdmin,error:adminError} = await userClient.rpc('sun_is_platform_admin')
if (adminError || isAdmin !== true) return json(req, {error:'Platform administrator required'},403)
const {error:mfaError} = await userClient.rpc('sun_dev_dashboard')
if (mfaError) return json(req, {error:'Developer MFA AAL2 required'},403)
const body = await req.json().catch(()=>({})) as Record<string,unknown>
const action = String(body.action || 'list')
const admin = createClient(url, service, {auth:{persistSession:false,autoRefreshToken:false}})
if (action === 'list') {
const users:any[] = []
for (let page=1; page<=20; page++) {
const {data,error} = await admin.auth.admin.listUsers({page,perPage:1000})
if (error) throw error
users.push(...(data.users||[]))
if ((data.users||[]).length < 1000) break
}
const {data:directory,error:dirError} = await userClient.rpc('sun_platform_list_users')
if (dirError) throw dirError
const byId = new Map<string,any>()
for (const row of (directory||[])) {
const id=String(row.user_id)
if(!byId.has(id))byId.set(id,{is_platform_admin:row.is_platform_admin===true,workspaces:[]})
if(row.workspace_id)byId.get(id).workspaces.push({id:row.workspace_id,name:row.workspace_name,role:row.role,display_name:row.display_name,is_active:row.is_active!==false})
}
return json(req, {users:users.map(u=>({
id:u.id,email:u.email||'',created_at:u.created_at||'',last_sign_in_at:u.last_sign_in_at||'',
email_confirmed_at:u.email_confirmed_at||u.confirmed_at||'',banned_until:u.banned_until||'',
is_banned:Boolean(u.banned_until&&new Date(u.banned_until)>new Date()),
is_platform_admin:byId.get(String(u.id))?.is_platform_admin===true,
workspaces:byId.get(String(u.id))?.workspaces||[]
}))})
}
const targetId = String(body.user_id || '')
if (!targetId) return json(req, {error:'user_id required'},400)
if (targetId === user.id && action === 'set_ban') return json(req, {error:'Нельзя заблокировать собственный аккаунт разработчика.'},400)
const {data:{user:target},error:targetError}=await admin.auth.admin.getUserById(targetId)
if(targetError||!target)return json(req, {error:'User not found'},404)
if (action === 'set_ban') {
const banned = body.banned === true
const {error}=await admin.auth.admin.updateUserById(targetId,{ban_duration:banned?'876000h':'none'})
if(error)throw error
await userClient.rpc('sun_platform_log_event',{p_action:banned?'account.ban':'account.unban',p_workspace:null,p_user:targetId,p_details:{email:target.email||''}})
return json(req, {ok:true,banned})
}
if (action === 'send_recovery') {
if(!target.email)return json(req, {error:'У пользователя нет email.'},400)
const publicClient=createClient(url,anon,{auth:{persistSession:false,autoRefreshToken:false}})
const {error}=await publicClient.auth.resetPasswordForEmail(target.email)
if(error)throw error
await userClient.rpc('sun_platform_log_event',{p_action:'account.recovery_sent',p_workspace:null,p_user:targetId,p_details:{email:target.email}})
return json(req, {ok:true})
}
return json(req, {error:'Unknown action'},400)
} catch (e) {
console.error(e)
return json(req, {error:e instanceof Error?e.message:String(e)},500)
}
})