fix(admin): security-audit findings — paginate chapters, lock down unchecked helper (0.41.2)
Addresses the security-audit findings on top of the admin feature stack: M1: /admin/mangas/:id/chapters now paginates (default limit 200, max 500). A long-runner with thousands of chapters would otherwise produce a multi-MB response with that many scalar subqueries per row — admin-only but a real stall risk on one expand-click. Adds explicit pagination tests for the cap and offset; frontend renders a "Showing first N of M" hint when the cap clips the result. L1: repo::user::set_is_admin renamed to set_is_admin_unchecked with a doc-comment pointing at admin_safe_set_is_admin for production use. The short name was a footgun — a future contributor reaching for it would silently bypass self-protection, the last-admin invariant, and the audit log. Used only by integration-test setup; production code goes through the admin_safe_* paths. CSRF posture: build_session_cookie carries a comment that the SameSite=Lax default is the project's CSRF defense for state-changing mutations and breaks the instant anyone adds a side-effecting GET under /admin/*. Spells out what to do then (Strict + explicit token check). Test counts: 43 backend admin tests + 12 vitest admin tests all green; svelte-check 0/0 across 446 files.
This commit is contained in:
@@ -149,7 +149,7 @@ describe('admin api client', () => {
|
||||
expect(url).toContain('limit=100');
|
||||
});
|
||||
|
||||
it('listAdminChapters GETs the nested chapter route', async () => {
|
||||
it('listAdminChapters GETs the nested chapter route and parses the paged envelope', async () => {
|
||||
const chapter = {
|
||||
id: 'c-1',
|
||||
manga_id: 'm-1',
|
||||
@@ -160,13 +160,26 @@ describe('admin api client', () => {
|
||||
sync_state: 'synced' as const,
|
||||
latest_seen_at: null
|
||||
};
|
||||
fetchSpy.mockResolvedValueOnce(ok([chapter]));
|
||||
const rows = await listAdminChapters('m-1');
|
||||
expect(rows).toEqual([chapter]);
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [chapter], page: { limit: 200, offset: 0, total: 1 } })
|
||||
);
|
||||
const resp = await listAdminChapters('m-1');
|
||||
expect(resp.items).toEqual([chapter]);
|
||||
expect(resp.page.total).toBe(1);
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/admin\/mangas\/m-1\/chapters$/);
|
||||
});
|
||||
|
||||
it('listAdminChapters forwards limit + offset query params', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [], page: { limit: 50, offset: 100, total: 0 } })
|
||||
);
|
||||
await listAdminChapters('m-1', { limit: 50, offset: 100 });
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toContain('limit=50');
|
||||
expect(url).toContain('offset=100');
|
||||
});
|
||||
|
||||
// ---- system ----
|
||||
|
||||
it('getSystemStats GETs /v1/admin/system and parses the four-key envelope', async () => {
|
||||
|
||||
Reference in New Issue
Block a user