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:
Claude
2026-04-03 13:45:51 +00:00
parent 9a8add0bcd
commit 01f34915f6
14 changed files with 1023 additions and 217 deletions

View File

@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'node:crypto';
const SECRET = process.env.SESSION_SECRET ?? 'dev-secret-change-in-production';
function verifySession(token: string): boolean {
const lastDot = token.lastIndexOf('.');
if (lastDot === -1) return false;
const payload = token.slice(0, lastDot);
const sig = token.slice(lastDot + 1);
const expected = crypto.createHmac('sha256', SECRET).update(payload).digest('hex');
return sig === expected;
}
export function middleware(req: NextRequest) {
if (!req.nextUrl.pathname.startsWith('/dashboard')) return NextResponse.next();
const session = req.cookies.get('session')?.value;
const oidc = req.cookies.get('oidc_token')?.value;
if ((session && verifySession(session)) || oidc) return NextResponse.next();
return NextResponse.redirect(new URL('/login', req.url));
}
export const config = { matcher: ['/dashboard/:path*'] };