feat: admin dashboard for user management
Adds an /admin route, visible only to users with role=admin, that lets an operator manage the user base from the UI: - list / search users (active + inactive) - create new accounts (with role and optional display name) - reset a user's password - promote/demote between admin and member - deactivate / reactivate (soft-delete via is_active flag) Backend changes: - New adminOrApiKeyMiddleware accepts EITHER a Bearer JWT belonging to a role=admin user (UI path) OR the existing X-API-Key (bot/server-to-server). - Existing /api/admin/* routes switched to the hybrid middleware, so the same endpoints serve both the dashboard and any external scripts. - Added PUT /api/admin/users/:id/role and PUT /api/admin/users/:id/reactivate. - Self-deactivation and self-demotion are explicitly blocked so an admin can't lock themselves out. Frontend changes: - New Admin.tsx page (table view, modals for create + reset, toast feedback). - Admin button in CollectionList header, only rendered for admin role. - Wired into App.tsx routing. Also: friendly error when poppler-utils is missing on the host (PDF uploads return 501 POPPLER_MISSING with a one-line install hint instead of crashing the request); README clarifies poppler is required for the manual install.
This commit is contained in:
+35
-1
@@ -15,6 +15,32 @@ const execFileAsync = promisify(execFile);
|
||||
|
||||
const TIMEOUT_MS = 60_000;
|
||||
|
||||
class PopplerMissingError extends Error {
|
||||
constructor(binary) {
|
||||
super(
|
||||
`RefBoard couldn't run "${binary}". PDF support requires poppler-utils to be installed on the host. ` +
|
||||
`On macOS: brew install poppler. On Debian/Ubuntu: apt install poppler-utils. ` +
|
||||
`The provided Dockerfile already installs it — this only matters for manual installs.`
|
||||
);
|
||||
this.code = 'POPPLER_MISSING';
|
||||
this.binary = binary;
|
||||
this.statusCode = 501;
|
||||
}
|
||||
}
|
||||
|
||||
function wrapEnoent(binary, fn) {
|
||||
return async (...args) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (err) {
|
||||
if (err && err.code === 'ENOENT' && (err.path === binary || err.syscall === `spawn ${binary}`)) {
|
||||
throw new PopplerMissingError(binary);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a buffer to a temporary file. Returns { tmpPath, cleanup }.
|
||||
* Caller MUST call cleanup() when done.
|
||||
@@ -131,4 +157,12 @@ async function pdfRenderPage(filePath, pageNum, dpi = 150) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { pdfInfo, pdfRenderPage, bufferToTempFile };
|
||||
const safePdfInfo = wrapEnoent('pdfinfo', pdfInfo);
|
||||
const safePdfRenderPage = wrapEnoent('pdftoppm', pdfRenderPage);
|
||||
|
||||
module.exports = {
|
||||
pdfInfo: safePdfInfo,
|
||||
pdfRenderPage: safePdfRenderPage,
|
||||
bufferToTempFile,
|
||||
PopplerMissingError,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user