Files
tutortool/frontend/src/lib/api.ts
s0wlz (Matthias Puchstein) 31f8ef74fe chore: remediate code audit findings and fix CI pipeline failures
- Security: Add Secure flag to checkin identity cookie, implement rate limiting on login, and harden Helm security context.
- Security: Add cargo-audit to CI and Release pipelines for dependency vulnerability scanning.
- Backend: Enable SQLite WAL mode and fix AppState initialization in tests.
- Frontend: Fully type the API client, fix importStudents FormData handling, and pin dependency versions.
- Frontend: Add auto-logout on 401 and resolve authentication initialization race conditions.
- CI/CD: Pin pnpm version in release workflow and include lint/audit quality gates.
2026-05-02 20:40:05 +02:00

155 lines
6.8 KiB
TypeScript

import { browser } from '$app/environment';
import type {
Course, Tutor, Student, Room, Session, Slot, Attendance, Note
} from './types';
import { auth } from './auth.svelte';
const BASE = '/api';
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(BASE + path, {
...init,
credentials: 'include',
headers: {
...(!(init?.body instanceof FormData) && { 'Content-Type': 'application/json' }),
...init?.headers,
}
});
if (res.status === 401 && browser) {
auth.logout();
throw new Error('Unauthorized');
}
if (!res.ok) {
const error = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(error.error || res.statusText);
}
if (res.status === 204) return {} as T;
return res.json() as Promise<T>;
}
export const api = {
auth: {
login: (email: string, password: string) =>
request<{is_superadmin: boolean}>('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
}),
me: () => request<{id: number, email: string, is_superadmin: boolean}>('/auth/me'),
logout: () => request<void>('/auth/logout', { method: 'POST' }),
},
admin: {
courses: {
list: () => request<Course[]>('/admin/courses'),
create: (name: string, semester: string) =>
request<Course>('/admin/courses', {
method: 'POST',
body: JSON.stringify({ name, semester })
}),
listStudents: (course_id: number) => request<Student[]>(`/admin/courses/${course_id}/students`),
addStudent: (course_id: number, name: string) =>
request<Student>(`/admin/courses/${course_id}/students`, {
method: 'POST',
body: JSON.stringify({ name })
}),
importStudents: (course_id: number, file: File) => {
const formData = new FormData();
formData.append('file', file);
return request<any>(`/admin/courses/${course_id}/students/import`, {
method: 'POST',
body: formData
});
},
listTutors: (course_id: number) => request<Tutor[]>(`/admin/courses/${course_id}/tutors`),
assignTutor: (course_id: number, tutor_id: number) =>
request<void>(`/admin/courses/${course_id}/tutors`, {
method: 'POST',
body: JSON.stringify({ tutor_id })
}),
unassignTutor: (course_id: number, tutor_id: number) =>
request<void>(`/admin/courses/${course_id}/tutors/${tutor_id}`, { method: 'DELETE' }),
},
tutors: {
list: () => request<Tutor[]>('/admin/tutors'),
create: (tutor: Partial<Tutor> & { password?: string }) =>
request<Tutor>('/admin/tutors', {
method: 'POST',
body: JSON.stringify(tutor)
}),
delete: (id: number) => request<void>(`/admin/tutors/${id}`, { method: 'DELETE' }),
},
students: {
delete: (id: number) => request<void>(`/admin/students/${id}`, { method: 'DELETE' }),
getAttendance: (id: number) => request<Attendance[]>(`/admin/students/${id}/attendance`),
getNotes: (id: number) => request<Note[]>(`/admin/students/${id}/notes`),
},
rooms: {
list: () => request<Room[]>('/admin/rooms'),
create: (name: string, layout: any[]) =>
request<Room>('/admin/rooms', {
method: 'POST',
body: JSON.stringify({ name, layout })
}),
get: (id: number) => request<Room>(`/admin/rooms/${id}`),
updateLayout: (id: number, layout: any[]) =>
request<Room>(`/admin/rooms/${id}/layout`, {
method: 'PUT',
body: JSON.stringify(layout)
}),
},
sessions: {
list: (course_id: number) => request<Session[]>(`/admin/sessions?course_id=${course_id}`),
create: (course_id: number, week_nr: number, date: string) =>
request<Session>('/admin/sessions', {
method: 'POST',
body: JSON.stringify({ course_id, week_nr, date })
}),
getAttendance: (id: number) => request<any>(`/admin/sessions/${id}/attendance`),
},
slots: {
create: (session_id: number, tutor_id: number, start_time: string, end_time: string, room_id?: number) =>
request<Slot>('/admin/slots', {
method: 'POST',
body: JSON.stringify({ session_id, tutor_id, start_time, end_time, room_id })
}),
updateStatus: (id: number, status: string) =>
request<Slot>(`/admin/slots/${id}/status`, {
method: 'PATCH',
body: JSON.stringify({ status })
}),
delete: (id: number) => request<void>(`/admin/slots/${id}`, { method: 'DELETE' }),
addAttendance: (id: number, student_id: number) =>
request<void>(`/admin/slots/${id}/attendance`, {
method: 'POST',
body: JSON.stringify({ student_id })
}),
deleteAttendance: (slot_id: number, student_id: number) =>
request<void>(`/admin/slots/${slot_id}/attendance/${student_id}`, { method: 'DELETE' }),
getNotes: (id: number) => request<Note[]>(`/admin/slots/${id}/notes`),
upsertNote: (slot_id: number, student_id: number, content: string) =>
request<void>(`/admin/slots/${slot_id}/notes/${student_id}`, {
method: 'PUT',
body: JSON.stringify({ content })
}),
},
export: {
sessionCsv: (id: number) => `${BASE}/admin/export/session/${id}/csv`,
sessionMd: (id: number) => `${BASE}/admin/export/session/${id}/md`,
courseCsv: (id: number) => `${BASE}/admin/export/course/${id}/csv`,
courseMd: (id: number) => `${BASE}/admin/export/course/${id}/md`,
backup: () => `${BASE}/admin/backup`,
}
},
checkin: {
getInfo: (code: string) => request<any>(`/checkin/${code}`),
getStudents: (code: string) => request<Student[]>(`/checkin/${code}/students`),
post: (code: string, student_id: number, seat_id?: string) =>
request<any>('/checkin', {
method: 'POST',
body: JSON.stringify({ code, student_id, seat_id })
}),
}
};