// server/supabase/functions/team-accept/index.ts // 팀 초대 수락 — token 검증 후 team_members에 추가. import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' interface AcceptRequest { token: string } // @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 AcceptRequest if (!body.token) { return new Response( JSON.stringify({ error: 'token is required' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const serviceClient = createServiceRoleClient() // 토큰으로 초대 조회 const { data: invite, error: inviteErr } = await serviceClient .from('team_invites') .select('id, team_id, email, role, expires_at, accepted_at') .eq('token', body.token) .maybeSingle() if (inviteErr || !invite) { return new Response( JSON.stringify({ error: 'invalid_token', message: '초대를 찾을 수 없습니다.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } if (invite.accepted_at) { return new Response( JSON.stringify({ error: 'already_accepted', message: '이미 수락된 초대입니다.' }), { status: 409, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const expiresAt = new Date(invite.expires_at as string).getTime() if (Date.now() > expiresAt) { return new Response( JSON.stringify({ error: 'expired', message: '만료된 초대입니다.' }), { status: 410, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // 이메일 일치 확인 (대소문자 무시) const invitedEmail = (invite.email as string).toLowerCase() const userEmail = (user.email ?? '').toLowerCase() if (invitedEmail !== userEmail) { return new Response( JSON.stringify({ error: 'email_mismatch', message: `이 초대는 ${invitedEmail}에게 발송되었습니다. 올바른 계정으로 로그인하세요.` }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // team_members에 추가 (이미 있으면 업데이트) const { error: memberErr } = await serviceClient.from('team_members').upsert( { team_id: invite.team_id as string, user_id: user.id, role: (invite.role as 'admin' | 'member') ?? 'member' }, { onConflict: 'team_id,user_id' } ) if (memberErr) { throw new Error(`멤버 등록 실패: ${memberErr.message}`) } // 초대 accepted 표시 await serviceClient .from('team_invites') .update({ accepted_at: new Date().toISOString(), accepted_by: user.id }) .eq('id', invite.id as string) return new Response( JSON.stringify({ team_id: invite.team_id, role: invite.role }), { 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' } }) } })