nuxt-supabase-team-auth
Version:
Drop-in Nuxt 3 module for team-based authentication with Supabase
191 lines (172 loc) • 4.72 kB
text/typescript
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
interface AcceptInviteRequest {
team_id: string
}
serve(async (req) => {
// Handle OPTIONS requests
if (req.method === 'OPTIONS') {
return new Response('ok')
}
try {
// Get user from request (they should be authenticated after Supabase invite flow)
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing authorization header' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Initialize Supabase clients
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
)
const supabaseUser = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
global: {
headers: {
Authorization: authHeader,
},
},
},
)
// Get current user
const { data: { user }, error: userError } = await supabaseUser.auth.getUser()
if (userError || !user) {
return new Response(
JSON.stringify({ error: 'Invalid or expired token' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Parse request body
const { team_id }: AcceptInviteRequest = await req.json()
if (!team_id) {
return new Response(
JSON.stringify({ error: 'Missing team_id' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Check if user has invitation metadata for this team
if (!user.user_metadata?.team_id || user.user_metadata.team_id !== team_id) {
return new Response(
JSON.stringify({
error: 'INVITE_NOT_FOUND',
message: 'No pending invitation found for this user and team',
}),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Check if user is already a member of this team
const { data: existingMember } = await supabaseAdmin
.from('team_members')
.select('id')
.eq('team_id', team_id)
.eq('user_id', user.id)
.single()
if (existingMember) {
return new Response(
JSON.stringify({
error: 'ALREADY_MEMBER',
message: 'User is already a member of this team',
}),
{
status: 409,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Get team info
const { data: team, error: teamError } = await supabaseAdmin
.from('teams')
.select('id, name')
.eq('id', team_id)
.single()
if (teamError || !team) {
return new Response(
JSON.stringify({ error: 'Team not found' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Get the role from invitation metadata
const invitedRole = user.user_metadata?.role || 'member'
// Add user to team
const { error: memberError } = await supabaseAdmin
.from('team_members')
.insert({
team_id: team_id,
user_id: user.id,
role: invitedRole,
})
if (memberError) {
return new Response(
JSON.stringify({
error: 'Failed to add user to team',
details: memberError.message,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}
// Return success response
return new Response(
JSON.stringify({
success: true,
team: {
id: team.id,
name: team.name,
},
user: {
id: user.id,
email: user.email,
role: invitedRole,
},
}),
{
headers: { 'Content-Type': 'application/json' },
},
)
}
catch (error) {
console.error('Unexpected error in accept-invite:', error)
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}
})