From 18ee68312f23a9a9f94648f190d574dfa69a7df9 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 09:12:36 +0200 Subject: [PATCH] u --- src/api/index.js | 4 ++ src/api/routes/category.js | 102 +++++++++++++++++++++++++++++++++++++ src/db/category.js | 16 ++++++ src/db/company.js | 4 +- 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/api/routes/category.js create mode 100644 src/db/category.js diff --git a/src/api/index.js b/src/api/index.js index f3967fd..7ac42e9 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -59,6 +59,10 @@ function corsMiddleware(req, res, next) { async function createApi() { const app = express(); + app.use((_req, res, next) => { + res.setHeader('Cache-Control', 'no-store'); + next(); + }); app.use(corsMiddleware); app.use(express.json()); app.use(cookieParser()); diff --git a/src/api/routes/category.js b/src/api/routes/category.js new file mode 100644 index 0000000..8543e99 --- /dev/null +++ b/src/api/routes/category.js @@ -0,0 +1,102 @@ +import * as db from '../../db/category.js'; + +function parsePositiveInt(value, field) { + const id = Number(value); + if (!Number.isInteger(id) || id <= 0) { + return { error: `${field} is required` }; + } + return { id }; +} + +function categoryErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Parent not found') return 404; + if (error.message === 'Cannot delete root category') return 400; + return 500; +} + +/** + * Registers category routes (requires auth middleware). + * @param {import('express').Application} app - Express app + * @param {string} [basePath=''] - Optional URL prefix for all routes in this module + */ +export function register(app, basePath = '') { + /** + * @route GET /category/tree + * @description Returns the category tree for a company the user belongs to. + * @query {number} companyId + * @returns {Object} 200 - `{ languageId, tree, articles }` + */ + app.get(`${basePath}/category/tree`, async (req, res) => { + const parsed = parsePositiveInt(req.query.companyId, 'companyId'); + if (parsed.error) { + return res.status(400).json({ error: parsed.error }); + } + + try { + const result = await db.getTreeForCompany(req.user.id, parsed.id); + res.status(200).json(result); + } catch (error) { + const status = categoryErrorStatus(error); + if (status === 500) console.error('Get category tree failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @route POST /category + * @description Creates a category under an authorized parent. + * @body {number} parentId + * @body {string} name + * @returns {Object} 201 - `{ category: { id, parentId, sort, name } }` + */ + app.post(`${basePath}/category`, async (req, res) => { + const parent = parsePositiveInt(req.body?.parentId, 'parentId'); + if (parent.error) { + return res.status(400).json({ error: parent.error }); + } + + const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''; + if (!name) { + return res.status(400).json({ error: 'name is required' }); + } + + try { + const category = await db.createUnderParent({ + googleAuthId: req.user.id, + parentId: parent.id, + name, + }); + res.status(201).json({ category }); + } catch (error) { + const status = categoryErrorStatus(error); + if (status === 500) console.error('Create category failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @route DELETE /category/:id + * @description Deletes a category the user is authorized for (not the company root). + * @returns {Object} 204 - No content + */ + app.delete(`${basePath}/category/:id`, async (req, res) => { + const parsed = parsePositiveInt(req.params.id, 'id'); + if (parsed.error) { + return res.status(400).json({ error: parsed.error }); + } + + try { + await db.deleteCategory({ + googleAuthId: req.user.id, + id: parsed.id, + }); + res.sendStatus(204); + } catch (error) { + const status = categoryErrorStatus(error); + if (status === 500) console.error('Delete category failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/db/category.js b/src/db/category.js new file mode 100644 index 0000000..0624c75 --- /dev/null +++ b/src/db/category.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function getTreeForCompany(googleAuthId, companyId) { + const store = await getStore(); + return store.category.getTreeForCompany(googleAuthId, companyId); +} + +export async function createUnderParent({ googleAuthId, parentId, name }) { + const store = await getStore(); + return store.category.createUnderParent({ googleAuthId, parentId, name }); +} + +export async function deleteCategory({ googleAuthId, id }) { + const store = await getStore(); + await store.category.delete({ googleAuthId, id }); +} diff --git a/src/db/company.js b/src/db/company.js index 4b1b350..ccaf5e5 100644 --- a/src/db/company.js +++ b/src/db/company.js @@ -5,9 +5,9 @@ export async function listCompanies(authId) { return store.company.list(authId); } -export async function createCompany(authId, data) { +export async function createCompany(googleAuthId, data) { const store = await getStore(); - return store.company.create({ ...data, authId }); + return store.company.create({ ...data, googleAuthId }); } export async function updateCompany(id, data) {