Files
iistwin/server/documents/routes.ts

506 lines
22 KiB
TypeScript

import { type Express } from "express";
import { authenticateToken, requireAdmin, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { upload } from "../utils/upload";
import { DocumentTemplateService } from "./template.service";
import { AssetService } from "./asset.service";
import { DataResolutionService } from "./data-resolution.service";
import { DocumentGenerationService } from "./generation.service";
import { PackageService } from "./package.service";
import { SignatureService } from "./signature.service";
import { DocxImporterService } from "./docx-importer.service";
import * as storage from "../storage/documents.storage";
import { storage as appStorage } from "../storage/index";
export async function registerDocumentRoutes(app: Express) {
const templateService = new DocumentTemplateService();
const assetService = new AssetService();
const dataResolution = new DataResolutionService();
const generationService = new DocumentGenerationService(templateService, dataResolution, assetService);
const packageService = new PackageService(generationService);
const signatureService = new SignatureService();
const docxImporter = new DocxImporterService(templateService);
// ── Template Categories ────────────────────────────────────────────────────
app.get("/api/documents/templates/categories", authenticateToken, async (req: AuthenticatedRequest, res) => {
const categories = await templateService.listCategories(req.organizationId!);
res.json({ success: true, categories });
});
app.post("/api/documents/templates/categories", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const category = await templateService.createCategory({
...req.body,
organizationId: req.organizationId!,
});
res.status(201).json({ success: true, category });
});
app.put("/api/documents/templates/categories/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const category = await templateService.updateCategory(
Number(req.params.id),
req.organizationId!,
req.body,
);
if (!category) return res.status(404).json({ error: "Категория не найдена" });
res.json({ success: true, category });
});
app.delete("/api/documents/templates/categories/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
await templateService.deleteCategory(Number(req.params.id), req.organizationId!);
res.json({ success: true });
});
// ── Template Folders ───────────────────────────────────────────────────────
app.get("/api/documents/folders", authenticateToken, async (req: AuthenticatedRequest, res) => {
const folders = await templateService.listFolders(req.organizationId!);
res.json({ success: true, folders });
});
app.post("/api/documents/folders", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const folder = await templateService.createFolder({
...req.body,
organizationId: req.organizationId!,
});
res.status(201).json({ success: true, folder });
});
app.put("/api/documents/folders/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const folder = await templateService.updateFolder(
Number(req.params.id),
req.organizationId!,
req.body,
);
if (!folder) return res.status(404).json({ error: "Папка не найдена" });
res.json({ success: true, folder });
});
app.delete("/api/documents/folders/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
try {
await templateService.deleteFolder(Number(req.params.id), req.organizationId!);
res.json({ success: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка удаления папки";
res.status(400).json({ error: message });
}
});
app.patch("/api/documents/folders/reorder", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
try {
await templateService.reorderFolders(req.organizationId!, req.body.updates || []);
res.json({ success: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка изменения порядка папок";
res.status(400).json({ error: message });
}
});
app.get("/api/documents/folders/:id/mappings", authenticateToken, async (req: AuthenticatedRequest, res) => {
const mappings = await templateService.listFolderMappings(Number(req.params.id), req.organizationId!);
res.json({ success: true, mappings });
});
app.post("/api/documents/folders/:id/mappings", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const mapping = await templateService.createFolderMapping({
...req.body,
folderId: Number(req.params.id),
});
res.status(201).json({ success: true, mapping });
});
app.put("/api/documents/folders/:id/mappings/:mappingId", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const mapping = await templateService.updateFolderMapping(
Number(req.params.mappingId),
Number(req.params.id),
req.body,
);
if (!mapping) return res.status(404).json({ error: "Маппинг не найден" });
res.json({ success: true, mapping });
});
app.delete("/api/documents/folders/:id/mappings/:mappingId", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
await templateService.deleteFolderMapping(Number(req.params.mappingId), Number(req.params.id));
res.json({ success: true });
});
// ── Templates ──────────────────────────────────────────────────────────────
app.get("/api/documents/templates", authenticateToken, async (req: AuthenticatedRequest, res) => {
const templates = await templateService.list({
organizationId: req.organizationId!,
status: req.query.status as string | undefined,
categoryId: req.query.categoryId ? Number(req.query.categoryId) : undefined,
folderId: req.query.folderId ? Number(req.query.folderId) : undefined,
formId: req.query.formId ? Number(req.query.formId) : undefined,
});
res.json({ success: true, templates });
});
app.get("/api/documents/templates/:id", authenticateToken, async (req: AuthenticatedRequest, res) => {
const template = await templateService.getById(Number(req.params.id), req.organizationId!);
if (!template) return res.status(404).json({ error: "Шаблон не найден" });
res.json({ success: true, template });
});
app.post("/api/documents/templates", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const template = await templateService.create({
...req.body,
organizationId: req.organizationId!,
createdBy: req.user!.id,
});
res.status(201).json({ success: true, template });
});
app.post("/api/documents/templates/import-docx", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => {
try {
if (!req.file) return res.status(400).json({ error: "Файл не передан" });
const ext = req.file.originalname.split(".").pop()?.toLowerCase();
if (ext !== "docx") {
return res.status(400).json({ error: "Поддерживаются только DOCX-файлы" });
}
const result = await docxImporter.importDocx(req.file, req.organizationId!, req.user!.id, {
name: req.body.name,
description: req.body.description,
categoryId: req.body.categoryId ? Number(req.body.categoryId) : undefined,
formId: req.body.formId ? Number(req.body.formId) : undefined,
});
res.status(201).json({ success: true, template: result.template, variables: result.variables, warnings: result.warnings });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка импорта DOCX";
res.status(400).json({ error: message });
}
});
app.post("/api/documents/templates/:id/replace-docx", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => {
try {
if (!req.file) return res.status(400).json({ error: "Файл не передан" });
const ext = req.file.originalname.split(".").pop()?.toLowerCase();
if (ext !== "docx") {
return res.status(400).json({ error: "Поддерживаются только DOCX-файлы" });
}
const result = await docxImporter.replaceDocx(
req.file,
Number(req.params.id),
req.organizationId!,
req.user!.id,
);
res.json({ success: true, template: result.template, variables: result.variables, warnings: result.warnings });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка замены DOCX";
res.status(400).json({ error: message });
}
});
app.put("/api/documents/templates/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const template = await templateService.update(
Number(req.params.id),
req.organizationId!,
req.body,
);
if (!template) return res.status(404).json({ error: "Шаблон не найден" });
res.json({ success: true, template });
});
app.patch("/api/documents/templates/:id/folder", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const template = await templateService.moveTemplateToFolder(
Number(req.params.id),
req.organizationId!,
req.body.folderId === null ? null : Number(req.body.folderId),
);
if (!template) return res.status(404).json({ error: "Шаблон не найден" });
res.json({ success: true, template });
});
app.delete("/api/documents/templates/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
try {
await templateService.delete(Number(req.params.id), req.organizationId!);
res.json({ success: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка удаления шаблона";
res.status(400).json({ error: message });
}
});
app.post("/api/documents/templates/:id/duplicate", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const template = await templateService.duplicate(
Number(req.params.id),
req.organizationId!,
req.body.name,
);
res.status(201).json({ success: true, template });
});
app.post("/api/documents/templates/:id/auto-variables", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const variables = await templateService.autoCreateVariablesFromContent(
Number(req.params.id),
req.organizationId!,
);
res.json({ success: true, variables });
});
app.get("/api/documents/templates/:id/variables", authenticateToken, async (req: AuthenticatedRequest, res) => {
const variables = await templateService.listVariables(
Number(req.params.id),
req.organizationId!,
);
res.json({ success: true, variables });
});
// ── Assets ─────────────────────────────────────────────────────────────────
app.get("/api/documents/assets", authenticateToken, async (req: AuthenticatedRequest, res) => {
const assets = await assetService.list({
organizationId: req.organizationId!,
type: req.query.type as string | undefined,
isActive: req.query.isActive === 'true' ? true : req.query.isActive === 'false' ? false : undefined,
});
res.json({ success: true, assets });
});
app.post("/api/documents/assets", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => {
try {
const asset = await assetService.create({
...req.body,
organizationId: req.organizationId!,
file: req.file,
});
res.status(201).json({ success: true, asset });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Ошибка создания актива';
res.status(400).json({ error: message });
}
});
app.get("/api/documents/assets/:id", authenticateToken, async (req: AuthenticatedRequest, res) => {
const asset = await assetService.getById(Number(req.params.id), req.organizationId!);
if (!asset) return res.status(404).json({ error: "Актив не найден" });
res.json({ success: true, asset });
});
app.put("/api/documents/assets/:id", authenticateToken, requireAdmin, upload.single("file"), async (req: AuthenticatedRequest, res) => {
const asset = await assetService.update(
Number(req.params.id),
req.organizationId!,
{ ...req.body, file: req.file },
);
if (!asset) return res.status(404).json({ error: "Актив не найден" });
res.json({ success: true, asset });
});
app.delete("/api/documents/assets/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
try {
await assetService.delete(Number(req.params.id), req.organizationId!);
res.json({ success: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка удаления актива";
res.status(400).json({ error: message });
}
});
// ── Generations ────────────────────────────────────────────────────────────
app.post("/api/documents/generations", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const result = await generationService.generate({
templateId: Number(req.body.templateId),
taskId: Number(req.body.taskId),
organizationId: req.organizationId!,
userId: req.user!.id,
outputFormat: req.body.format || 'both',
});
res.json({ success: true, ...result });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Ошибка генерации документа';
res.status(400).json({ error: message });
}
});
app.get("/api/documents/generate-link", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const templateId = Number(req.query.templateId);
const taskId = Number(req.query.taskId);
const format = (req.query.format as string) || 'both';
const returnTo = (req.query.returnTo as string) || '';
if (!templateId || !taskId) {
return res.status(400).json({ error: "templateId и taskId обязательны" });
}
if (!['pdf', 'docx', 'both'].includes(format)) {
return res.status(400).json({ error: "Недопустимый формат" });
}
await generationService.generate({
templateId,
taskId,
organizationId: req.organizationId!,
userId: req.user!.id,
outputFormat: format as any,
});
let redirectUrl = returnTo;
if (!redirectUrl) {
const task = await appStorage.getTask(taskId, req.organizationId!);
redirectUrl = task ? `/forms/${task.formId}/tasks/${taskId}` : '/';
}
res.redirect(redirectUrl);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка генерации документа";
res.status(400).json({ error: message });
}
});
app.post("/api/documents/generations/preview", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const preview = await generationService.preview(
Number(req.body.templateId),
Number(req.body.taskId),
req.organizationId!,
req.user!.id,
);
res.json({ success: true, ...preview });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Ошибка предпросмотра';
res.status(400).json({ error: message });
}
});
app.get("/api/documents/generations", authenticateToken, async (req: AuthenticatedRequest, res) => {
const filters = {
organizationId: req.organizationId!,
templateId: req.query.templateId ? Number(req.query.templateId) : undefined,
taskId: req.query.taskId ? Number(req.query.taskId) : undefined,
status: req.query.status as string | undefined,
limit: req.query.limit ? Number(req.query.limit) : 100,
offset: req.query.offset ? Number(req.query.offset) : 0,
};
const [generations, total] = await Promise.all([
storage.listDocumentGenerations(filters),
storage.countDocumentGenerations(filters),
]);
res.json({ success: true, generations, total });
});
app.get("/api/documents/generations/:id", authenticateToken, async (req: AuthenticatedRequest, res) => {
const generation = await storage.getDocumentGeneration(Number(req.params.id), req.organizationId!);
if (!generation) return res.status(404).json({ error: "Генерация не найдена" });
res.json({ success: true, generation });
});
app.get("/api/documents/generations/:id/download/:format", authenticateToken, async (req: AuthenticatedRequest, res) => {
const generation = await storage.getDocumentGeneration(Number(req.params.id), req.organizationId!);
if (!generation) return res.status(404).json({ error: "Генерация не найдена" });
const format = req.params.format as 'pdf' | 'docx';
const fileKey = format === 'pdf' ? generation.pdfFileKey : generation.docxFileKey;
if (!fileKey) return res.status(404).json({ error: "Файл не найден" });
res.redirect(`/api/files/${fileKey}`);
});
// ── Packages ───────────────────────────────────────────────────────────────
app.get("/api/documents/packages", authenticateToken, async (req: AuthenticatedRequest, res) => {
const packages = await packageService.list(req.organizationId!);
res.json({ success: true, packages });
});
app.get("/api/documents/packages/:id", authenticateToken, async (req: AuthenticatedRequest, res) => {
const pkg = await packageService.getById(Number(req.params.id), req.organizationId!);
if (!pkg) return res.status(404).json({ error: "Пакет не найден" });
res.json({ success: true, package: pkg });
});
app.post("/api/documents/packages", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const pkg = await packageService.create(req.body, req.organizationId!);
res.status(201).json({ success: true, package: pkg });
});
app.put("/api/documents/packages/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
const pkg = await packageService.update(Number(req.params.id), req.organizationId!, req.body);
if (!pkg) return res.status(404).json({ error: "Пакет не найден" });
res.json({ success: true, package: pkg });
});
app.delete("/api/documents/packages/:id", authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res) => {
try {
await packageService.delete(Number(req.params.id), req.organizationId!);
res.json({ success: true });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка удаления пакета";
res.status(400).json({ error: message });
}
});
app.post("/api/documents/packages/:id/generate", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const result = await packageService.generatePackage({
packageId: Number(req.params.id),
taskId: Number(req.body.taskId),
organizationId: req.organizationId!,
userId: req.user!.id,
});
res.json({ success: true, ...result });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка генерации пакета";
res.status(400).json({ error: message });
}
});
// ── Signatures ─────────────────────────────────────────────────────────────
app.get("/api/documents/signatures", authenticateToken, async (req: AuthenticatedRequest, res) => {
const generationId = Number(req.query.generationId);
if (!generationId) return res.status(400).json({ error: "generationId обязателен" });
const signatures = await signatureService.listByGeneration(generationId);
res.json({ success: true, signatures });
});
app.post("/api/documents/signatures", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const signature = await signatureService.createPendingSignature(
Number(req.body.generationId),
req.user!.id,
req.body.comment,
);
res.status(201).json({ success: true, signature });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка создания подписи";
res.status(400).json({ error: message });
}
});
app.post("/api/documents/signatures/:id/sign", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const signature = await signatureService.sign(
Number(req.params.id),
req.user!.id,
req.body.comment,
);
res.json({ success: true, signature });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка подписания";
res.status(400).json({ error: message });
}
});
app.post("/api/documents/signatures/:id/reject", authenticateToken, async (req: AuthenticatedRequest, res) => {
try {
const signature = await signatureService.reject(
Number(req.params.id),
req.user!.id,
req.body.comment,
);
res.json({ success: true, signature });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Ошибка отклонения";
res.status(400).json({ error: message });
}
});
}