fix(auth): split Book Dock access from Book Dock administration

Book Dock was a single permission that the standard preset handed to every user, so an ordinary
account could pause processing, trigger a rescan, and finalize straight into the library. Keep
book_dock_access for personal upload and review, and move global control behind a new
manage_book_dock permission in the administration group.

Finalization now also requires library_upload, since it writes into a library. Per-file reads and
mutations are scoped to the uploader unless the caller can manage the dock, which closes the gap
that let any Book Dock user read, edit, or discard another user's entry by id. The summary
websocket now announces a change instead of broadcasting global counts, so each client refetches
its own scoped totals.

Closes #961
This commit is contained in:
neon
2026-08-09 21:27:33 +00:00
parent 68fae9a121
commit ca3c637271
31 changed files with 424 additions and 215 deletions
@@ -774,14 +774,18 @@ describe('Book Dock ingest + finalize (e2e)', () => {
it('enforces Book Dock permission and library access for finalize', async () => {
const noPermissionUser = await createUserAndLogin(context);
const withPermissionUser = await createUserAndLogin(context, {
const accessOnlyUser = await createUserAndLogin(context, {
permissions: [Permission.BookDockAccess],
});
const withPermissionUser = await createUserAndLogin(context, {
permissions: [Permission.BookDockAccess, Permission.LibraryUpload],
});
const destination = await createLibraryWithFolder(context);
const bookDockRow = await createBookDockRow(context, {
fileName: 'permission-check.fb2',
selectedMetadata: { title: 'Permission Check' },
status: 'ready',
uploadedBy: withPermissionUser.userId,
});
const forbiddenSummary = await context.app.inject({
@@ -798,6 +802,19 @@ describe('Book Dock ingest + finalize (e2e)', () => {
});
expect(allowedSummary.statusCode).toBe(200);
const missingUploadPermission = await context.app.inject({
method: 'POST',
url: '/api/v1/book-dock/finalize',
headers: authHeader(accessOnlyUser.accessToken),
payload: {
fileIds: [bookDockRow.id],
defaultLibraryId: destination.libraryId,
defaultFolderId: destination.libraryFolderId,
},
});
expect(missingUploadPermission.statusCode).toBe(403);
expect(missingUploadPermission.json()).toMatchObject({ message: `Missing permission: ${Permission.LibraryUpload}` });
const noAccessFinalize = await context.app.inject({
method: 'POST',
url: '/api/v1/book-dock/finalize',