Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ next-env.d.ts
# CheerpJ build artifacts (built with: bun run build:cheerpj)
/public/cheerpj/

# Playwright
/test-results/
/playwright-report/

# IDE
.idea
.vscode
7 changes: 6 additions & 1 deletion app/api/analytics/event/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { auth } from "@/auth"
import { recordEvent } from "@/lib/analytics"
import type { EventType } from "@/prisma/generated/prisma/client"

const ALLOWED_TYPES: EventType[] = ["lesson_view", "hint_view", "solution_view"]
const ALLOWED_TYPES: EventType[] = [
"lesson_view",
"hint_view",
"solution_view",
"stage_complete",
]

export async function POST(request: Request) {
try {
Expand Down
27 changes: 25 additions & 2 deletions app/api/progress/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { isMultiStageProgressState } from "@/lib/types"

export async function GET(request: Request) {
const session = await auth()
Expand Down Expand Up @@ -28,11 +29,33 @@ export async function PUT(request: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}

const { lessonId, code } = await request.json() as { lessonId: string; code: string }
if (!lessonId || typeof code !== "string") {
const body = (await request.json()) as { lessonId?: unknown; code?: unknown }
const { lessonId, code } = body
if (typeof lessonId !== "string" || !lessonId || typeof code !== "string") {
return NextResponse.json({ error: "Missing lessonId or code" }, { status: 400 })
}

// If the payload looks like JSON, validate it against the multi-stage schema.
// Reject malformed JSON payloads that claim to be v2 but aren't well-formed.
if (code.startsWith("{")) {
try {
const parsed = JSON.parse(code) as unknown
if (
parsed &&
typeof parsed === "object" &&
(parsed as { __v?: unknown }).__v !== undefined &&
!isMultiStageProgressState(parsed)
) {
return NextResponse.json(
{ error: "Invalid multi-stage progress payload" },
{ status: 400 }
)
}
} catch {
// Not JSON — treat as legacy plain-string code (no-op here)
}
}

await prisma.userProgress.upsert({
where: { userId_lessonId: { userId: session.user.id, lessonId } },
create: { userId: session.user.id, lessonId, code },
Expand Down
Loading
Loading