Files
lms-sb/src/app/api/wrapped/run/route.ts
T
admins 06b8d9f64b Add Obsidian Wrapped feature (inside-LMS, client-side vault parsing)
- WrappedRun Prisma model + migration (aggregates only, no note names/text)
- /wrapped authed route: FSA + webkitdirectory + drag-drop reader, Web Worker parser
- archetype + score heuristics, ДС-2 result card
- POST /api/wrapped/run (userId from session), public /share/wrapped/[token]
- next/og OG image (Satori+resvg-wasm, alpine-safe) + Fira Mono ttf
- middleware: /share/wrapped public

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:19:59 +05:00

48 lines
1.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { randomBytes } from "crypto";
// Пишет агрегаты прогона Wrapped. userId — строго из сессии, не из тела.
// Имена/тексты заметок сюда не приходят (только числа/строки-агрегаты).
export async function POST(req: Request) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
let b: {
period?: string;
noteCount?: number;
linkCount?: number;
tagCount?: number;
topTag?: string | null;
archetype?: string | null;
score?: number;
};
try {
b = await req.json();
} catch {
return NextResponse.json({ error: "bad json" }, { status: 400 });
}
const period = ["year", "quarter", "all"].includes(String(b.period)) ? String(b.period) : "year";
const VALID = ["architect", "gardener", "librarian", "pragmatist"];
const run = await prisma.wrappedRun.create({
data: {
userId: session.user.id,
period,
noteCount: Number(b.noteCount) || 0,
linkCount: Number(b.linkCount) || 0,
tagCount: Number(b.tagCount) || 0,
topTag: b.topTag ? String(b.topTag).slice(0, 60) : null,
archetype: VALID.includes(String(b.archetype)) ? String(b.archetype) : null,
score: Math.max(0, Math.min(100, Number(b.score) || 0)),
shareToken: randomBytes(9).toString("base64url"),
},
select: { shareToken: true },
});
return NextResponse.json({ shareToken: run.shareToken });
}