fix: secret guard, lu bounds, grist typing
This commit is contained in:
+2
-2
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user