- 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.
155 lines
6.8 KiB
TypeScript
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 })
|
|
}),
|
|
}
|
|
};
|