fix: secret guard, lu bounds, grist typing

This commit is contained in:
electron-rare
2026-06-10 23:16:13 +02:00
parent 81c34cc3fc
commit 04f829c0cf
3 changed files with 13 additions and 3 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ export interface ReadMark {
id: number;
}
async function grist(path: string, init?: RequestInit): Promise<any> {
async function grist<T = unknown>(path: string, init?: RequestInit): Promise<T> {
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<string,
export async function getReads(sub: string, courseSlug: string): Promise<ReadMark[]> {
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<string, unknown> }[] }>(`/tables/Progression/records?filter=${filter}`));
}
export async function addRead(sub: string, email: string, courseSlug: string, moduleN: number, chapitreN: number): Promise<void> {
+1
View File
@@ -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<SessionUser, 'exp'>, 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');
+10 -1
View File
@@ -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 {