feat: full feature buildout — streaming, i18n, mastery map, admin, jobs
Progressive lesson streaming via onSegment callback (fixes SSE for non-English users — locale was shadowed in lesson-reader useEffect). Adds: BullMQ workers, Redis stream buffer, token budget enforcement, Langfuse tracing, golden-eval runner, Playwright e2e scaffolding, lesson depth/locale/preferences schema, mastery map UI, admin panel (blueprints/users/reports/quality/misconceptions), image queries, source citations, view transitions, reading animations, i18n (next-intl), PDF export, surprise endpoint, and 402 passing unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { createHmac } from 'crypto';
|
||||
import { type NextRequest } from 'next/server';
|
||||
import { setUserPreferences } from '@/lib/db/queries';
|
||||
|
||||
function verifySignature(userId: string, sig: string): boolean {
|
||||
const secret = process.env.UNSUBSCRIBE_SECRET ?? 'dev-secret-change-in-prod';
|
||||
const expected = createHmac('sha256', secret).update(userId).digest('hex');
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if (expected.length !== sig.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
diff |= expected.charCodeAt(i) ^ sig.charCodeAt(i);
|
||||
}
|
||||
return diff === 0;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const userId = searchParams.get('userId') ?? '';
|
||||
const sig = searchParams.get('sig') ?? '';
|
||||
|
||||
if (!userId || !verifySignature(userId, sig)) {
|
||||
return new Response('Invalid or expired unsubscribe link.', { status: 400 });
|
||||
}
|
||||
|
||||
await setUserPreferences(userId, { emailDigest: false });
|
||||
|
||||
const BASE_URL = process.env.AUTH_URL ?? 'http://localhost:3000';
|
||||
|
||||
return new Response(
|
||||
`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Unsubscribed</title>
|
||||
<style>body{font-family:Georgia,serif;background:#f2f1ee;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
||||
.w{max-width:400px;text-align:center;padding:40px}h1{font-size:1.25rem;color:#1a1a24}p{color:#4a4858}a{color:#2e3a8c}</style>
|
||||
</head><body><div class="w"><h1>Unsubscribed</h1>
|
||||
<p>You've been removed from Curio review digests. You can re-enable them in <a href="${BASE_URL}/settings">Settings</a>.</p>
|
||||
</div></body></html>`,
|
||||
{ headers: { 'Content-Type': 'text/html; charset=utf-8' } },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user