feat(backend): books, tasks, calendar feed, time tracking, profiles, admin API
- Schema: books, tasks (book/page links, status, priority, due_at), time_entries (open entry = running timer), user profile columns (avatar, locale, theme, bg_image, bg_blur) - Books CRUD; pages can live in a book (book_id, validated same-workspace) - Tasks CRUD with status filter and due_at range query for the calendar - Time API: start (409 if running), stop (computes duration), list with open timer, update, delete - Users: fuzzy discovery for invites, PATCH /me profile/appearance, password change; members list now includes avatars - Admin API (is_admin guard): overview counts, user list, grant/revoke admin, delete user (self-demote/delete blocked)
This commit is contained in:
@@ -30,24 +30,31 @@ router.get('/by-workspace/:workspaceId', async (req, res) => {
|
||||
return res.json({ pages: rows, role });
|
||||
});
|
||||
|
||||
// POST /api/pages { workspace_id, parent_id?, title? }
|
||||
// POST /api/pages { workspace_id, parent_id?, book_id?, title? }
|
||||
router.post('/', async (req, res) => {
|
||||
const { workspace_id, parent_id = null, title = 'Untitled' } = req.body ?? {};
|
||||
const { workspace_id, parent_id = null, book_id = null, title = 'Untitled' } = req.body ?? {};
|
||||
if (!workspace_id) return res.status(400).json({ error: 'workspace_id required' });
|
||||
const role = await getRole(workspace_id, req.user.id);
|
||||
if (role !== 'owner' && role !== 'admin' && role !== 'member') {
|
||||
return res.status(403).json({ error: 'Viewers cannot create pages' });
|
||||
}
|
||||
if (book_id) {
|
||||
const { rows } = await query('SELECT id FROM books WHERE id = $1 AND workspace_id = $2', [book_id, workspace_id]);
|
||||
if (!rows[0]) return res.status(400).json({ error: 'Invalid book_id' });
|
||||
}
|
||||
if (parent_id) {
|
||||
const parent = await getPageWithWorkspace(parent_id);
|
||||
if (!parent || parent.workspace_id !== workspace_id) {
|
||||
return res.status(400).json({ error: 'Invalid parent_id' });
|
||||
}
|
||||
if ((parent.book_id || null) !== (book_id || null)) {
|
||||
return res.status(400).json({ error: 'Parent lives in a different book' });
|
||||
}
|
||||
}
|
||||
const { rows } = await query(
|
||||
`INSERT INTO pages (workspace_id, parent_id, title, created_by)
|
||||
VALUES ($1,$2,$3,$4) RETURNING *`,
|
||||
[workspace_id, parent_id, String(title).slice(0, 300) || 'Untitled', req.user.id],
|
||||
`INSERT INTO pages (workspace_id, parent_id, book_id, title, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5) RETURNING *`,
|
||||
[workspace_id, parent_id, book_id, String(title).slice(0, 300) || 'Untitled', req.user.id],
|
||||
);
|
||||
return res.status(201).json({ page: rows[0] });
|
||||
});
|
||||
@@ -71,7 +78,7 @@ router.patch('/:id', async (req, res) => {
|
||||
const page = await getPageWithWorkspace(req.params.id);
|
||||
if (!page) return res.status(404).json({ error: 'Not found' });
|
||||
if (!(await canWrite(req.user.id, page))) return res.status(403).json({ error: 'Viewers cannot edit' });
|
||||
const { title, icon, cover, content_json, content_html, position, parent_id, is_archived } = req.body ?? {};
|
||||
const { title, icon, cover, content_json, content_html, position, parent_id, book_id, is_archived } = req.body ?? {};
|
||||
if (parent_id !== undefined && parent_id !== null) {
|
||||
if (parent_id === page.id) return res.status(400).json({ error: 'Page cannot be its own parent' });
|
||||
const parent = await getPageWithWorkspace(parent_id);
|
||||
@@ -79,6 +86,10 @@ router.patch('/:id', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid parent_id' });
|
||||
}
|
||||
}
|
||||
if (book_id !== undefined && book_id !== null) {
|
||||
const { rows } = await query('SELECT id FROM books WHERE id = $1 AND workspace_id = $2', [book_id, page.workspace_id]);
|
||||
if (!rows[0]) return res.status(400).json({ error: 'Invalid book_id' });
|
||||
}
|
||||
const { rows } = await query(
|
||||
`UPDATE pages SET
|
||||
title = COALESCE($1, title),
|
||||
@@ -88,6 +99,7 @@ router.patch('/:id', async (req, res) => {
|
||||
content_html = COALESCE($5, content_html),
|
||||
position = COALESCE($6, position),
|
||||
parent_id = CASE WHEN $8 THEN $7 ELSE parent_id END,
|
||||
book_id = CASE WHEN $12 THEN $11 ELSE book_id END,
|
||||
is_archived = COALESCE($9, is_archived)
|
||||
WHERE id = $10 RETURNING *`,
|
||||
[
|
||||
@@ -101,6 +113,8 @@ router.patch('/:id', async (req, res) => {
|
||||
parent_id !== undefined,
|
||||
is_archived !== undefined ? Boolean(is_archived) : null,
|
||||
page.id,
|
||||
book_id !== undefined ? book_id : null,
|
||||
book_id !== undefined,
|
||||
],
|
||||
);
|
||||
const updated = rows[0];
|
||||
|
||||
Reference in New Issue
Block a user