mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-07-15 19:03:38 +00:00
- 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
44 lines
1.7 KiB
TypeScript
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;
|
|
}
|