// server/supabase/functions/team-invite/index.ts // 팀 초대 생성 — 이메일 기반 초대 토큰 발급. // 초대 링크는 클라이언트가 이메일로 공유 (추후 Edge Function에서 SMTP/Resend 연동). import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' interface InviteRequest { team_id: string email: string role?: 'admin' | 'member' } // @ts-expect-error — Deno 런타임 전역 Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } try { const user = await requireUser(req) const body = (await req.json()) as InviteRequest if (!body.team_id || !body.email) { return new Response( JSON.stringify({ error: 'team_id and email are required' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const serviceClient = createServiceRoleClient() // 권한 확인: 요청자가 해당 팀의 owner/admin 인가? const { data: membership } = await serviceClient .from('team_members') .select('role') .eq('team_id', body.team_id) .eq('user_id', user.id) .maybeSingle() const role = (membership?.role as string | undefined) ?? null if (role !== 'owner' && role !== 'admin') { return new Response( JSON.stringify({ error: 'forbidden', message: '팀 owner/admin만 초대할 수 있습니다.' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // 토큰 생성 (service_role RPC) const { data: tokenData, error: tokenErr } = await serviceClient.rpc('generate_invite_token') if (tokenErr || !tokenData) { throw new Error(`토큰 생성 실패: ${tokenErr?.message ?? 'unknown'}`) } const token = tokenData as string // 초대 INSERT const { data: invite, error: insertErr } = await serviceClient .from('team_invites') .insert({ team_id: body.team_id, invited_by: user.id, email: body.email.toLowerCase().trim(), role: body.role ?? 'member', token }) .select('id, token, expires_at') .single() if (insertErr || !invite) { throw new Error(`초대 생성 실패: ${insertErr?.message ?? 'unknown'}`) } // 초대 링크 구성 (클라이언트에서 이메일로 공유) // @ts-expect-error — Deno.env const siteUrl = Deno.env.get('SITE_URL') ?? 'https://d3ro.dev' const inviteUrl = `${siteUrl}/accept-invite?token=${encodeURIComponent(token)}` // TODO(V2-7c): Resend/SendGrid로 이메일 자동 발송 // 현재는 URL만 반환 → 클라이언트가 복사/공유 return new Response( JSON.stringify({ id: invite.id, url: inviteUrl, expires_at: invite.expires_at }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } catch (err) { if (err && typeof err === 'object' && 'status' in err && 'message' in err) { return authErrorResponse(err as AuthError, corsHeaders) } const message = err instanceof Error ? err.message : 'Unknown error' return new Response(JSON.stringify({ error: message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } })