This Edge Function grants platform-admin power (list every user across every workspace, ban/unban accounts, trigger password resets for any user_id) but answered with Access-Control-Allow-Origin: '*', unlike the sibling caterium-create-employee function which already uses an origin allowlist. Authorization itself was never bypassable this way (the function still requires the caller's own Bearer token and re-checks sun_is_platform_admin() server-side), but a wildcard CORS response removes a real layer of defense-in-depth if a platform-admin token were ever exposed to another origin. Applies the same allowedOrigin()/corsHeaders() pattern already proven in caterium-create-employee, and extends edge-security-v1774.mjs (which already asserted the wildcard was gone from create-employee, but never checked this function) to cover both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
5.2 KiB
TypeScript
102 lines
5.2 KiB
TypeScript
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 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)
|
||
}
|
||
})
|