first commit
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import {
|
||||
SESSION_COOKIE,
|
||||
SESSION_MAX_AGE,
|
||||
createSession,
|
||||
verifyPassword,
|
||||
} from '@/lib/auth/session';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
let password = '';
|
||||
try {
|
||||
const body = await req.json();
|
||||
password = typeof body?.password === 'string' ? body.password : '';
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'bad request' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!(await verifyPassword(password))) {
|
||||
// Small constant delay-ish guard; password compare is already constant-time.
|
||||
return NextResponse.json({ error: 'invalid' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = await createSession();
|
||||
const res = NextResponse.json({ ok: true });
|
||||
res.cookies.set(SESSION_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: SESSION_MAX_AGE,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { SESSION_COOKIE } from '@/lib/auth/session';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function POST() {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
res.cookies.set(SESSION_COOKIE, '', {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: 0,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { setSection, resetSection } from '@/lib/db/store';
|
||||
import {
|
||||
POSTS_KEY,
|
||||
loadPost,
|
||||
loadPostOverrides,
|
||||
isKnownSlug,
|
||||
} from '@/lib/content/posts-store';
|
||||
import type { PostContent } from '@/lib/content/posts';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
const ACCENTS = ['electric', 'violet', 'magenta', 'emerald', 'cyan'];
|
||||
|
||||
/** Minimal structural validation for an incoming PostContent payload. */
|
||||
function validPost(data: unknown): data is {
|
||||
date: string;
|
||||
accent: string;
|
||||
en: { lead: string; blocks: unknown[] };
|
||||
fa: { lead: string; blocks: unknown[] };
|
||||
} {
|
||||
if (!data || typeof data !== 'object') return false;
|
||||
const d = data as Record<string, unknown>;
|
||||
if (typeof d.date !== 'string') return false;
|
||||
if (typeof d.accent !== 'string' || !ACCENTS.includes(d.accent)) return false;
|
||||
for (const loc of ['en', 'fa'] as const) {
|
||||
const art = d[loc] as Record<string, unknown> | undefined;
|
||||
if (!art || typeof art !== 'object') return false;
|
||||
if (typeof art.lead !== 'string') return false;
|
||||
if (!Array.isArray(art.blocks)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET ?slug=rag-eval-framework -> live (merged) article + override flag
|
||||
export async function GET(req: Request) {
|
||||
const slug = new URL(req.url).searchParams.get('slug') ?? '';
|
||||
if (!isKnownSlug(slug)) {
|
||||
return NextResponse.json({ error: 'unknown post' }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({
|
||||
slug,
|
||||
post: loadPost(slug),
|
||||
overridden: slug in loadPostOverrides(),
|
||||
});
|
||||
}
|
||||
|
||||
// POST { slug, data: PostContent } -> save the article override
|
||||
export async function POST(req: Request) {
|
||||
let body: { slug?: string; data?: unknown };
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'bad json' }, { status: 400 });
|
||||
}
|
||||
|
||||
const slug = body.slug ?? '';
|
||||
if (!isKnownSlug(slug)) {
|
||||
return NextResponse.json({ error: 'unknown post' }, { status: 400 });
|
||||
}
|
||||
if (!validPost(body.data)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'expected a { date, accent, en:{lead,blocks}, fa:{lead,blocks} } payload' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const overrides = loadPostOverrides();
|
||||
// validPost has confirmed the shape (incl. accent ∈ ACCENTS) above.
|
||||
overrides[slug] = body.data as unknown as PostContent;
|
||||
setSection(POSTS_KEY, overrides);
|
||||
|
||||
revalidatePath(`/blog/${slug}`);
|
||||
revalidatePath('/', 'layout');
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
// DELETE ?slug=… -> revert one article to its in-code default
|
||||
export async function DELETE(req: Request) {
|
||||
const slug = new URL(req.url).searchParams.get('slug') ?? '';
|
||||
if (!isKnownSlug(slug)) {
|
||||
return NextResponse.json({ error: 'unknown post' }, { status: 400 });
|
||||
}
|
||||
|
||||
const overrides = loadPostOverrides();
|
||||
delete overrides[slug];
|
||||
if (Object.keys(overrides).length === 0) {
|
||||
resetSection(POSTS_KEY);
|
||||
} else {
|
||||
setSection(POSTS_KEY, overrides);
|
||||
}
|
||||
|
||||
revalidatePath(`/blog/${slug}`);
|
||||
revalidatePath('/', 'layout');
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { setSection, resetSection, getSection } from '@/lib/db/store';
|
||||
import { isEditableKey } from '@/lib/content/sections';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
// GET ?key=hero -> current stored override (or null)
|
||||
export async function GET(req: Request) {
|
||||
const key = new URL(req.url).searchParams.get('key') ?? '';
|
||||
if (!isEditableKey(key)) {
|
||||
return NextResponse.json({ error: 'unknown section' }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ key, override: getSection(key) });
|
||||
}
|
||||
|
||||
// POST { key, data: { fa, en } } -> save override
|
||||
export async function POST(req: Request) {
|
||||
let body: { key?: string; data?: { fa?: unknown; en?: unknown } };
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'bad json' }, { status: 400 });
|
||||
}
|
||||
|
||||
const key = body.key ?? '';
|
||||
if (!isEditableKey(key)) {
|
||||
return NextResponse.json({ error: 'unknown section' }, { status: 400 });
|
||||
}
|
||||
if (!body.data || typeof body.data !== 'object' || !('fa' in body.data) || !('en' in body.data)) {
|
||||
return NextResponse.json({ error: 'expected { fa, en } payload' }, { status: 400 });
|
||||
}
|
||||
|
||||
setSection(key, { fa: body.data.fa, en: body.data.en });
|
||||
// (site) layout is force-dynamic, but revalidate keeps any cached routes fresh.
|
||||
revalidatePath('/', 'layout');
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
// DELETE ?key=hero -> revert to in-code default
|
||||
export async function DELETE(req: Request) {
|
||||
const key = new URL(req.url).searchParams.get('key') ?? '';
|
||||
if (!isEditableKey(key)) {
|
||||
return NextResponse.json({ error: 'unknown section' }, { status: 400 });
|
||||
}
|
||||
resetSection(key);
|
||||
revalidatePath('/', 'layout');
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { extname, join } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { UPLOADS_DIR } from '@/lib/db/store';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
const ALLOWED = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.avif']);
|
||||
const MAX_BYTES = 8 * 1024 * 1024; // 8 MB
|
||||
|
||||
export async function POST(req: Request) {
|
||||
let form: FormData;
|
||||
try {
|
||||
form = await req.formData();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'expected multipart form' }, { status: 400 });
|
||||
}
|
||||
|
||||
const file = form.get('file');
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ error: 'no file' }, { status: 400 });
|
||||
}
|
||||
if (file.size > MAX_BYTES) {
|
||||
return NextResponse.json({ error: 'file too large (max 8MB)' }, { status: 413 });
|
||||
}
|
||||
|
||||
const ext = extname(file.name).toLowerCase();
|
||||
if (!ALLOWED.has(ext)) {
|
||||
return NextResponse.json({ error: `unsupported type ${ext}` }, { status: 415 });
|
||||
}
|
||||
|
||||
const name = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
try {
|
||||
await mkdir(UPLOADS_DIR, { recursive: true });
|
||||
await writeFile(join(UPLOADS_DIR, name), buffer);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'write failed' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ url: `/api/uploads/${name}`, name });
|
||||
}
|
||||
Reference in New Issue
Block a user