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 });
}
}

View File

@@ -1,27 +1,40 @@
import { NextResponse } from 'next/server';
import { getLogger } from '@kit/shared/logger';
export async function POST(request: Request) {
const logger = await getLogger();
try {
const body = await request.json();
const { recipientEmail, name, email, subject, message } = body;
if (!email || !message) {
return NextResponse.json({ error: 'E-Mail und Nachricht sind erforderlich' }, { status: 400 });
return NextResponse.json(
{ error: 'E-Mail und Nachricht sind erforderlich' },
{ status: 400 },
);
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ error: 'Ungültige E-Mail-Adresse' }, { status: 400 });
return NextResponse.json(
{ error: 'Ungültige E-Mail-Adresse' },
{ status: 400 },
);
}
// In production: use @kit/mailers to send the email
// For now: log and return success
console.log('[contact] Form submission:', {
to: recipientEmail || 'admin',
from: email,
name,
subject: subject || 'Kontaktanfrage',
message,
});
logger.info(
{
to: recipientEmail || 'admin',
from: email,
name,
subject: subject || 'Kontaktanfrage',
message,
},
'[contact] Form submission',
);
// TODO: Wire to @kit/mailers
// const mailer = await getMailer();
@@ -34,7 +47,7 @@ export async function POST(request: Request) {
return NextResponse.json({ success: true, message: 'Nachricht gesendet' });
} catch (err) {
console.error('[contact] Error:', err);
logger.error({ error: err, context: 'contact' }, '[contact] Error');
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 });
}
}

View File

@@ -1,8 +1,11 @@
import { NextResponse } from 'next/server';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
export async function POST(request: Request) {
const logger = await getLogger();
try {
const body = await request.json();
const { courseId, firstName, lastName, email, phone } = body;
@@ -34,7 +37,10 @@ export async function POST(request: Request) {
});
if (error) {
console.error('[course-register] Insert error:', error.message);
logger.error(
{ error, context: 'course-register-insert' },
'[course-register] Insert error',
);
return NextResponse.json(
{ error: 'Anmeldung fehlgeschlagen' },
{ status: 500 },
@@ -46,7 +52,10 @@ export async function POST(request: Request) {
message: 'Anmeldung erfolgreich',
});
} catch (err) {
console.error('[course-register] Error:', err);
logger.error(
{ error: err, context: 'course-register' },
'[course-register] Error',
);
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 });
}
}

View File

@@ -1,8 +1,11 @@
import { NextResponse } from 'next/server';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
export async function POST(request: Request) {
const logger = await getLogger();
try {
const body = await request.json();
const {
@@ -46,7 +49,10 @@ export async function POST(request: Request) {
});
if (error) {
console.error('[event-register] Insert error:', error.message);
logger.error(
{ error, context: 'event-register-insert' },
'[event-register] Insert error',
);
return NextResponse.json(
{ error: 'Anmeldung fehlgeschlagen' },
{ status: 500 },
@@ -58,7 +64,10 @@ export async function POST(request: Request) {
message: 'Anmeldung erfolgreich',
});
} catch (err) {
console.error('[event-register] Error:', err);
logger.error(
{ error: err, context: 'event-register' },
'[event-register] Error',
);
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 });
}
}

View File

@@ -1,8 +1,11 @@
import { NextResponse } from 'next/server';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
export async function POST(request: Request) {
const logger = await getLogger();
try {
const body = await request.json();
const {
@@ -21,8 +24,7 @@ export async function POST(request: Request) {
if (!accountId || !firstName || !lastName || !email) {
return NextResponse.json(
{
error:
'Konto-ID, Vorname, Nachname und E-Mail sind erforderlich',
error: 'Konto-ID, Vorname, Nachname und E-Mail sind erforderlich',
},
{ status: 400 },
);
@@ -52,7 +54,10 @@ export async function POST(request: Request) {
});
if (error) {
console.error('[membership-apply] Insert error:', error.message);
logger.error(
{ error, context: 'membership-apply-insert' },
'[membership-apply] Insert error',
);
return NextResponse.json(
{ error: 'Bewerbung fehlgeschlagen' },
{ status: 500 },
@@ -64,7 +69,10 @@ export async function POST(request: Request) {
message: 'Bewerbung erfolgreich eingereicht',
});
} catch (err) {
console.error('[membership-apply] Error:', err);
logger.error(
{ error: err, context: 'membership-apply' },
'[membership-apply] Error',
);
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 });
}
}

View File

@@ -1,42 +1,66 @@
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 body = await request.json();
const { accountId, email, name } = body;
if (!accountId || !email) {
return NextResponse.json({ error: 'accountId und email sind erforderlich' }, { status: 400 });
return NextResponse.json(
{ error: 'accountId und email sind erforderlich' },
{ status: 400 },
);
}
// Validate email format
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ error: 'Ungültige E-Mail-Adresse' }, { status: 400 });
return NextResponse.json(
{ error: 'Ungültige E-Mail-Adresse' },
{ status: 400 },
);
}
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!,
process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!,
);
const token = crypto.randomUUID();
const { error } = await supabase.from('newsletter_subscriptions').upsert({
account_id: accountId,
email,
name: name || null,
confirmation_token: token,
is_active: true,
}, { onConflict: 'account_id,email' });
const { error } = await supabase.from('newsletter_subscriptions').upsert(
{
account_id: accountId,
email,
name: name || null,
confirmation_token: token,
is_active: true,
},
{ onConflict: 'account_id,email' },
);
if (error) {
console.error('[newsletter] Subscription error:', error.message);
return NextResponse.json({ error: 'Anmeldung fehlgeschlagen' }, { status: 500 });
logger.error(
{ error, context: 'newsletter-subscription' },
'[newsletter] Subscription error',
);
return NextResponse.json(
{ error: 'Anmeldung fehlgeschlagen' },
{ status: 500 },
);
}
return NextResponse.json({ success: true, message: 'Erfolgreich angemeldet' });
return NextResponse.json({
success: true,
message: 'Erfolgreich angemeldet',
});
} catch (err) {
console.error('[newsletter] Error:', err);
logger.error({ error: err, context: 'newsletter' }, '[newsletter] Error');
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 });
}
}