Initial working commit
Some checks failed
Automated Container Build / build-and-push (push) Failing after 7s
Some checks failed
Automated Container Build / build-and-push (push) Failing after 7s
This commit is contained in:
parent
666ceb7325
commit
b7ce314f01
105 changed files with 35510 additions and 11 deletions
105
src/app/api/auth/login/route.ts
Normal file
105
src/app/api/auth/login/route.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { createSession } from "@/lib/auth";
|
||||
import { checkRateLimit } from "@/lib/rateLimiter";
|
||||
|
||||
// Progressive lockout thresholds from Section 4
|
||||
function getLockoutDuration(failedAttempts: number): number {
|
||||
if (failedAttempts >= 20) return 24 * 60 * 60 * 1000; // 24 hours
|
||||
if (failedAttempts >= 15) return 30 * 60 * 1000; // 30 minutes
|
||||
if (failedAttempts >= 10) return 5 * 60 * 1000; // 5 minutes
|
||||
if (failedAttempts >= 5) return 1 * 60 * 1000; // 1 minute
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Rate limit by IP
|
||||
const forwarded = request.headers.get("x-forwarded-for");
|
||||
const ip = forwarded?.split(",")[0]?.trim() ?? "unknown";
|
||||
const rateCheck = checkRateLimit(ip);
|
||||
|
||||
if (!rateCheck.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests. Try again shortly." },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body?.password || typeof body.password !== "string") {
|
||||
return NextResponse.json(
|
||||
{ error: "Password is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check lockout state
|
||||
let security = await prisma.authSecurity.findUnique({ where: { id: 1 } });
|
||||
if (!security) {
|
||||
security = await prisma.authSecurity.create({ data: { id: 1 } });
|
||||
}
|
||||
|
||||
if (security.lockedUntil && security.lockedUntil > new Date()) {
|
||||
const remainingMs =
|
||||
security.lockedUntil.getTime() - Date.now();
|
||||
const remainingMin = Math.ceil(remainingMs / 60000);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Account locked. Try again in ${remainingMin} minute${remainingMin !== 1 ? "s" : ""}.`,
|
||||
},
|
||||
{ status: 423 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const passwordHash = process.env.ADMIN_PASSWORD_HASH;
|
||||
|
||||
// For development: if no hash is set, accept any non-empty password
|
||||
let isValid = false;
|
||||
if (!passwordHash) {
|
||||
isValid = body.password.length > 0;
|
||||
} else {
|
||||
// Dynamic import to avoid issues when argon2 isn't installed
|
||||
try {
|
||||
const argon2 = await import("argon2");
|
||||
isValid = await argon2.verify(passwordHash, body.password);
|
||||
} catch {
|
||||
// Fallback: direct comparison (only for development)
|
||||
isValid = body.password === passwordHash;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
const newFailedAttempts = security.failedAttempts + 1;
|
||||
const lockoutMs = getLockoutDuration(newFailedAttempts);
|
||||
const lockedUntil = lockoutMs > 0 ? new Date(Date.now() + lockoutMs) : null;
|
||||
|
||||
await prisma.authSecurity.update({
|
||||
where: { id: 1 },
|
||||
data: {
|
||||
failedAttempts: newFailedAttempts,
|
||||
lockedUntil,
|
||||
lastAttemptAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid password" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Success — reset lockout, create session
|
||||
await prisma.authSecurity.update({
|
||||
where: { id: 1 },
|
||||
data: {
|
||||
failedAttempts: 0,
|
||||
lockedUntil: null,
|
||||
lastAttemptAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await createSession();
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
7
src/app/api/auth/logout/route.ts
Normal file
7
src/app/api/auth/logout/route.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { destroySession } from "@/lib/auth";
|
||||
|
||||
export async function POST() {
|
||||
await destroySession();
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
34
src/app/api/cards/[id]/route.ts
Normal file
34
src/app/api/cards/[id]/route.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as cardService from "@/services/cardService";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
ctx: RouteContext<"/api/cards/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
try {
|
||||
const updated = await cardService.updateCard(id, {
|
||||
front: body?.front?.trim(),
|
||||
back: body?.back?.trim(),
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Card not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/cards/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
|
||||
try {
|
||||
await cardService.deleteCard(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Card not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
37
src/app/api/classes/[id]/route.ts
Normal file
37
src/app/api/classes/[id]/route.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as classService from "@/services/classService";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
ctx: RouteContext<"/api/classes/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body || (!body.name && body.name !== "")) {
|
||||
return NextResponse.json({ error: "Nothing to update" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await classService.updateClass(id, {
|
||||
name: body.name?.trim(),
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Class not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/classes/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
|
||||
try {
|
||||
await classService.deleteClass(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Class not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
20
src/app/api/classes/route.ts
Normal file
20
src/app/api/classes/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as classService from "@/services/classService";
|
||||
|
||||
export async function GET() {
|
||||
const classes = await classService.listClasses();
|
||||
return NextResponse.json(classes);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body?.name || typeof body.name !== "string" || !body.name.trim()) {
|
||||
return NextResponse.json(
|
||||
{ error: "Class name is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const newClass = await classService.createClass(body.name.trim());
|
||||
return NextResponse.json(newClass, { status: 201 });
|
||||
}
|
||||
29
src/app/api/decks/[id]/cards/route.ts
Normal file
29
src/app/api/decks/[id]/cards/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as cardService from "@/services/cardService";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
ctx: RouteContext<"/api/decks/[id]/cards">
|
||||
) {
|
||||
const { id: deckId } = await ctx.params;
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (
|
||||
!body?.front ||
|
||||
!body?.back ||
|
||||
typeof body.front !== "string" ||
|
||||
typeof body.back !== "string"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "front and back are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const card = await cardService.createCard(deckId, {
|
||||
front: body.front.trim(),
|
||||
back: body.back.trim(),
|
||||
});
|
||||
|
||||
return NextResponse.json(card, { status: 201 });
|
||||
}
|
||||
48
src/app/api/decks/[id]/route.ts
Normal file
48
src/app/api/decks/[id]/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as deckService from "@/services/deckService";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/decks/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const deck = await deckService.getDeckWithCards(id);
|
||||
|
||||
if (!deck) {
|
||||
return NextResponse.json({ error: "Deck not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(deck);
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
ctx: RouteContext<"/api/decks/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
try {
|
||||
const updated = await deckService.updateDeck(id, {
|
||||
name: body?.name?.trim(),
|
||||
description: body?.description,
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Deck not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/decks/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
|
||||
try {
|
||||
await deckService.deleteDeck(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Deck not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
14
src/app/api/decks/list/route.ts
Normal file
14
src/app/api/decks/list/route.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as deckService from "@/services/deckService";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const classId = searchParams.get("classId");
|
||||
|
||||
if (!classId) {
|
||||
return NextResponse.json({ error: "classId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const decks = await deckService.listDecksByClass(classId);
|
||||
return NextResponse.json(decks);
|
||||
}
|
||||
34
src/app/api/decks/route.ts
Normal file
34
src/app/api/decks/route.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as deckService from "@/services/deckService";
|
||||
import { flashcardImportSchema } from "@/lib/validation/importSchemas";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { classId, data, name } = body;
|
||||
|
||||
if (!classId || typeof classId !== "string") {
|
||||
return NextResponse.json({ error: "classId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Server-side validation — never trust client
|
||||
const parsed = flashcardImportSchema.safeParse(data);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: "Validation failed", details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const deck = await deckService.createDeckFromImport(
|
||||
classId,
|
||||
parsed.data,
|
||||
name?.trim() || undefined
|
||||
);
|
||||
|
||||
return NextResponse.json(deck, { status: 201 });
|
||||
}
|
||||
66
src/app/api/progress/route.ts
Normal file
66
src/app/api/progress/route.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as progressService from "@/services/progressService";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const contentType = searchParams.get("contentType") as "DECK" | "QUIZ";
|
||||
const contentId = searchParams.get("contentId");
|
||||
const mode = searchParams.get("mode") as "SEQUENTIAL" | "SHUFFLED";
|
||||
|
||||
if (!contentType || !contentId || !mode) {
|
||||
return NextResponse.json(
|
||||
{ error: "contentType, contentId, and mode are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const progress = await progressService.getProgress(
|
||||
contentType,
|
||||
contentId,
|
||||
mode
|
||||
);
|
||||
|
||||
return NextResponse.json(progress);
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body?.contentType || !body?.contentId || !body?.mode) {
|
||||
return NextResponse.json(
|
||||
{ error: "contentType, contentId, and mode are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const progress = await progressService.upsertProgress({
|
||||
contentType: body.contentType,
|
||||
contentId: body.contentId,
|
||||
mode: body.mode,
|
||||
currentIndex: body.currentIndex ?? 0,
|
||||
orderJson: body.orderJson,
|
||||
answersJson: body.answersJson,
|
||||
cardResultsJson: body.cardResultsJson,
|
||||
});
|
||||
|
||||
return NextResponse.json(progress);
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body?.contentType || !body?.contentId || !body?.mode) {
|
||||
return NextResponse.json(
|
||||
{ error: "contentType, contentId, and mode are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await progressService.clearProgress(
|
||||
body.contentType,
|
||||
body.contentId,
|
||||
body.mode
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
72
src/app/api/quizzes/[id]/attempt/route.ts
Normal file
72
src/app/api/quizzes/[id]/attempt/route.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getQuizSetWithQuestions, createQuizAttempt, listQuizAttempts } from "@/services/quizService";
|
||||
import { scoreQuiz } from "@/lib/scoring";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
ctx: RouteContext<"/api/quizzes/[id]/attempt">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body || !body.answersJson) {
|
||||
return NextResponse.json(
|
||||
{ error: "answersJson is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const quizSet = await getQuizSetWithQuestions(id);
|
||||
if (!quizSet) {
|
||||
return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Parse answers
|
||||
let answers: Record<string, string[]>;
|
||||
try {
|
||||
answers = JSON.parse(body.answersJson);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid answers JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Determine which questions were included in this attempt
|
||||
const isPartialRetake = body.isPartialRetake === true;
|
||||
|
||||
// If partial retake, we only score the questions that actually had answers provided
|
||||
// or that were explicitly passed in a questionIds array.
|
||||
// For simplicity, we filter the quizSet questions down to what's in the answers object
|
||||
// if it's a partial retake, though the client will only show those anyway.
|
||||
let questionsToScore = quizSet.questions;
|
||||
if (isPartialRetake) {
|
||||
const answeredIds = Object.keys(answers);
|
||||
questionsToScore = quizSet.questions.filter(q => answeredIds.includes(q.id));
|
||||
}
|
||||
|
||||
// Format questions for the scoring utility
|
||||
const formattedQuestions = questionsToScore.map((q) => ({
|
||||
id: q.id,
|
||||
type: q.type as "MULTIPLE_CHOICE" | "SATA",
|
||||
options: q.options.map((o) => ({ id: o.id, isCorrect: o.isCorrect })),
|
||||
}));
|
||||
|
||||
const { total, maxScore } = scoreQuiz(formattedQuestions, answers);
|
||||
|
||||
const attempt = await createQuizAttempt({
|
||||
quizSetId: id,
|
||||
score: total,
|
||||
maxScore: maxScore,
|
||||
answersJson: body.answersJson,
|
||||
isPartialRetake,
|
||||
});
|
||||
|
||||
return NextResponse.json(attempt, { status: 201 });
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/quizzes/[id]/attempt">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const attempts = await listQuizAttempts(id);
|
||||
return NextResponse.json(attempts);
|
||||
}
|
||||
48
src/app/api/quizzes/[id]/route.ts
Normal file
48
src/app/api/quizzes/[id]/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as quizService from "@/services/quizService";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/quizzes/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const quizSet = await quizService.getQuizSetWithQuestions(id);
|
||||
|
||||
if (!quizSet) {
|
||||
return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(quizSet);
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
ctx: RouteContext<"/api/quizzes/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
try {
|
||||
const updated = await quizService.updateQuizSet(id, {
|
||||
name: body?.name?.trim(),
|
||||
description: body?.description,
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
ctx: RouteContext<"/api/quizzes/[id]">
|
||||
) {
|
||||
const { id } = await ctx.params;
|
||||
|
||||
try {
|
||||
await quizService.deleteQuizSet(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
14
src/app/api/quizzes/list/route.ts
Normal file
14
src/app/api/quizzes/list/route.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as quizService from "@/services/quizService";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const classId = searchParams.get("classId");
|
||||
|
||||
if (!classId) {
|
||||
return NextResponse.json({ error: "classId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const quizSets = await quizService.listQuizSetsByClass(classId);
|
||||
return NextResponse.json(quizSets);
|
||||
}
|
||||
34
src/app/api/quizzes/route.ts
Normal file
34
src/app/api/quizzes/route.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as quizService from "@/services/quizService";
|
||||
import { quizImportSchema } from "@/lib/validation/importSchemas";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { classId, data, name } = body;
|
||||
|
||||
if (!classId || typeof classId !== "string") {
|
||||
return NextResponse.json({ error: "classId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Server-side validation
|
||||
const parsed = quizImportSchema.safeParse(data);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: "Validation failed", details: parsed.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const quizSet = await quizService.createQuizSetFromImport(
|
||||
classId,
|
||||
parsed.data,
|
||||
name?.trim() || undefined
|
||||
);
|
||||
|
||||
return NextResponse.json(quizSet, { status: 201 });
|
||||
}
|
||||
31
src/app/api/settings/llm-instructions/route.ts
Normal file
31
src/app/api/settings/llm-instructions/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import * as settingsService from "@/services/settingsService";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const type = searchParams.get("type") as "flashcards" | "quizzes" || "flashcards";
|
||||
const instructions = await settingsService.getLlmInstructions(type);
|
||||
return NextResponse.json({ value: instructions });
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const type = searchParams.get("type") as "flashcards" | "quizzes" || "flashcards";
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body || typeof body.value !== "string") {
|
||||
return NextResponse.json(
|
||||
{ error: "value is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (body.value === "__RESET__") {
|
||||
await settingsService.resetLlmInstructions(type);
|
||||
const instructions = await settingsService.getLlmInstructions(type);
|
||||
return NextResponse.json({ value: instructions });
|
||||
}
|
||||
|
||||
await settingsService.updateLlmInstructions(type, body.value);
|
||||
return NextResponse.json({ value: body.value });
|
||||
}
|
||||
32
src/app/api/share/route.ts
Normal file
32
src/app/api/share/route.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { toggleShareLink, getShareLinkForContent } from "@/services/shareService";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const targetType = searchParams.get("targetType") as "DECK" | "QUIZ";
|
||||
const contentId = searchParams.get("contentId");
|
||||
|
||||
if (!targetType || !contentId) {
|
||||
return NextResponse.json(
|
||||
{ error: "targetType and contentId are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const link = await getShareLinkForContent(targetType, contentId);
|
||||
return NextResponse.json({ token: link?.id || null });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => null);
|
||||
|
||||
if (!body?.targetType || !body?.contentId) {
|
||||
return NextResponse.json(
|
||||
{ error: "targetType and contentId are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const link = await toggleShareLink(body.targetType, body.contentId);
|
||||
return NextResponse.json({ token: link?.id || null });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue