Files
mortgagefi-helper/app/api/tasks/route.ts
Siavash Sameni 6ae581ab2e feat(ui): Ghibli/Miyazaki reskin + Obsidian docs vault + project audit
UI: warm daylight design system (Tailwind v4 @theme palette, gh-* component
classes, watercolor grain, Zen Maru Gothic + Klee One fonts), animated SSR-safe
GhibliBackground (drifting clouds, meadow hills, soot sprites), and a full reskin
of navbar, connect button, dapp page, loan cards, settings modal, and readme.
Fixes the bg-white-on-dark loan-card inconsistency. Web3/business logic untouched.

Docs: converted docs/ into an Obsidian vault (frontmatter, [[wikilinks]],
callouts, Home MOC, folders Architecture/Operations/Audits) and added a
full-project audit note (Project Audit 2026-06). Redacted a real leaked Schedy
key value from the security audit example (rotate it at Schedy).

Also commits the previously-untracked server layer: app/api (cron + tasks routes)
and lib (redis, ssrf-guard, task-store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 08:13:53 +04:00

73 lines
2.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { saveTask, listAllTasks, checkRateLimit } from '@/lib/task-store';
import { isAllowedUrl, sanitizeHeaders } from '@/lib/ssrf-guard';
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
?? request.headers.get('x-real-ip')
?? 'unknown';
const allowed = await checkRateLimit(ip, 30, 3600); // 30 tasks/hour per IP
if (!allowed) {
return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 });
}
let body: any;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const url = typeof body.url === 'string' ? body.url : '';
const executeAt = typeof body.execute_at === 'number' ? body.execute_at : 0;
const headers = typeof body.headers === 'object' && body.headers !== null ? body.headers : {};
const payload = body.payload ?? null;
const retries = typeof body.retries === 'number' ? Math.max(0, Math.min(body.retries, 10)) : 3;
const retryInterval = typeof body.retry_interval === 'number' ? Math.max(1000, body.retry_interval) : 5000;
if (!url) {
return NextResponse.json({ error: 'Missing url' }, { status: 400 });
}
const urlCheck = isAllowedUrl(url);
if (!urlCheck.ok) {
return NextResponse.json({ error: `Blocked URL: ${urlCheck.reason}` }, { status: 400 });
}
const nowSec = Math.floor(Date.now() / 1000);
if (executeAt <= nowSec) {
return NextResponse.json({ error: 'execute_at must be in the future' }, { status: 400 });
}
if (executeAt > nowSec + 86400 * 365 * 2) {
return NextResponse.json({ error: 'execute_at too far in the future (max 2 years)' }, { status: 400 });
}
const id = `task_${crypto.randomUUID()}`;
const task = {
id,
url,
executeAt,
headers: sanitizeHeaders(headers),
payload,
retries,
retryInterval,
createdAt: nowSec,
};
try {
await saveTask(task);
return NextResponse.json(task, { status: 201 });
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'Failed to save task' }, { status: 500 });
}
}
export async function GET() {
try {
const tasks = await listAllTasks();
return NextResponse.json(tasks);
} catch (e: any) {
return NextResponse.json({ error: e?.message || 'Failed to list tasks' }, { status: 500 });
}
}