Files
dashback/src/api/routes/company.js
2026-11-03 06:20:06 +01:00

1020 lines
38 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import multer from 'multer';
import JSZip from 'jszip';
import env from '../../config/env.js';
import * as db from '../../db/company.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import * as googleAuth from '../../db/googleAuth.js';
import { clearCachedUser } from '../middleware/auth.js';
import { emit } from '../sse.js';
import { initCompanyVfs } from '../../tools/vfs.js';
import { createDefaultLabelTemplates } from '../../services/defaultLabelTemplates.js';
import { executeCompanyImport } from '../../services/companyImport.js';
import { executeCompanyExport } from '../../services/companyExport.js';
import { clearCompanyPrinters } from '../../services/companyPrinterService.js';
// #region Helpers
const uploadZip = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 100 * 1024 * 1024 }, // 100 MB
fileFilter: (_req, file, cb) => {
const name = file.originalname?.toLowerCase() || '';
const isZipExt = name.endsWith('.zip');
const isZipMime =
file.mimetype === 'application/zip' ||
file.mimetype === 'application/x-zip-compressed';
if (isZipExt || isZipMime) {
cb(null, true);
} else {
cb(new Error('Only .zip archives are accepted'));
}
},
}).single('file');
function uploadCompanyImportFile(req, res, next) {
uploadZip(req, res, (err) => {
if (err) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ error: 'File too large (max 100 MB)' });
}
return res.status(400).json({ error: err.message });
}
next();
});
}
function userPayload(authRow) {
return {
id: authRow.id,
email: authRow.email,
selectedCompanyId: authRow.selectedCompanyId,
};
}
function parseCompanyBody(body) {
const { iso639, name, taxrate, reducedtaxrate } = body ?? {};
if (!iso639 || !name || taxrate == null || reducedtaxrate == null) {
return { error: 'iso639, name, taxrate, and reducedtaxrate are required' };
}
if (typeof taxrate !== 'number' || !Number.isFinite(taxrate) || taxrate < 0) {
return { error: 'taxrate must be a non-negative number' };
}
if (typeof reducedtaxrate !== 'number' || !Number.isFinite(reducedtaxrate) || reducedtaxrate < 0) {
return { error: 'reducedtaxrate must be a non-negative number' };
}
return { iso639, name, taxrate, reducedtaxrate };
}
function parseCompanyId(id) {
const companyId = Number(id);
if (!Number.isInteger(companyId) || companyId <= 0) {
return { error: 'Invalid company id' };
}
return { companyId };
}
function requireSelectedCompany(req) {
const id = req.user?.selectedCompanyId;
if (id == null) {
const err = new Error('No company selected');
err.statusCode = 400;
throw err;
}
return id;
}
function companyErrorStatus(error) {
if (error.message === 'Company not found') return 404;
if (error.message === 'Company name not available') return 409;
if (error.message === 'Invalid tax rate' || error.message === 'Text required' || error.message === 'No company selected') return 400;
if (error.message === 'Not authorized' || error.message === 'Invited users cannot create a company') return 403;
return 500;
}
// #endregion
/**
* Registers company CRUD 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 = '') {
/**
* @openapi
* /company:
* get:
* tags: [Company]
* summary: List companies
* description: Lists companies for the authenticated user.
* security:
* - cookieAuth: []
* x-markdown: company/list.md
* responses:
* 200:
* description: Company list
* content:
* application/json:
* schema:
* type: object
* properties:
* companies:
* type: array
* items:
* $ref: '#/components/schemas/Company'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Create company
* description: Creates a company and selects it for the current user.
* security:
* - cookieAuth: []
* x-markdown: company/create.md
* x-signals: [user, company]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [iso639, name, taxrate, reducedtaxrate]
* properties:
* iso639:
* type: string
* name:
* type: string
* taxrate:
* type: number
* reducedtaxrate:
* type: number
* example:
* iso639: de
* name: Acme GmbH
* taxrate: 19
* reducedtaxrate: 7
* responses:
* 201:
* description: Created; selected company updated
* content:
* application/json:
* schema:
* type: object
* properties:
* company:
* $ref: '#/components/schemas/Company'
* user:
* $ref: '#/components/schemas/User'
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (invited users cannot create companies when implicit create is disabled)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /company
app.get(`${basePath}/company`, async (req, res) => {
try {
const companies = await db.listCompanies(req.user.id);
res.status(200).json({ companies });
} catch (error) {
console.error('List companies failed:', error);
res.status(500).json({ error: 'Failed to list companies' });
}
});
// #endregion
// #region POST /company
app.post(`${basePath}/company`, async (req, res) => {
const body = parseCompanyBody(req.body);
if (body.error) {
return res.status(400).json({ error: body.error });
}
if (!env.implicitCreate) {
const isInvited = await db.isInvitedUser(req.user.id);
if (isInvited) {
return res.status(403).json({ error: 'Invited users cannot create a company' });
}
}
try {
const company = await db.createCompany(req.user.id, body);
await googleAuth.setSelectedCompany(req.user.id, company.id);
clearCachedUser(req.user.id);
try {
await initCompanyVfs({ googleAuthId: req.user.id });
} catch (vfsErr) {
console.error('Failed to initialize company VFS:', vfsErr);
}
try {
await createDefaultLabelTemplates(req.user.id);
} catch (tplErr) {
console.error('Failed to create default label templates:', tplErr);
}
const authRow = await googleAuth.getAllowedEmail(req.user.email);
res.status(201).json({ company, user: userPayload(authRow) });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Create company failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /company/history:
* get:
* tags: [Company]
* summary: Get company history
* description: |
* Returns `companyhistory` rows for the selected company, newest first (`date` DESC, `id` DESC).
* security:
* - cookieAuth: []
* x-markdown: company/history.md
* parameters:
* - in: query
* name: limit
* required: false
* schema:
* type: integer
* minimum: 1
* maximum: 1000
* description: Maximum number of entries to return (newest first). Omit for all entries.
* responses:
* 200:
* description: Company history (newest first)
* content:
* application/json:
* schema:
* type: object
* required: [history, truncated]
* properties:
* history:
* type: array
* items:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* truncated:
* type: boolean
* description: True when a limit was applied and the result set was exactly that size (more entries may exist).
* 400:
* description: Invalid limit 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Add company history entry
* description: |
* Appends an entry to `companyhistory` for the selected company.
* security:
* - cookieAuth: []
* x-markdown: company/addHistory.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [text]
* properties:
* text:
* type: string
* description: History entry text or note
* responses:
* 201:
* description: History entry created
* content:
* application/json:
* schema:
* type: object
* required: [history]
* properties:
* history:
* $ref: '#/components/schemas/CompanyHistoryEntry'
* 400:
* description: Text required 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: Forbidden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /company/history
app.get(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const rawLimit = req.query.limit;
let limit = null;
if (rawLimit !== undefined) {
limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
return res.status(400).json({ error: 'limit must be an integer between 1 and 1000' });
}
}
try {
const history = await db.getCompanyHistory(req.user.id, limit != null ? { limit } : undefined);
const truncated = limit != null && history.length === limit;
res.status(200).json({ history, truncated });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Get company history failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
// #region POST /company/history
app.post(`${basePath}/company/history`, async (req, res) => {
try {
requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
if (!text) {
return res.status(400).json({ error: 'text is required' });
}
try {
const entry = await db.addCompanyHistory(req.user.id, { text });
res.status(201).json({ history: entry });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Add company history failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /company/import:
* post:
* tags: [Company]
* summary: Import company catalog from zip archive (SSE)
* description: |
* Uploads a .zip archive containing company export data (categories, articles, pictures),
* preserves archive contents under `data/import/<companyId>/`, and executes full catalog
* import (categories hierarchy, articles upsert, variant linking, image processing).
* Streams progress events via Server-Sent Events (`text/event-stream`).
* security:
* - cookieAuth: []
* x-markdown: company/import.md
* requestBody:
* required: false
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* file:
* type: string
* format: binary
* description: ZIP archive (.zip) containing company data
* responses:
* 200:
* description: Import progress stream (`text/event-stream`) or JSON result
* content:
* text/event-stream:
* schema:
* type: string
* application/json:
* schema:
* type: object
* required: [ok, message]
* properties:
* ok:
* type: boolean
* example: true
* message:
* type: string
* example: Import erfolgreich abgeschlossen.
* stats:
* type: object
* 400:
* description: Bad request (missing file, not a zip, or no company selected)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /company/import
const activeImportsByCompany = new Set();
/** Signals for data the catalog import may create/modify (emitted even after a failed, partially applied run). */
const IMPORT_SIGNALS = ['category', 'articles', 'brand', 'attributename', 'warehouse', 'carrier', 'supplier', 'order', 'shipment', 'stock', 'ledger'];
const emitImportSignals = (companyId) => {
for (const signal of IMPORT_SIGNALS) {
emit(signal, signal === 'brand'
? { companyId, data: { sender: '', brandId: null } }
: { companyId });
}
};
app.post(
`${basePath}/company/import`,
async (req, res, next) => {
try {
const companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
next();
} catch (error) {
const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
}
},
uploadCompanyImportFile,
async (req, res) => {
let companyId;
try {
companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
} catch (error) {
const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
}
if (activeImportsByCompany.has(companyId)) {
return res.status(409).json({ error: 'Import already in progress for this company' });
}
activeImportsByCompany.add(companyId);
try {
const importDir = path.resolve(process.cwd(), 'data/import', String(companyId));
// If a file was uploaded, unpack and preserve it
if (req.file?.buffer) {
const originalName = req.file.originalname || 'import.zip';
if (!originalName.toLowerCase().endsWith('.zip')) {
return res.status(400).json({ error: 'Uploaded file must have a .zip extension' });
}
let zip;
try {
zip = await JSZip.loadAsync(req.file.buffer);
} catch (err) {
return res.status(400).json({ error: 'Invalid or corrupt zip archive' });
}
try {
// Do not clean the import upload directory so files remain available for inspection/analysis
// fs.rmSync(importDir, { recursive: true, force: true });
fs.mkdirSync(importDir, { recursive: true });
// Preserve the raw zip archive
fs.writeFileSync(path.join(importDir, 'import.zip'), req.file.buffer);
console.log(`[Company Import] Extracting archive "${originalName}" into ${importDir}:`);
let fileCount = 0;
let dirCount = 0;
for (const [relativePath, entry] of Object.entries(zip.files)) {
const safePath = path.normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
const destPath = path.resolve(importDir, safePath);
if (!destPath.startsWith(importDir)) {
console.warn(`[Company Import] Skipped unsafe path: ${relativePath}`);
continue;
}
if (entry.dir) {
dirCount++;
fs.mkdirSync(destPath, { recursive: true });
} else {
fileCount++;
fs.mkdirSync(path.dirname(destPath), { recursive: true });
const content = await entry.async('nodebuffer');
fs.writeFileSync(destPath, content);
}
}
console.log(`[Company Import] Preserved ${fileCount} files and ${dirCount} directories in ${importDir}`);
} catch (err) {
console.error('[Company Import] Failed to extract archive:', err);
return res.status(500).json({ error: 'Failed to extract import archive' });
}
} else {
// No file uploaded in this request; check if existing preserved files are present
const catFile = path.join(importDir, 'categories.csv');
const artFile = path.join(importDir, 'articles.csv');
if (!fs.existsSync(catFile) || !fs.existsSync(artFile)) {
return res.status(400).json({ error: 'Zip file is required in "file" form field' });
}
}
const wantsSse =
req.headers.accept?.includes('text/event-stream') ||
req.query.stream === '1' ||
req.query.sse === '1' ||
!req.headers.accept?.includes('application/json');
if (wantsSse) {
req.socket?.setTimeout(0);
res.setTimeout?.(0);
req.socket?.setNoDelay(true);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
if (typeof res.flushHeaders === 'function') {
res.flushHeaders();
}
// Initial ping to establish frame through any proxy
res.write(': ping\n\n');
req.on('close', () => {
console.warn(`[Company Import SSE] Client request closed (writableEnded: ${res.writableEnded}, destroyed: ${res.destroyed})`);
});
res.on('close', () => {
if (!res.writableEnded) {
console.warn(`[Company Import SSE] Response stream closed (destroyed: ${res.destroyed})`);
}
});
const sseWrite = (event, data) => {
if (res.writableEnded || res.destroyed) {
console.warn(`[Company Import SSE DROP] event="${event}" dropped: connection closed (destroyed: ${res.destroyed}, ended: ${res.writableEnded})`);
return;
}
if (event === 'progress') {
console.log(`[Company Import SSE] progress cat=${data.categoriesdone}/${data.categoriestotal} art=${data.articlesdone}/${data.articlestotal} img=${data.imagesdone}/${data.imagestotal} rec=${data.receiptsdone}/${data.receiptstotal}`);
} else if (event === 'images') {
console.log(`[Company Import SSE] images batch count=${Array.isArray(data) ? data.length : 1}`);
} else {
console.log(`[Company Import SSE] event="${event}"`, data);
}
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
if (typeof res.flush === 'function') res.flush();
if (typeof req.socket?.uncork === 'function') req.socket.uncork();
};
sseWrite('status', { message: 'Import gestartet. Daten werden verarbeitet...' });
try {
const outcome = await executeCompanyImport({
googleAuthId: req.user.id,
companyId,
importDir,
onProgress: (counts) => {
sseWrite('progress', counts);
},
onImages: (skelehashes) => {
sseWrite('images', skelehashes);
},
});
sseWrite('done', outcome);
res.end();
} catch (err) {
console.error('[Company Import] Import execution failed:', err);
sseWrite('error', { message: err.message || 'Import fehlgeschlagen' });
res.end();
}
} else {
try {
const outcome = await executeCompanyImport({
googleAuthId: req.user.id,
companyId,
importDir,
onProgress: (counts) => {
console.log('[Import Progress]', counts);
},
});
res.status(200).json(outcome);
} catch (err) {
console.error('[Company Import] Import execution failed:', err);
res.status(500).json({ error: err.message || 'Import fehlgeschlagen' });
}
}
} finally {
activeImportsByCompany.delete(companyId);
emitImportSignals(companyId);
}
}
);
// #endregion
/**
* @openapi
* /company/export:
* get:
* tags: [Company]
* summary: Export company catalog as zip archive (SSE)
* description: |
* Exports full company catalog data (categories hierarchy, articles, variants, brands, pictures)
* into a round-trip compatible .zip archive matching the format of `/company/import`.
* Streams progress events via Server-Sent Events (`text/event-stream`), followed by base64-encoded
* binary chunks in `chunk` events and a final `done` event.
* security:
* - cookieAuth: []
* x-markdown: company/export.md
* parameters:
* - in: query
* name: stream
* schema:
* type: string
* enum: ['0', '1']
* description: Force SSE stream mode (`1`) or direct binary download (`0`)
* responses:
* 200:
* description: Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
* content:
* text/event-stream:
* schema:
* type: string
* application/zip:
* schema:
* type: string
* format: binary
* 400:
* description: Bad request (no company selected)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Export company catalog as zip archive (SSE)
* description: POST variant of `/company/export` with identical SSE streaming semantics.
* security:
* - cookieAuth: []
* responses:
* 200:
* description: Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
* content:
* text/event-stream:
* schema:
* type: string
* application/zip:
* schema:
* type: string
* format: binary
* 400:
* description: Bad request (no company selected)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET/POST /company/export
const handleCompanyExport = async (req, res) => {
let companyId;
try {
companyId = requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const wantsSse =
req.headers.accept?.includes('text/event-stream') ||
req.query.stream === '1' ||
req.query.sse === '1' ||
(!req.headers.accept?.includes('application/zip') && !req.path.endsWith('.zip') && req.query.download !== '1');
if (wantsSse) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
if (typeof res.flushHeaders === 'function') {
res.flushHeaders();
}
const sseWrite = (event, data) => {
if (res.writableEnded || res.destroyed) return;
const payload = typeof data === 'string' ? data : JSON.stringify(data);
res.write(`event: ${event}\ndata: ${payload}\n\n`);
if (typeof res.flush === 'function') res.flush();
};
sseWrite('status', { message: 'Export gestartet. Daten werden verarbeitet...' });
try {
const outcome = await executeCompanyExport({
googleAuthId: req.user.id,
companyId,
onStatus: (status) => sseWrite('status', status),
onProgress: (counts) => sseWrite('progress', counts),
});
// Stream binary chunks as base64 in SSE 'chunk' events
const CHUNK_SIZE = 64 * 1024; // 64 KB binary slice per SSE event
const { zipBuffer, filename, stats } = outcome;
const totalChunks = Math.ceil(zipBuffer.length / CHUNK_SIZE);
sseWrite('status', {
message: `Sende Archiv (${(zipBuffer.length / (1024 * 1024)).toFixed(2)} MB in ${totalChunks} Chunks)...`,
});
for (let i = 0; i < totalChunks; i++) {
if (res.writableEnded || res.destroyed) break;
const slice = zipBuffer.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
const base64 = slice.toString('base64');
res.write(`event: chunk\ndata: ${base64}\n\n`);
if (typeof res.flush === 'function') res.flush();
}
sseWrite('done', {
ok: true,
filename,
size: zipBuffer.length,
totalChunks,
stats,
});
res.end();
} catch (err) {
console.error('[Company Export] Export execution failed:', err);
sseWrite('error', { message: err.message || 'Export fehlgeschlagen' });
res.end();
}
} else {
// Direct binary download
try {
const outcome = await executeCompanyExport({
googleAuthId: req.user.id,
companyId,
});
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${outcome.filename}"`);
res.status(200).send(outcome.zipBuffer);
} catch (err) {
console.error('[Company Export] Export failed:', err);
const status = companyErrorStatus(err);
res.status(status).json({ error: err.message || 'Export fehlgeschlagen' });
}
}
};
app.get(`${basePath}/company/export`, handleCompanyExport);
app.get(`${basePath}/company/export.zip`, handleCompanyExport);
app.post(`${basePath}/company/export`, handleCompanyExport);
// #endregion
/**
* @openapi
* /company/{id}:
* put:
* tags: [Company]
* summary: Update company
* description: Updates a company.
* security:
* - cookieAuth: []
* x-markdown: company/update.md
* x-signals: [company]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [iso639, name, taxrate, reducedtaxrate]
* properties:
* iso639:
* type: string
* name:
* type: string
* taxrate:
* type: number
* reducedtaxrate:
* type: number
* responses:
* 200:
* description: Updated
* content:
* application/json:
* schema:
* type: object
* properties:
* company:
* $ref: '#/components/schemas/Company'
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Company not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* delete:
* tags: [Company]
* summary: Delete company
* description: Deletes a company.
* security:
* - cookieAuth: []
* x-markdown: company/delete.md
* x-signals: [user, company]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 204:
* description: Deleted
* 400:
* description: Invalid id
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region PUT /company/:id
app.put(`${basePath}/company/:id`, async (req, res) => {
const parsedId = parseCompanyId(req.params.id);
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
const body = parseCompanyBody(req.body);
if (body.error) {
return res.status(400).json({ error: body.error });
}
try {
const company = await db.updateCompany(parsedId.companyId, body, req.user.id);
emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } });
res.status(200).json({ company });
} catch (error) {
const status = companyErrorStatus(error);
if (status === 500) console.error('Update company failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
// #region DELETE /company/:id
app.delete(`${basePath}/company/:id`, async (req, res) => {
const parsedId = parseCompanyId(req.params.id);
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
try {
await db.deleteCompany(parsedId.companyId, req.user.id);
clearCompanyPrinters(parsedId.companyId);
res.sendStatus(204);
} catch (error) {
const status = companyErrorStatus(error);
if (status !== 500) {
return res.status(status).json({ error: error.message });
}
console.error('Delete company failed:', error);
res.status(500).json({ error: 'Failed to delete company' });
}
});
// #endregion
}