Files
cvfs/apps/webapp/src/app/api/auth/callback/route.ts
Claude 01f34915f6 feat(dashboard): complete CV branching dashboard with auth and full editing workflow
- Visual branch heritage tree with colored dots and connecting lines, depth-aware expand/collapse
- Dashboard 3-tab layout: Content (inline block editing + patch staging), Patches (diff view), Submissions (AI suggestions)
- Inline block editing: click to edit any CV block, stage edits, save as named branch with pre-filled patches
- Submissions tab: create applications, request AI tailoring suggestions, accept/reject per suggestion
- Simple hardcoded login (username/password via env vars LOGIN_USER/LOGIN_PASS, defaults admin/admin)
- Authentik OIDC integration: authorize redirect + callback exchange, configurable via NEXT_PUBLIC_AUTHENTIK_*
- Middleware protecting /dashboard with session cookie verification (HMAC-SHA256)
- Auth API routes: /api/auth/login, /api/auth/logout, /api/auth/callback, /api/auth/token
- Backend: GET/PATCH submission routes for listing submissions and accepting/rejecting AI suggestions
- API client: OIDC bearer token forwarding from client-readable cookie

https://claude.ai/code/session_01CdisLhbC2kVt2hxfJ7TNPf
2026-04-03 13:45:51 +00:00

44 lines
1.7 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const { searchParams, origin } = new URL(req.url);
const code = searchParams.get('code');
if (!code) return NextResponse.redirect(`${origin}/login?error=no_code`);
const issuer = process.env.AUTHENTIK_ISSUER;
const clientId = process.env.AUTHENTIK_CLIENT_ID;
const clientSecret = process.env.AUTHENTIK_CLIENT_SECRET;
const redirectUri = `${process.env.NEXT_PUBLIC_BASE_URL ?? origin}/api/auth/callback`;
if (!issuer || !clientId || !clientSecret) {
return NextResponse.redirect(`${origin}/login?error=oidc_not_configured`);
}
const tokenRes = await fetch(`${issuer}/application/o/token/`, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code', code,
redirect_uri: redirectUri, client_id: clientId, client_secret: clientSecret,
}),
}).catch(() => null);
if (!tokenRes?.ok) return NextResponse.redirect(`${origin}/login?error=token_exchange`);
const tokens = await tokenRes.json();
const res = NextResponse.redirect(`${origin}/dashboard`);
res.cookies.set('oidc_token', tokens.access_token, {
httpOnly: true, sameSite: 'lax', path: '/',
maxAge: tokens.expires_in ?? 3600,
secure: process.env.NODE_ENV === 'production',
});
// non-httpOnly copy for client-side API bearer usage
res.cookies.set('oidc_token_pub', tokens.access_token, {
httpOnly: false, sameSite: 'lax', path: '/',
maxAge: tokens.expires_in ?? 3600,
secure: process.env.NODE_ENV === 'production',
});
return res;
}