import * as db from '../../db/companyuser.js'; import { clearCachedUser } from '../middleware/auth.js'; import { emit } from '../sse.js'; // #region Helpers function requireSelectedCompany(req) { const companyId = req.user?.selectedCompanyId; if (companyId == null) { return { error: 'No company selected' }; } return { companyId }; } function parsePositiveInt(val, field = 'id') { const id = Number(val); if (!Number.isInteger(id) || id <= 0) { return { error: `${field} is required` }; } return { id }; } function companyuserErrorStatus(error) { if (error.message === 'No company selected') return 400; if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found') return 404; if (error.message === 'Company user not found') return 404; if (error.message === 'User not found') return 404; if (error.message === 'Invalid role') return 400; if (error.message === 'Invalid email') return 400; if (error.message === 'User already in company') return 409; if (error.message === 'Cannot demote the last admin') return 409; if (error.message === 'Cannot remove the last admin') return 409; return 500; } // #endregion /** * Registers company user (membership) routes. All routes require the caller * to be an admin of the selected company. * @param {import('express').Application} app * @param {string} [basePath=''] */ export function register(app, basePath = '') { /** * @openapi * /companyuser: * get: * tags: [CompanyUser] * summary: List company users * description: Lists the members of the selected company with their role. Admin only. * security: * - cookieAuth: [] * x-markdown: companyuser/list.md * responses: * 200: * description: Company members * content: * application/json: * schema: * type: object * properties: * companyusers: * type: array * items: * $ref: '#/components/schemas/CompanyUser' * 400: * description: No company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized (admin of the selected company required) * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region GET /companyuser app.get(`${basePath}/companyuser`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } try { const companyusers = await db.list(req.user.id); res.status(200).json({ companyusers }); } catch (error) { const status = companyuserErrorStatus(error); if (status === 500) console.error('List company users failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion /** * @openapi * /companyuser: * post: * tags: [CompanyUser] * summary: Add an existing user to the company * description: | * Adds an existing (registered) user, identified by email, to the selected company. Admin only. * The added user can then switch to this company via `PUT /auth/selected-company`. * For emails that are not registered yet use `POST /autoinvite`. * security: * - cookieAuth: [] * x-signals: [companyuser] * x-markdown: companyuser/create.md * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [email] * properties: * email: * type: string * role: * type: string * enum: [admin, user] * default: user * example: * email: colleague@example.com * role: user * responses: * 201: * description: Created company user * content: * application/json: * schema: * type: object * properties: * companyuser: * $ref: '#/components/schemas/CompanyUser' * 400: * description: No company selected, invalid email or invalid role * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized (admin of the selected company required) * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: No registered user with this email * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 409: * description: User already in company * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region POST /companyuser app.post(`${basePath}/companyuser`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } try { const companyuser = await db.addByEmail(req.user.id, { email: req.body?.email, role: req.body?.role, }); emit('companyuser', { companyId: selected.companyId }); res.status(201).json({ companyuser }); } catch (error) { const status = companyuserErrorStatus(error); if (status === 500) console.error('Add company user failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion /** * @openapi * /companyuser/{id}: * patch: * tags: [CompanyUser] * summary: Change a company user's role * description: Changes the role of a member of the selected company. The last admin cannot be demoted. Admin only. * security: * - cookieAuth: [] * x-signals: [companyuser] * x-markdown: companyuser/update.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: [role] * properties: * role: * type: string * enum: [admin, user] * responses: * 200: * description: Updated company user * content: * application/json: * schema: * type: object * properties: * companyuser: * $ref: '#/components/schemas/CompanyUser' * 400: * description: Invalid id, role or no company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized (admin of the selected company required) * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company user not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 409: * description: Cannot demote the last admin * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region PATCH /companyuser/:id app.patch(`${basePath}/companyuser/:id`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const parsedId = parsePositiveInt(req.params.id); if (parsedId.error) { return res.status(400).json({ error: parsedId.error }); } try { const companyuser = await db.setRole(req.user.id, parsedId.id, req.body?.role); clearCachedUser(companyuser.googleAuthId); emit('companyuser', { companyId: selected.companyId }); res.status(200).json({ companyuser }); } catch (error) { const status = companyuserErrorStatus(error); if (status === 500) console.error('Update company user failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion /** * @openapi * /companyuser/{id}: * delete: * tags: [CompanyUser] * summary: Remove a user from the company * description: | * Removes a member from the selected company. History entries stay but lose their author link; * if the removed user had this company selected, their selection is cleared. * The last admin cannot be removed. Admin only. * security: * - cookieAuth: [] * x-signals: [companyuser] * x-markdown: companyuser/delete.md * parameters: * - in: path * name: id * required: true * schema: * type: integer * responses: * 204: * description: Removed * 400: * description: Invalid id or no company selected * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 403: * description: Not authorized (admin of the selected company required) * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 404: * description: Company user not found * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 409: * description: Cannot remove the last admin * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ // #region DELETE /companyuser/:id app.delete(`${basePath}/companyuser/:id`, async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { return res.status(400).json({ error: selected.error }); } const parsedId = parsePositiveInt(req.params.id); if (parsedId.error) { return res.status(400).json({ error: parsedId.error }); } try { const removed = await db.remove(req.user.id, parsedId.id); clearCachedUser(removed.googleAuthId); emit('companyuser', { companyId: selected.companyId }); res.sendStatus(204); } catch (error) { const status = companyuserErrorStatus(error); if (status === 500) console.error('Remove company user failed:', error); res.status(status).json({ error: error.message }); } }); // #endregion }