mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-07-15 19:03:38 +00:00
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
This commit is contained in:
43
apps/webapp/src/app/api/auth/callback/route.ts
Normal file
43
apps/webapp/src/app/api/auth/callback/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user