feat: add admin endpoints for listing and moving user contract documents
This commit is contained in:
parent
5764ce5cdc
commit
bf8f94b848
@ -1,6 +1,6 @@
|
||||
const DocumentTemplateService = require('../../services/template/DocumentTemplateService');
|
||||
const ContractUploadService = require('../../services/contracts/ContractUploadService');
|
||||
const { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } = require('@aws-sdk/client-s3');
|
||||
const { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||
const PDFDocument = require('pdfkit');
|
||||
const stream = require('stream');
|
||||
@ -253,6 +253,48 @@ function streamToString(s3BodyStream, templateId) {
|
||||
});
|
||||
}
|
||||
|
||||
async function listAllKeys(s3Client, prefix) {
|
||||
const keys = [];
|
||||
let token = undefined;
|
||||
do {
|
||||
const res = await s3Client.send(new ListObjectsV2Command({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Prefix: prefix,
|
||||
ContinuationToken: token
|
||||
}));
|
||||
const batch = (res && res.Contents ? res.Contents : [])
|
||||
.map(item => item && item.Key)
|
||||
.filter(Boolean);
|
||||
keys.push(...batch);
|
||||
token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
|
||||
} while (token);
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getTimestamp() {
|
||||
const d = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
function appendTimestampToKey(key) {
|
||||
const ext = path.posix.extname(key);
|
||||
const base = ext ? key.slice(0, -ext.length) : key;
|
||||
return `${base}-${getTimestamp()}${ext}`;
|
||||
}
|
||||
|
||||
async function objectExists(s3Client, key) {
|
||||
try {
|
||||
const { HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||
await s3Client.send(new HeadObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key }));
|
||||
return true;
|
||||
} catch (e) {
|
||||
const status = e && e.$metadata && e.$metadata.httpStatusCode;
|
||||
if (status === 404) return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function formatS3Error(err) {
|
||||
if (!err) return null;
|
||||
if (typeof err === 'string') return { message: err };
|
||||
@ -1423,6 +1465,9 @@ exports.previewLatestForUser = async (req, res) => {
|
||||
const allowedContractTypes = ['contract', 'gdpr'];
|
||||
const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';
|
||||
|
||||
const documentId = parseInt((req.query.documentId || req.query.document_id || '').toString(), 10);
|
||||
const objectKeyParam = (req.query.objectKey || req.query.object_key || '').toString();
|
||||
|
||||
let folderStructureWarning = null;
|
||||
|
||||
logger.info('[previewLatestForUser] start', { targetUserId, contractType, requestId: req.id });
|
||||
@ -1492,40 +1537,75 @@ exports.previewLatestForUser = async (req, res) => {
|
||||
return { ...payload, warning: folderStructureWarning };
|
||||
};
|
||||
|
||||
// Choose document_type set based on contractType (aligned with ContractUploadService paths)
|
||||
// uploadContract stores under contracts/<category>/<userId>/<contract_type> with document_type 'contract'
|
||||
// so use contract_type column to disambiguate between contract vs gdpr
|
||||
const docTypesMap = {
|
||||
contract: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
|
||||
gdpr: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf']
|
||||
};
|
||||
const docTypes = docTypesMap[contractType] || docTypesMap.contract;
|
||||
const placeholders = docTypes.map(() => '?').join(',');
|
||||
|
||||
// Fetch latest uploaded document for this type from user_documents
|
||||
// Fetch a specific document by id (if provided), by objectKey, otherwise latest by contractType
|
||||
let doc = null;
|
||||
try {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT object_storage_id
|
||||
FROM user_documents
|
||||
WHERE user_id = ?
|
||||
AND document_type IN (${placeholders})
|
||||
AND object_storage_id IS NOT NULL
|
||||
AND (contract_type = ? OR (contract_type IS NULL AND ? = 'contract'))
|
||||
ORDER BY upload_at DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[targetUserId, ...docTypes, contractType, contractType]
|
||||
);
|
||||
const arr = Array.isArray(rows) ? rows : (rows ? [rows] : []);
|
||||
doc = arr[0] || null;
|
||||
logger.info('[previewLatestForUser] user_documents lookup', { targetUserId, contractType, count: arr.length });
|
||||
} catch (e) {
|
||||
logger.warn('[previewLatestForUser] user_documents lookup failed', e && (e.stack || e.message));
|
||||
let resolvedContractType = contractType;
|
||||
if (Number.isFinite(documentId) && documentId > 0) {
|
||||
try {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT id, object_storage_id, contract_type
|
||||
FROM user_documents
|
||||
WHERE id = ? AND user_id = ?
|
||||
LIMIT 1`,
|
||||
[documentId, targetUserId]
|
||||
);
|
||||
const arr = Array.isArray(rows) ? rows : (rows ? [rows] : []);
|
||||
doc = arr[0] || null;
|
||||
if (doc && doc.contract_type && allowedContractTypes.includes(String(doc.contract_type).toLowerCase())) {
|
||||
resolvedContractType = String(doc.contract_type).toLowerCase();
|
||||
} else if (doc && doc.object_storage_id && /\/gdpr\//i.test(doc.object_storage_id)) {
|
||||
resolvedContractType = 'gdpr';
|
||||
}
|
||||
logger.info('[previewLatestForUser] user_documents lookup by documentId', { targetUserId, documentId, resolvedContractType, found: !!doc });
|
||||
} catch (e) {
|
||||
logger.warn('[previewLatestForUser] user_documents lookup by documentId failed', e && (e.stack || e.message));
|
||||
}
|
||||
} else if (objectKeyParam) {
|
||||
const key = objectKeyParam;
|
||||
const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
|
||||
if (!key.startsWith(basePrefix)) {
|
||||
return res.status(400).json({ error: 'Invalid object key' });
|
||||
}
|
||||
if (key.includes('/gdpr/')) resolvedContractType = 'gdpr';
|
||||
else if (key.includes('/contract/')) resolvedContractType = 'contract';
|
||||
doc = { object_storage_id: key };
|
||||
logger.info('[previewLatestForUser] using objectKey directly', { targetUserId, resolvedContractType, key });
|
||||
} else {
|
||||
// Choose document_type set based on contractType (aligned with ContractUploadService paths)
|
||||
// uploadContract stores under contracts/<category>/<userId>/<contract_type> with document_type 'contract'
|
||||
// so use contract_type column to disambiguate between contract vs gdpr
|
||||
const docTypesMap = {
|
||||
contract: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
|
||||
gdpr: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf']
|
||||
};
|
||||
const docTypes = docTypesMap[contractType] || docTypesMap.contract;
|
||||
const placeholders = docTypes.map(() => '?').join(',');
|
||||
|
||||
try {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT object_storage_id
|
||||
FROM user_documents
|
||||
WHERE user_id = ?
|
||||
AND document_type IN (${placeholders})
|
||||
AND object_storage_id IS NOT NULL
|
||||
AND (contract_type = ? OR (contract_type IS NULL AND ? = 'contract'))
|
||||
ORDER BY upload_at DESC, id DESC
|
||||
LIMIT 1`,
|
||||
[targetUserId, ...docTypes, contractType, contractType]
|
||||
);
|
||||
const arr = Array.isArray(rows) ? rows : (rows ? [rows] : []);
|
||||
doc = arr[0] || null;
|
||||
logger.info('[previewLatestForUser] user_documents lookup', { targetUserId, contractType, count: arr.length });
|
||||
} catch (e) {
|
||||
logger.warn('[previewLatestForUser] user_documents lookup failed', e && (e.stack || e.message));
|
||||
}
|
||||
}
|
||||
|
||||
const activeContractType = resolvedContractType || contractType;
|
||||
|
||||
if (!doc || !doc.object_storage_id) {
|
||||
if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
|
||||
return res.status(404).json(jsonWithWarning({ message: `No uploaded ${contractType.toUpperCase()} file found for this user` }));
|
||||
return res.status(404).json(jsonWithWarning({ message: `No uploaded ${activeContractType.toUpperCase()} file found for this user` }));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -1543,7 +1623,7 @@ exports.previewLatestForUser = async (req, res) => {
|
||||
bucket: process.env.EXOSCALE_BUCKET,
|
||||
key: doc.object_storage_id,
|
||||
userId: targetUserId,
|
||||
contractType
|
||||
contractType: activeContractType
|
||||
});
|
||||
const cmd = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: doc.object_storage_id });
|
||||
let fileObj;
|
||||
@ -1569,14 +1649,14 @@ exports.previewLatestForUser = async (req, res) => {
|
||||
}
|
||||
const pdfBuffer = await s3BodyToBuffer(fileObj.Body);
|
||||
if (!pdfBuffer || !pdfBuffer.length) {
|
||||
logger.warn('[previewLatestForUser] S3 returned empty Body', { key: doc.object_storage_id, userId: targetUserId, contractType });
|
||||
logger.warn('[previewLatestForUser] S3 returned empty Body', { key: doc.object_storage_id, userId: targetUserId, contractType: activeContractType });
|
||||
if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
|
||||
return res.status(404).json(jsonWithWarning({ message: `${contractType.toUpperCase()} file not available` }));
|
||||
return res.status(404).json(jsonWithWarning({ message: `${activeContractType.toUpperCase()} file not available` }));
|
||||
}
|
||||
|
||||
const b64 = pdfBuffer.toString('base64');
|
||||
const html = ensureHtmlDocument(`
|
||||
<html><head><meta charset="utf-8" /><title>${contractType.toUpperCase()} Preview</title></head>
|
||||
<html><head><meta charset="utf-8" /><title>${resolvedContractType.toUpperCase()} Preview</title></head>
|
||||
<body style="margin:0;padding:0;height:100vh;background:#0f172a;">
|
||||
<embed src="data:application/pdf;base64,${b64}" type="application/pdf" style="width:100%;height:100%;border:none;" />
|
||||
</body></html>
|
||||
@ -1586,14 +1666,14 @@ exports.previewLatestForUser = async (req, res) => {
|
||||
return res.send(html);
|
||||
} catch (e) {
|
||||
if (e && (e.name === 'NoSuchKey' || (e.$metadata && e.$metadata.httpStatusCode === 404))) {
|
||||
logger.warn('[previewLatestForUser] object missing in storage', { key: doc.object_storage_id, userId: targetUserId, contractType });
|
||||
logger.warn('[previewLatestForUser] object missing in storage', { key: doc.object_storage_id, userId: targetUserId, contractType: activeContractType });
|
||||
if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
|
||||
return res.status(404).json(jsonWithWarning({ message: `${contractType.toUpperCase()} file not available` }));
|
||||
return res.status(404).json(jsonWithWarning({ message: `${activeContractType.toUpperCase()} file not available` }));
|
||||
}
|
||||
logger.error('[previewLatestForUser] S3 fetch failed', {
|
||||
key: doc.object_storage_id,
|
||||
userId: targetUserId,
|
||||
contractType,
|
||||
contractType: activeContractType,
|
||||
error: formatS3Error(e)
|
||||
});
|
||||
if (folderStructureWarning) res.setHeader('X-Contract-Preview-Warning', folderStructureWarning);
|
||||
@ -1605,6 +1685,227 @@ exports.previewLatestForUser = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Admin-only endpoint to list contract files from object storage for a user
|
||||
// GET /api/admin/contracts/:id/files?userType=personal|company
|
||||
exports.listUserContractFiles = async (req, res) => {
|
||||
const targetUserId = parseInt(req.params.id, 10);
|
||||
if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Forbidden: Admins only' });
|
||||
}
|
||||
if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
|
||||
return res.status(400).json({ error: 'Invalid user id' });
|
||||
}
|
||||
|
||||
try {
|
||||
const [uRows] = await db.execute('SELECT id, user_type FROM users WHERE id = ? LIMIT 1', [targetUserId]);
|
||||
const userRow = (uRows && uRows[0]) ? uRows[0] : null;
|
||||
const userType = (userRow && userRow.user_type) ? String(userRow.user_type).toLowerCase() : null;
|
||||
const contractCategory = userType === 'company' ? 'company' : 'personal';
|
||||
|
||||
const s3Client = sharedExoscaleClient || new S3Client({
|
||||
region: process.env.EXOSCALE_REGION,
|
||||
endpoint: process.env.EXOSCALE_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
||||
secretAccessKey: process.env.EXOSCALE_SECRET_KEY
|
||||
}
|
||||
});
|
||||
|
||||
const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
|
||||
const contractPrefix = `${basePrefix}contract/`;
|
||||
const gdprPrefix = `${basePrefix}gdpr/`;
|
||||
|
||||
const [contractKeysRaw, gdprKeysRaw, docRows] = await Promise.all([
|
||||
listAllKeys(s3Client, contractPrefix),
|
||||
listAllKeys(s3Client, gdprPrefix),
|
||||
db.execute(
|
||||
`SELECT id, object_storage_id, contract_type
|
||||
FROM user_documents
|
||||
WHERE user_id = ? AND document_type = 'contract'`,
|
||||
[targetUserId]
|
||||
)
|
||||
]);
|
||||
const normalizeKeys = (keys) => (Array.isArray(keys) ? keys : [])
|
||||
.filter(k => !!k && !k.endsWith('/'))
|
||||
.filter(k => /\.pdf$/i.test(String(k)));
|
||||
const contractKeys = normalizeKeys(contractKeysRaw);
|
||||
const gdprKeys = normalizeKeys(gdprKeysRaw);
|
||||
const rows = Array.isArray(docRows && docRows[0]) ? docRows[0] : [];
|
||||
const idMap = new Map(rows.map(r => [String(r.object_storage_id), r]));
|
||||
|
||||
const toItem = (key) => {
|
||||
const doc = idMap.get(String(key));
|
||||
return {
|
||||
key,
|
||||
filename: path.posix.basename(key),
|
||||
documentId: doc ? doc.id : null,
|
||||
contract_type: doc ? doc.contract_type : null
|
||||
};
|
||||
};
|
||||
|
||||
return res.json({
|
||||
userType,
|
||||
contract: contractKeys.map(toItem),
|
||||
gdpr: gdprKeys.map(toItem)
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('[listUserContractFiles] error', err && err.stack ? err.stack : err);
|
||||
return res.status(500).json({ error: 'Failed to list user contract files' });
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Admin-only endpoint to list contract documents for a user
|
||||
// GET /api/admin/contracts/:id/documents?userType=personal|company
|
||||
exports.listUserContractDocuments = async (req, res) => {
|
||||
const targetUserId = parseInt(req.params.id, 10);
|
||||
if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Forbidden: Admins only' });
|
||||
}
|
||||
if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
|
||||
return res.status(400).json({ error: 'Invalid user id' });
|
||||
}
|
||||
|
||||
try {
|
||||
let userRow = null;
|
||||
try {
|
||||
const [uRows] = await db.execute('SELECT id, email, user_type FROM users WHERE id = ? LIMIT 1', [targetUserId]);
|
||||
userRow = (uRows && uRows[0]) ? uRows[0] : null;
|
||||
} catch (e) {
|
||||
logger.warn('[listUserContractDocuments] failed to load users row', e && e.message);
|
||||
}
|
||||
|
||||
const [rows] = await db.execute(
|
||||
`SELECT id, user_id, document_type, contract_type, object_storage_id, original_filename, file_size, mime_type, upload_at
|
||||
FROM user_documents
|
||||
WHERE user_id = ? AND document_type = 'contract'
|
||||
ORDER BY upload_at DESC, id DESC`,
|
||||
[targetUserId]
|
||||
);
|
||||
|
||||
const docs = (Array.isArray(rows) ? rows : []).map((d) => {
|
||||
const key = d.object_storage_id || '';
|
||||
let folderType = 'loose';
|
||||
if (/\/gdpr\//i.test(key)) folderType = 'gdpr';
|
||||
else if (/\/contract\//i.test(key)) folderType = 'contract';
|
||||
return {
|
||||
...d,
|
||||
folderType
|
||||
};
|
||||
});
|
||||
|
||||
return res.json({
|
||||
userType: userRow ? userRow.user_type : null,
|
||||
documents: docs
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('[listUserContractDocuments] error', err && err.stack ? err.stack : err);
|
||||
return res.status(500).json({ error: 'Failed to list user contract documents' });
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Admin-only endpoint to move a contract document between /contract and /gdpr
|
||||
// POST /api/admin/contracts/:id/move { documentId, targetType }
|
||||
exports.moveUserContractDocument = async (req, res) => {
|
||||
const targetUserId = parseInt(req.params.id, 10);
|
||||
const { documentId, targetType, objectKey } = req.body || {};
|
||||
const allowedTypes = ['contract', 'gdpr'];
|
||||
const target = (targetType || '').toString().toLowerCase();
|
||||
|
||||
if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Forbidden: Admins only' });
|
||||
}
|
||||
if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
|
||||
return res.status(400).json({ error: 'Invalid user id' });
|
||||
}
|
||||
if ((!Number.isFinite(documentId) || documentId <= 0) && !objectKey) {
|
||||
return res.status(400).json({ error: 'Invalid document id' });
|
||||
}
|
||||
if (!allowedTypes.includes(target)) {
|
||||
return res.status(400).json({ error: 'Invalid target type' });
|
||||
}
|
||||
|
||||
try {
|
||||
const [uRows] = await db.execute('SELECT id, user_type FROM users WHERE id = ? LIMIT 1', [targetUserId]);
|
||||
const userRow = (uRows && uRows[0]) ? uRows[0] : null;
|
||||
const userType = (userRow && userRow.user_type) ? String(userRow.user_type).toLowerCase() : null;
|
||||
const contractCategory = userType === 'company' ? 'company' : 'personal';
|
||||
|
||||
let currentKey = null;
|
||||
if (objectKey) {
|
||||
const key = String(objectKey);
|
||||
const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
|
||||
if (!key.startsWith(basePrefix) || (!key.includes('/contract/') && !key.includes('/gdpr/'))) {
|
||||
return res.status(400).json({ error: 'Invalid object key' });
|
||||
}
|
||||
currentKey = key;
|
||||
} else {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT id, object_storage_id, contract_type
|
||||
FROM user_documents
|
||||
WHERE id = ? AND user_id = ?
|
||||
LIMIT 1`,
|
||||
[documentId, targetUserId]
|
||||
);
|
||||
const doc = (rows && rows[0]) ? rows[0] : null;
|
||||
if (!doc || !doc.object_storage_id) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
currentKey = String(doc.object_storage_id);
|
||||
}
|
||||
const fileName = path.posix.basename(currentKey);
|
||||
const basePrefix = `contracts/${contractCategory}/${targetUserId}/`;
|
||||
let destKey = `${basePrefix}${target}/${fileName}`;
|
||||
|
||||
const s3Client = sharedExoscaleClient || new S3Client({
|
||||
region: process.env.EXOSCALE_REGION,
|
||||
endpoint: process.env.EXOSCALE_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
||||
secretAccessKey: process.env.EXOSCALE_SECRET_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (currentKey !== destKey) {
|
||||
try {
|
||||
if (await objectExists(s3Client, destKey)) {
|
||||
destKey = appendTimestampToKey(destKey);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('[moveUserContractDocument] objectExists failed', { destKey, error: formatS3Error(e) });
|
||||
}
|
||||
|
||||
const copySource = `${process.env.EXOSCALE_BUCKET}/${encodeURIComponent(currentKey)}`;
|
||||
await s3Client.send(new CopyObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
CopySource: copySource,
|
||||
Key: destKey
|
||||
}));
|
||||
await s3Client.send(new DeleteObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Key: currentKey
|
||||
}));
|
||||
}
|
||||
|
||||
await db.execute(
|
||||
`UPDATE user_documents SET object_storage_id = ?, contract_type = ? WHERE object_storage_id = ?`,
|
||||
[destKey, target, currentKey]
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
documentId,
|
||||
from: currentKey,
|
||||
to: destKey,
|
||||
contract_type: target
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('[moveUserContractDocument] error', err && err.stack ? err.stack : err);
|
||||
return res.status(500).json({ error: 'Failed to move document' });
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Authenticated user endpoint to preview their own latest active contract with DB-filled placeholders
|
||||
// GET /api/contracts/preview/latest
|
||||
exports.previewLatestForMe = async (req, res) => {
|
||||
|
||||
@ -64,6 +64,10 @@ router.get('/admin/dev/exoscale/loose-files', authMiddleware, adminOnly, DevMana
|
||||
router.get('/admin/dev/exoscale/ghost-directories', authMiddleware, adminOnly, DevManagementController.listGhostDirectories);
|
||||
// Contract preview for admin: latest active by user type
|
||||
router.get('/admin/contracts/:id/preview', authMiddleware, adminOnly, DocumentTemplateController.previewLatestForUser);
|
||||
// Admin: list all contract documents for a user
|
||||
router.get('/admin/contracts/:id/documents', authMiddleware, adminOnly, DocumentTemplateController.listUserContractDocuments);
|
||||
// Admin: list contract files from object storage for a user
|
||||
router.get('/admin/contracts/:id/files', authMiddleware, adminOnly, DocumentTemplateController.listUserContractFiles);
|
||||
|
||||
// permissions.js GETs
|
||||
router.get('/permissions', authMiddleware, PermissionController.list);
|
||||
|
||||
@ -168,6 +168,9 @@ router.post('/admin/dev/sql', authMiddleware, adminOnly, upload.single('file'),
|
||||
router.post('/admin/dev/exoscale/create-folder-structure', authMiddleware, adminOnly, DevManagementController.createFolderStructure);
|
||||
router.post('/admin/dev/exoscale/move-loose-files', authMiddleware, adminOnly, DevManagementController.moveLooseFilesToContract);
|
||||
|
||||
// NEW: Admin move contract documents between /contract and /gdpr
|
||||
router.post('/admin/contracts/:id/move', authMiddleware, adminOnly, DocumentTemplateController.moveUserContractDocument);
|
||||
|
||||
// Abonement POSTs
|
||||
router.post('/abonements/subscribe', authMiddleware, AbonemmentController.subscribe);
|
||||
router.post('/abonements/:id/pause', authMiddleware, AbonemmentController.pause);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user