Add account hierarchy framework with migrations, RLS policies, and UI components

This commit is contained in:
T. Zehetbauer
2026-03-31 22:18:04 +02:00
parent 7e7da0b465
commit 59546ad6d2
262 changed files with 11671 additions and 3927 deletions

View File

@@ -1,7 +1,12 @@
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { getLogger } from '@kit/shared/logger';
export async function POST(request: Request) {
const logger = await getLogger();
try {
const formData = await request.formData();
const token = formData.get('token') as string;
@@ -26,50 +31,88 @@ export async function POST(request: Request) {
.single();
if (invError || !invitation || invitation.status !== 'pending') {
return NextResponse.redirect(new URL(`/club/${slug}/portal/invite?token=${token}&error=invalid`, request.url));
return NextResponse.redirect(
new URL(
`/club/${slug}/portal/invite?token=${token}&error=invalid`,
request.url,
),
);
}
if (new Date(invitation.expires_at) < new Date()) {
return NextResponse.redirect(new URL(`/club/${slug}/portal/invite?token=${token}&error=expired`, request.url));
return NextResponse.redirect(
new URL(
`/club/${slug}/portal/invite?token=${token}&error=expired`,
request.url,
),
);
}
// 2. Create auth user
const { data: authData, error: authError } = await supabase.auth.admin.createUser({
email: invitation.email,
password,
email_confirm: true,
user_metadata: { invited_via: 'member_portal' },
});
const { data: authData, error: authError } =
await supabase.auth.admin.createUser({
email: invitation.email,
password,
email_confirm: true,
user_metadata: { invited_via: 'member_portal' },
});
if (authError) {
// User might already exist — try to find them
const { data: existingUsers } = await supabase.auth.admin.listUsers();
const existing = existingUsers?.users?.find(u => u.email === invitation.email);
const existing = existingUsers?.users?.find(
(u) => u.email === invitation.email,
);
if (existing) {
// Link existing user to member
await supabase.from('members').update({ user_id: existing.id }).eq('id', invitation.member_id);
await supabase.from('member_portal_invitations').update({ status: 'accepted', accepted_at: new Date().toISOString() }).eq('id', invitation.id);
return NextResponse.redirect(new URL(`/club/${slug}/portal`, request.url));
await supabase
.from('members')
.update({ user_id: existing.id })
.eq('id', invitation.member_id);
await supabase
.from('member_portal_invitations')
.update({ status: 'accepted', accepted_at: new Date().toISOString() })
.eq('id', invitation.id);
return NextResponse.redirect(
new URL(`/club/${slug}/portal`, request.url),
);
}
console.error('[accept-invite] Auth error:', authError.message);
return NextResponse.redirect(new URL(`/club/${slug}/portal/invite?token=${token}&error=auth`, request.url));
logger.error(
{ error: authError, context: 'accept-invite-auth' },
'[accept-invite] Auth error',
);
return NextResponse.redirect(
new URL(
`/club/${slug}/portal/invite?token=${token}&error=auth`,
request.url,
),
);
}
// 3. Link member to user
await supabase.from('members').update({ user_id: authData.user.id }).eq('id', invitation.member_id);
await supabase
.from('members')
.update({ user_id: authData.user.id })
.eq('id', invitation.member_id);
// 4. Mark invitation as accepted
await supabase.from('member_portal_invitations').update({
status: 'accepted',
accepted_at: new Date().toISOString(),
}).eq('id', invitation.id);
await supabase
.from('member_portal_invitations')
.update({
status: 'accepted',
accepted_at: new Date().toISOString(),
})
.eq('id', invitation.id);
// 5. Redirect to portal login
return NextResponse.redirect(new URL(`/club/${slug}/portal`, request.url));
} catch (err) {
console.error('[accept-invite] Error:', err);
logger.error(
{ error: err, context: 'accept-invite' },
'[accept-invite] Error',
);
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 });
}
}