CentralBackend/services/CompanyStampService.js
2025-09-07 12:44:01 +02:00

165 lines
6.0 KiB
JavaScript

const CompanyStampRepository = require('../repositories/CompanyStampRepository');
const UnitOfWork = require('../repositories/UnitOfWork');
const { logger } = require('../middleware/logger');
const ALLOWED_MIME = new Set(['image/png', 'image/jpeg', 'image/webp']);
const MAX_IMAGE_BYTES = 500 * 1024; // 500 KB
const MAX_BASE64_LENGTH = Math.ceil((MAX_IMAGE_BYTES / 3) * 4) + 16; // safety
const STAMP_CACHE_TTL_MS = 5 * 60 * 1000;
const _cache = new Map(); // companyId -> { stamp, ts }
const PRIMARY_COMPANY_ID = 1; // Our own company (single stamp policy)
function stripDataUri(b64, providedMime) {
if (!b64) return { pure: '', mime: providedMime };
const m = /^data:([\w/+.-]+);base64,(.+)$/i.exec(b64);
if (m) return { mime: m[1], pure: m[2] };
return { pure: b64, mime: providedMime };
}
function validateBase64(str) {
return /^[A-Za-z0-9+/=\r\n]+$/.test(str);
}
function decodeSizeApprox(base64) {
const cleaned = base64.replace(/[\r\n]/g, '');
const len = cleaned.length;
if (!len) return 0;
const padding = (cleaned.endsWith('==') ? 2 : cleaned.endsWith('=') ? 1 : 0);
return (len * 3) / 4 - padding;
}
class CompanyStampService {
async uploadStamp({ user, base64, mimeType, label, activate = false }) {
if (!user || user.user_type !== 'company')
throw new Error('Only company users can upload stamps');
const companyId = user.id || user.userId;
// Enforce single-stamp rule for primary company BEFORE any mutation
if (companyId === PRIMARY_COMPANY_ID) {
const existingPrimary = await CompanyStampRepository.findByCompanyId(PRIMARY_COMPANY_ID);
if (existingPrimary && existingPrimary.length) {
const err = new Error('Primary company stamp already exists');
err.code = 'PRIMARY_STAMP_EXISTS';
err.existing = existingPrimary[0];
throw err; // controller will translate to preview response
}
}
const { pure, mime } = stripDataUri(base64, mimeType);
const finalMime = mime || mimeType;
if (!ALLOWED_MIME.has(finalMime)) throw new Error('Unsupported MIME type');
if (!validateBase64(pure)) throw new Error('Invalid base64 data');
if (pure.length > MAX_BASE64_LENGTH) throw new Error('Image too large (base64 length)');
const bytes = decodeSizeApprox(pure);
if (bytes > MAX_IMAGE_BYTES) throw new Error('Image exceeds size limit');
const uow = new UnitOfWork();
await uow.start();
try {
// default inactive until optionally activated
const created = await CompanyStampRepository.create(
{
company_id: companyId,
label,
mime_type: finalMime,
image_base64: pure,
is_active: !!activate // treat activate flag directly (single stamp ok)
},
uow.connection
);
if (activate) {
// For primary company single stamp: no need to deactivate; for others keep old logic
if (companyId !== PRIMARY_COMPANY_ID) {
await CompanyStampRepository.deactivateAllForCompany(companyId, uow.connection);
await CompanyStampRepository.activate(created.id, companyId, uow.connection);
created.is_active = true;
}
_cache.delete(companyId);
}
await uow.commit();
logger.info('CompanyStampService.uploadStamp:success', { id: created.id, activate, companyId });
return created;
} catch (e) {
await uow.rollback(e);
throw e;
}
}
async listCompanyStamps(user) {
if (!user || user.user_type !== 'company')
throw new Error('Forbidden');
return CompanyStampRepository.findByCompanyId(user.id || user.userId);
}
async getActiveStampForCompany(companyId) {
const cached = _cache.get(companyId);
const now = Date.now();
if (cached && now - cached.ts < STAMP_CACHE_TTL_MS) return cached.stamp;
const stamp = await CompanyStampRepository.findActiveByCompanyId(companyId);
_cache.set(companyId, { stamp, ts: now });
return stamp;
}
async getMyActive(user) {
if (!user || user.user_type !== 'company')
throw new Error('Forbidden');
return this.getActiveStampForCompany(user.id || user.userId);
}
async activateStamp(id, user) {
if (!user || user.user_type !== 'company')
throw new Error('Forbidden');
const companyId = user.id || user.userId;
const uow = new UnitOfWork();
await uow.start();
try {
const stamp = await CompanyStampRepository.findById(id, uow.connection);
if (!stamp || stamp.company_id !== companyId) throw new Error('Not found');
await CompanyStampRepository.deactivateAllForCompany(companyId, uow.connection);
await CompanyStampRepository.activate(id, companyId, uow.connection);
await uow.commit();
_cache.delete(companyId);
return { ...stamp, is_active: true };
} catch (e) {
await uow.rollback(e);
throw e;
}
}
async deleteStamp(id, user) {
if (!user || user.user_type !== 'company')
throw new Error('Forbidden');
const ok = await CompanyStampRepository.delete(id, user.id || user.userId);
if (ok) _cache.delete(user.id || user.userId);
return ok;
}
async getProfitPlanetStamp() {
// Tries cache (reuse normal cache key)
const cached = _cache.get(PRIMARY_COMPANY_ID);
const now = Date.now();
if (cached && now - cached.ts < STAMP_CACHE_TTL_MS) return cached.stamp;
// Accept active first; if none (single stamp not active), just any
let stamp = await CompanyStampRepository.findActiveByCompanyId(PRIMARY_COMPANY_ID);
if (!stamp) {
const all = await CompanyStampRepository.findByCompanyId(PRIMARY_COMPANY_ID);
stamp = all && all[0] ? all[0] : null;
}
_cache.set(PRIMARY_COMPANY_ID, { stamp, ts: now });
return stamp;
}
// Convenience for placeholder
async getProfitPlanetSignatureTag(sizes = { maxW: 300, maxH: 300 }) {
const stamp = await this.getProfitPlanetStamp();
if (!stamp) return '';
return `<img src="data:${stamp.mime_type};base64,${stamp.image_base64}" style="max-width:${sizes.maxW}px;max-height:${sizes.maxH}px;">`;
}
}
module.exports = new CompanyStampService();