documents preview

This commit is contained in:
2026-03-11 09:09:15 +01:00
parent ffdea4fe62
commit 0e4c79b735
13 changed files with 558 additions and 13 deletions

68
server/dist/index.js vendored
View File

@@ -2,13 +2,14 @@ import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { TextEncoder } from 'node:util';
import { promisify, TextEncoder } from 'node:util';
import { DatabaseSync } from 'node:sqlite';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import fastifyStatic from '@fastify/static';
import websocket from '@fastify/websocket';
import dotenv from 'dotenv';
import libreOffice from 'libreoffice-convert';
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
import Fastify from 'fastify';
import { Redis } from 'ioredis';
@@ -47,6 +48,11 @@ const adminDeleteUserParamsSchema = z.object({
const webBundleFileParamsSchema = z.object({
'*': z.string().min(1),
});
const officePreviewSchema = z.object({
fileName: z.string().trim().min(1).max(256),
mimeType: z.string().trim().min(1).max(256),
fileBase64: z.string().min(1).max(96_000_000),
});
const wsQuerySchema = z.object({
token: z.string().min(1),
});
@@ -111,6 +117,7 @@ const webAuthnRpName = process.env.WEBAUTHN_RP_NAME ?? 'PrivateChat';
const webAuthnUserVerification = resolveWebAuthnUserVerification(process.env.WEBAUTHN_USER_VERIFICATION);
const frontendIndexPath = path.join(frontendDistPath, 'index.html');
const hasFrontendBuild = fs.existsSync(frontendIndexPath);
const convertOfficeDocument = promisify(libreOffice.convertWithOptions);
const speechTranscriber = new SpeechTranscriber({
serviceUrl: speechTranscriptionServiceUrl,
language: speechTranscriptionLanguage,
@@ -462,6 +469,35 @@ app.get('/api/auth/session', async (request, reply) => {
messageEncryptionKey: authContext.user.messageEncryptionKey,
};
});
app.post('/api/files/office-preview', { bodyLimit: 64 * 1024 * 1024 }, async (request, reply) => {
const authContext = await authenticateRequest(request, reply);
if (!authContext) {
return;
}
const parsed = officePreviewSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({
message: 'Invalid office preview payload.',
issues: parsed.error.flatten(),
});
}
if (!isSupportedOfficeDocument(parsed.data.fileName, parsed.data.mimeType)) {
return reply.code(400).send({ message: 'Only DOCX, XLSX, and PPTX files can be previewed.' });
}
try {
const pdfBuffer = await convertOfficeDocumentToPdf(parsed.data.fileName, parsed.data.fileBase64);
return {
mimeType: 'application/pdf',
pdfBase64: pdfBuffer.toString('base64'),
};
}
catch (error) {
app.log.warn({ err: error, userId: authContext.user.id }, 'Office preview generation failed');
return reply.code(422).send({
message: describeOfficePreviewFailure(error),
});
}
});
app.get('/api/admin/pending-users', async (request, reply) => {
const authContext = await authenticateRequest(request, reply);
if (!authContext) {
@@ -835,6 +871,36 @@ async function authenticateTokenFromSession(userId, sessionId, decoded) {
},
};
}
async function convertOfficeDocumentToPdf(fileName, fileBase64) {
const inputBuffer = Buffer.from(fileBase64, 'base64');
if (inputBuffer.byteLength === 0) {
throw new Error('The uploaded office document is empty.');
}
const normalizedFileName = normalizeOfficeDocumentFileName(fileName);
return convertOfficeDocument(inputBuffer, '.pdf', undefined, { fileName: normalizedFileName });
}
function isSupportedOfficeDocument(fileName, mimeType) {
const normalizedFileName = fileName.trim().toLowerCase();
const normalizedMimeType = mimeType.trim().toLowerCase();
if (normalizedMimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|| normalizedMimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|| normalizedMimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
return true;
}
return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedFileName);
}
function normalizeOfficeDocumentFileName(fileName) {
return fileName.trim().replace(/\.xslx$/i, '.xlsx');
}
function describeOfficePreviewFailure(error) {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
return 'Office preview generation failed because LibreOffice is not installed on the server.';
}
if (error instanceof Error && error.message.trim()) {
return `Office preview generation failed: ${error.message}`;
}
return 'Office preview generation failed.';
}
function createUser(input) {
const createdAt = new Date().toISOString();
const user = {