diff --git a/src/lib/grist.ts b/src/lib/grist.ts index 084b494..07c3a90 100644 --- a/src/lib/grist.ts +++ b/src/lib/grist.ts @@ -10,7 +10,7 @@ export interface ReadMark { id: number; } -async function grist(path: string, init?: RequestInit): Promise { +async function grist(path: string, init?: RequestInit): Promise { const res = await fetch(`${BASE()}/api/docs/${DOC()}${path}`, { ...init, headers: { Authorization: `Bearer ${KEY()}`, 'Content-Type': 'application/json', ...(init?.headers ?? {}) }, @@ -32,7 +32,7 @@ export function mapRecords(raw: { records: { id: number; fields: Record { const filter = encodeURIComponent(JSON.stringify({ user_sub: [sub], course_slug: [courseSlug] })); - return mapRecords(await grist(`/tables/Progression/records?filter=${filter}`)); + return mapRecords(await grist<{ records: { id: number; fields: Record }[] }>(`/tables/Progression/records?filter=${filter}`)); } export async function addRead(sub: string, email: string, courseSlug: string, moduleN: number, chapitreN: number): Promise { diff --git a/src/lib/session.ts b/src/lib/session.ts index d72fe5e..47e170e 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -8,6 +8,7 @@ const secret = () => process.env.SESSION_SECRET || ''; const b64u = (b: Buffer | string) => Buffer.from(b).toString('base64url'); export function signSession(user: Omit, ttlSec = 7 * 86400): string { + if (!secret()) throw new Error('SESSION_SECRET is not configured'); const payload: SessionUser = { ...user, exp: Math.floor(Date.now() / 1000) + ttlSec }; const body = b64u(JSON.stringify(payload)); const mac = createHmac('sha256', secret()).update(body).digest('base64url'); diff --git a/src/pages/api/lu.ts b/src/pages/api/lu.ts index 0001766..e146a3f 100644 --- a/src/pages/api/lu.ts +++ b/src/pages/api/lu.ts @@ -2,6 +2,7 @@ import type { APIRoute } from 'astro'; import { verifySession, SESSION_COOKIE } from '../../lib/session'; import { bySlug } from '../../config/courses'; import { addRead } from '../../lib/grist'; +import { getCourseBooks } from '../../lib/moodle'; import { syncToMoodle } from '../../lib/moodle-sync'; const json = (status: number, body: unknown) => @@ -12,7 +13,7 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => { if (!user) return json(401, { error: 'auth' }); const ct = request.headers.get('content-type') ?? ''; - const isForm = ct.includes('application/x-www-form-urlencoded') || ct.includes('multipart/form-data'); + const isForm = ct.includes('application/x-www-form-urlencoded'); let slug = '', moduleRaw = '', chapitreRaw = ''; try { @@ -36,6 +37,14 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => { const c = Number(chapitreRaw); if (!course || !course.published || !Number.isInteger(m) || m < 1 || !Number.isInteger(c) || c < 1) return json(400, { error: 'params' }); + // upper bounds against the real course structure (SWR-cached, cheap) + try { + const modules = await getCourseBooks(course.id); + if (m > modules.length || c > (modules[m - 1]?.chapters.length ?? 0)) + return json(400, { error: 'params' }); + } catch { + return json(503, { error: 'moodle' }); + } const back = `/cours/${slug}/module-${m}/chapitre-${c}`; try {