diff --git a/README.md b/README.md index dc96418..e4d7e31 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,11 @@ Backend mapping: | `updateParentId` | `(id, parentId)` | `void` | | `delete` | `(id)` | `void` — blocked by FK if `categoryarticle` (or child categories) still reference it | | `getTree` | `(rootId)` | `{ id, parentId, sort }[]` | +| `getTreeForCompany` | `(authId, companyId)` | `{ id, parentId, sort }[]` — membership check + company root | + +`getTreeForCompany` throws `Not authorized` if `authId` is not a `companyuser` of `companyId`, and `Company not found` if the company is missing or has no `root_category_id`. Reuses `GET_TREE` after access check. + +`getTree(rootId)` stays for low-level use (tests, internal) when the root id is already known. --- @@ -131,6 +136,7 @@ Backend mapping: | `PATCH /api/company/:id` | `company.update(id, { iso639, name, taxrate, reducedtaxrate })` | | `DELETE /api/company/:id` | `company.delete(id)` | | `PUT /api/auth/selected-company` | `google_auth.setSelectedCompany(authId, companyId)` | +| `GET /api/category/tree?companyId=` | `category.getTreeForCompany(authId, companyId)` | --- diff --git a/scripts/test-api.js b/scripts/test-api.js index 90820e8..2d239af 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -99,6 +99,7 @@ const MENU = { { value: 'category.updateParentId', label: 'updateParentId', hint: 'id, parentId' }, { value: 'category.delete', label: 'delete', hint: 'id' }, { value: 'category.getTree', label: 'getTree', hint: 'rootId → rows' }, + { value: 'category.getTreeForCompany', label: 'getTreeForCompany', hint: 'authId, companyId → rows' }, ], }, companyuser: { @@ -338,6 +339,16 @@ async function runMethod(client, action) { if (rootId == null) return; return ['rows', await client.category.getTree(Number(rootId))]; } + case 'category.getTreeForCompany': { + const authId = await askInt('google_auth id'); + if (authId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + return [ + 'rows', + await client.category.getTreeForCompany(Number(authId), Number(companyId)), + ]; + } case 'companyuser.add': { const companyId = await askInt('Company id'); if (companyId == null) return; diff --git a/src/index.js b/src/index.js index 5cc423a..83a32dd 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import companyUpdate from './store/company/update.js'; import companyDelete from './store/company/delete.js'; import companyList from './store/company/list.js'; import categoryGetTree from './store/category/getTree.js'; +import categoryGetTreeForCompany from './store/category/getTreeForCompany.js'; import categoryCreate from './store/category/create.js'; import categoryUpdateParentId from './store/category/updateParentId.js'; import categoryDelete from './store/category/delete.js'; @@ -45,6 +46,7 @@ export default class LibDashStore { this.category = { getTree: categoryGetTree(pool), + getTreeForCompany: categoryGetTreeForCompany(pool), create: categoryCreate(pool), updateParentId: categoryUpdateParentId(pool), delete: categoryDelete(pool), diff --git a/src/sql/companyuser.js b/src/sql/companyuser.js index 79b5e42..cd0082d 100644 --- a/src/sql/companyuser.js +++ b/src/sql/companyuser.js @@ -5,6 +5,16 @@ INNER JOIN google_auth ga ON ga.id = cu.google_auth_id WHERE ga.email = @email `; +export const GET_COMPANY_ACCESS = ` +SELECT + c.root_category_id AS rootCategoryId, + cu.id AS companyuserId +FROM company c +LEFT JOIN companyuser cu + ON cu.company_id = c.id AND cu.google_auth_id = @authId +WHERE c.id = @companyId +`; + export const ADD = ` INSERT INTO companyuser (google_auth_id, company_id) OUTPUT INSERTED.id diff --git a/src/store/category/getTreeForCompany.js b/src/store/category/getTreeForCompany.js new file mode 100644 index 0000000..e594e2d --- /dev/null +++ b/src/store/category/getTreeForCompany.js @@ -0,0 +1,17 @@ +import { GET_TREE } from '../../sql/category.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function getTreeForCompany(pool) { + const access = resolveCompanyAccess(pool); + + return async (authId, companyId) => { + const { rootCategoryId } = await access(authId, companyId); + + const result = await pool + .request() + .input('rootId', rootCategoryId) + .query(GET_TREE); + + return result.recordset; + }; +} diff --git a/src/store/companyuser/resolveCompanyAccess.js b/src/store/companyuser/resolveCompanyAccess.js new file mode 100644 index 0000000..838994f --- /dev/null +++ b/src/store/companyuser/resolveCompanyAccess.js @@ -0,0 +1,24 @@ +import { GET_COMPANY_ACCESS } from '../../sql/companyuser.js'; + +export default function resolveCompanyAccess(pool) { + return async (authId, companyId) => { + const result = await pool + .request() + .input('authId', authId) + .input('companyId', companyId) + .query(GET_COMPANY_ACCESS); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Company not found'); + } + if (!row.companyuserId) { + throw new Error('Not authorized'); + } + if (!row.rootCategoryId) { + throw new Error('Company not found'); + } + + return { rootCategoryId: row.rootCategoryId }; + }; +}