feat: ONLYOFFICE inline editor integration with JWT auth and docker-compose
This commit is contained in:
1
.env.docker
Normal file
1
.env.docker
Normal file
@@ -0,0 +1 @@
|
|||||||
|
JWT_SECRET=cvapp_onlyoffice_jwt_secret_2026
|
||||||
97
docker-compose.yml
Normal file
97
docker-compose.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# ============================================================
|
||||||
|
# CV Application - Docker Compose
|
||||||
|
#
|
||||||
|
# Containers:
|
||||||
|
# cv-backend - FastAPI + Node Carbone service (existing code, containerized)
|
||||||
|
# cv-postgres - PostgreSQL database
|
||||||
|
# onlyoffice-server - ONLYOFFICE Document Server (inline .docx editing)
|
||||||
|
#
|
||||||
|
# Networking:
|
||||||
|
# - Internal Docker network: cvnet
|
||||||
|
# - Backend talks to ONLYOFFICE via http://onlyoffice-server/ (internal)
|
||||||
|
# - Browser talks to ONLYOFFICE via http://<host>:8080/ (public)
|
||||||
|
# - JWT tokens secure all document session communication
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
cv-postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: cv-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: cvapp
|
||||||
|
POSTGRES_USER: cvapp
|
||||||
|
POSTGRES_PASSWORD: cvapp2026
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --template=template0"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- cvnet
|
||||||
|
ports:
|
||||||
|
- "5433:5432" # Different host port to avoid conflict with local PG
|
||||||
|
|
||||||
|
# ONLYOFFICE Document Server
|
||||||
|
onlyoffice-server:
|
||||||
|
image: onlyoffice/documentserver:latest
|
||||||
|
container_name: onlyoffice-server
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# JWT Security - shared secret with backend
|
||||||
|
JWT_ENABLED: "true"
|
||||||
|
JWT_SECRET: "${JWT_SECRET:-cvapp_onlyoffice_jwt_secret_2026}"
|
||||||
|
JWT_HEADER: "Authorization"
|
||||||
|
JWT_IN_BODY: "true"
|
||||||
|
# Disable example apps (not needed in production)
|
||||||
|
ONLYOFFICE_EXAMPLE: "false"
|
||||||
|
volumes:
|
||||||
|
- onlyoffice_data:/var/www/onlyoffice/Data
|
||||||
|
- onlyoffice_logs:/var/log/onlyoffice
|
||||||
|
- onlyoffice_cache:/var/lib/onlyoffice
|
||||||
|
networks:
|
||||||
|
- cvnet
|
||||||
|
ports:
|
||||||
|
- "8080:80" # Public URL for browser to load ONLYOFFICE JS API
|
||||||
|
depends_on:
|
||||||
|
- cv-postgres
|
||||||
|
|
||||||
|
# CV Application Backend (FastAPI + Carbone Express)
|
||||||
|
# We keep this as the existing systemd service for now, but this config
|
||||||
|
# shows how to containerize it. The backend talks to ONLYOFFICE internally.
|
||||||
|
# cv-backend:
|
||||||
|
# build: .
|
||||||
|
# container_name: cv-backend
|
||||||
|
# restart: unless-stopped
|
||||||
|
# environment:
|
||||||
|
# DB_HOST: cv-postgres
|
||||||
|
# DB_PORT: 5432
|
||||||
|
# DB_NAME: cvapp
|
||||||
|
# DB_USER: cvapp
|
||||||
|
# DB_PASSWORD: cvapp2026
|
||||||
|
# OLLAMA_API_KEY: "${OLLAMA_API_KEY}"
|
||||||
|
# JWT_SECRET: "${JWT_SECRET:-cvapp_onlyoffice_jwt_secret_2026}"
|
||||||
|
# ONLYOFFICE_INTERNAL_URL: "http://onlyoffice-server/"
|
||||||
|
# ONLYOFFICE_PUBLIC_URL: "http://localhost:8080/"
|
||||||
|
# volumes:
|
||||||
|
# - ./uploads:/app/uploads
|
||||||
|
# - ./renderer/carbone-templates:/app/carbone-templates
|
||||||
|
# networks:
|
||||||
|
# - cvnet
|
||||||
|
# ports:
|
||||||
|
# - "8770:8770" # FastAPI
|
||||||
|
# - "8771:8771" # Carbone Express
|
||||||
|
# depends_on:
|
||||||
|
# - cv-postgres
|
||||||
|
# - onlyoffice-server
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
onlyoffice_data:
|
||||||
|
onlyoffice_logs:
|
||||||
|
onlyoffice_cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cvnet:
|
||||||
|
driver: bridge
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Carbone Express Server - Standalone microservice
|
* Carbone Express Server - Standalone microservice
|
||||||
* Runs on port 8771 alongside the FastAPI app on 8770
|
* Runs on port 8771 alongside the FastAPI app on 8770
|
||||||
|
* Also serves ONLYOFFICE callback endpoints
|
||||||
*/
|
*/
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { router } = require('./carbone-service');
|
const { router } = require('./carbone-service');
|
||||||
|
const onlyofficeRouter = require('./onlyoffice-service');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 8771;
|
const PORT = 8771;
|
||||||
@@ -16,9 +18,12 @@ app.use(express.json({ limit: '50mb' }));
|
|||||||
// Mount Carbone routes
|
// Mount Carbone routes
|
||||||
app.use('/api/carbone', router);
|
app.use('/api/carbone', router);
|
||||||
|
|
||||||
|
// Mount ONLYOFFICE routes
|
||||||
|
app.use('/api/onlyoffice', onlyofficeRouter);
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/health', (req, res) => res.json({ status: 'ok', service: 'carbone' }));
|
app.get('/health', (req, res) => res.json({ status: 'ok', service: 'carbone+onlyoffice' }));
|
||||||
|
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`Carbone service running on http://0.0.0.0:${PORT}`);
|
console.log(`Carbone + ONLYOFFICE service running on http://0.0.0.0:${PORT}`);
|
||||||
});
|
});
|
||||||
332
renderer/onlyoffice-service.js
Normal file
332
renderer/onlyoffice-service.js
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
/**
|
||||||
|
* ONLYOFFICE Integration Service
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Serving .docx templates to ONLYOFFICE for inline editing
|
||||||
|
* - Receiving ONLYOFFICE save callbacks (status=2 -> download updated file)
|
||||||
|
* - Generating JWT-signed editor config for the browser
|
||||||
|
* - Managing the "Edit in ONLYOFFICE" session lifecycle
|
||||||
|
*
|
||||||
|
* Docker Networking:
|
||||||
|
* - Public URL (browser -> ONLYOFFICE): http://<host>:8080/
|
||||||
|
* - Internal URL (backend -> ONLYOFFICE): http://onlyoffice-server/
|
||||||
|
* - The backend serves the .docx file at a publicly-accessible URL so
|
||||||
|
* ONLYOFFICE can fetch it, and receives callbacks at a publicly-accessible URL.
|
||||||
|
*
|
||||||
|
* JWT Security:
|
||||||
|
* - The JWT_SECRET env var is shared between this backend and the ONLYOFFICE container.
|
||||||
|
* - ALL communication (editor config, callbacks) is signed with this secret.
|
||||||
|
* - ONLYOFFICE validates the JWT on incoming requests and signs outgoing callbacks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const axios = require('axios');
|
||||||
|
const multer = require('multer');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CONFIGURATION
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ONLYOFFICE URLs - these are different for browser vs backend:
|
||||||
|
*
|
||||||
|
* ONLYOFFICE_PUBLIC_URL: The URL the user's BROWSER uses to load the ONLYOFFICE
|
||||||
|
* JavaScript API (e.g., http://100.91.25.139:8080/ or https://docs.example.com/)
|
||||||
|
* This is set via env var and must be accessible from the user's browser.
|
||||||
|
*
|
||||||
|
* ONLYOFFICE_INTERNAL_URL: The URL our BACKEND uses to talk to ONLYOFFICE
|
||||||
|
* over the Docker network (e.g., http://onlyoffice-server/).
|
||||||
|
* In dev mode (no Docker), this is the same as the public URL.
|
||||||
|
*/
|
||||||
|
const ONLYOFFICE_PUBLIC_URL = process.env.ONLYOFFICE_PUBLIC_URL || 'http://localhost:8080';
|
||||||
|
const ONLYOFFICE_INTERNAL_URL = process.env.ONLYOFFICE_INTERNAL_URL || ONLYOFFICE_PUBLIC_URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BACKEND_PUBLIC_URL: The URL that ONLYOFFICE uses to fetch the .docx template
|
||||||
|
* and send callbacks back to. This must be accessible from the ONLYOFFICE
|
||||||
|
* container's network perspective.
|
||||||
|
* - In Docker: http://cv-backend:8771 (internal Docker DNS)
|
||||||
|
* - In dev: http://<host>:8771 (same machine)
|
||||||
|
*/
|
||||||
|
const BACKEND_PUBLIC_URL = process.env.BACKEND_PUBLIC_URL || 'http://localhost:8771';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT_SECRET: Shared secret between this backend and ONLYOFFICE.
|
||||||
|
* Must be identical in both containers for JWT validation to work.
|
||||||
|
*/
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'cvapp_onlyoffice_jwt_secret_2026';
|
||||||
|
|
||||||
|
// Template storage (shared with carbone-service)
|
||||||
|
const TEMPLATES_DIR = path.join(__dirname, 'carbone-templates');
|
||||||
|
fs.mkdirSync(TEMPLATES_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Load registry (shared with carbone-service)
|
||||||
|
const REGISTRY_PATH = path.join(TEMPLATES_DIR, 'registry.json');
|
||||||
|
function loadRegistry() {
|
||||||
|
try { return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8')); }
|
||||||
|
catch { return []; }
|
||||||
|
}
|
||||||
|
function saveRegistry(registry) {
|
||||||
|
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// JWT TOKEN HELPERS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a payload with the JWT secret.
|
||||||
|
* ONLYOFFICE expects the token in the Authorization header as "Bearer <token>".
|
||||||
|
*/
|
||||||
|
function signToken(payload) {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a JWT token from ONLYOFFICE callbacks.
|
||||||
|
* ONLYOFFICE sends the token in the Authorization header.
|
||||||
|
*/
|
||||||
|
function verifyToken(token) {
|
||||||
|
try {
|
||||||
|
// Remove "Bearer " prefix if present
|
||||||
|
if (token && token.startsWith('Bearer ')) {
|
||||||
|
token = token.substring(7);
|
||||||
|
}
|
||||||
|
return jwt.verify(token, JWT_SECRET);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique document key.
|
||||||
|
* ONLYOFFICE uses this to identify document sessions and for caching.
|
||||||
|
* Must be unique per document + version (so re-edits get fresh sessions).
|
||||||
|
*/
|
||||||
|
function generateDocKey(templateId) {
|
||||||
|
return `cvtemplate_${templateId}_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ENDPOINT: GET /api/onlyoffice/:templateId/config
|
||||||
|
// Returns the JWT-signed editor config for the browser to initialize
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.get('/:templateId/config', (req, res) => {
|
||||||
|
const templateId = req.params.templateId;
|
||||||
|
const registry = loadRegistry();
|
||||||
|
const template = registry.find(t => t.id === templateId);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ error: 'Template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(template.path)) {
|
||||||
|
return res.status(404).json({ error: 'Template file missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const docKey = generateDocKey(templateId);
|
||||||
|
const fileName = template.originalName || template.name + '.docx';
|
||||||
|
|
||||||
|
// Build the editor config
|
||||||
|
// The document URL must be accessible from ONLYOFFICE's perspective.
|
||||||
|
// In Docker: http://cv-backend:8771/api/onlyoffice/<id>/download
|
||||||
|
// In dev: http://<host>:8771/api/onlyoffice/<id>/download
|
||||||
|
const documentUrl = `${BACKEND_PUBLIC_URL}/api/onlyoffice/${templateId}/download`;
|
||||||
|
const callbackUrl = `${BACKEND_PUBLIC_URL}/api/onlyoffice/${templateId}/callback`;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
document: {
|
||||||
|
fileType: 'docx',
|
||||||
|
key: docKey,
|
||||||
|
title: fileName,
|
||||||
|
url: documentUrl,
|
||||||
|
permissions: {
|
||||||
|
edit: true,
|
||||||
|
download: true,
|
||||||
|
review: true,
|
||||||
|
print: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editorConfig: {
|
||||||
|
mode: 'edit',
|
||||||
|
lang: 'en',
|
||||||
|
callbackUrl: callbackUrl,
|
||||||
|
user: {
|
||||||
|
id: 'cv-app-user',
|
||||||
|
name: 'CV App Editor',
|
||||||
|
},
|
||||||
|
customization: {
|
||||||
|
autosave: true,
|
||||||
|
forcesave: true,
|
||||||
|
compactHeader: false,
|
||||||
|
toolbarNoTabs: false,
|
||||||
|
// Hide features not relevant to CV templates
|
||||||
|
hideRightMenu: true,
|
||||||
|
hideRulers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// The type of document editor to load
|
||||||
|
documentType: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign the entire config with JWT
|
||||||
|
// ONLYOFFICE validates this token when the editor loads
|
||||||
|
const token = signToken(config);
|
||||||
|
config.token = token;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
config: config,
|
||||||
|
onlyofficeUrl: ONLYOFFICE_PUBLIC_URL,
|
||||||
|
templateId: templateId,
|
||||||
|
templateName: template.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ENDPOINT: GET /api/onlyoffice/:templateId/download
|
||||||
|
// ONLYOFFICE fetches the .docx file from this URL
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.get('/:templateId/download', (req, res) => {
|
||||||
|
const templateId = req.params.templateId;
|
||||||
|
const registry = loadRegistry();
|
||||||
|
const template = registry.find(t => t.id === templateId);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ error: 'Template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(template.path)) {
|
||||||
|
return res.status(404).json({ error: 'Template file missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = template.originalName || template.name + '.docx';
|
||||||
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||||
|
|
||||||
|
const fileStream = fs.createReadStream(template.path);
|
||||||
|
fileStream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ENDPOINT: POST /api/onlyoffice/:templateId/callback
|
||||||
|
//
|
||||||
|
// ONLYOFFICE calls this endpoint when the document state changes.
|
||||||
|
// Status codes:
|
||||||
|
// 0 = Document being edited (no action needed)
|
||||||
|
// 1 = Document ready for saving (prepare to receive)
|
||||||
|
// 2 = Document saved, URL contains the updated file (DOWNLOAD AND OVERWRITE)
|
||||||
|
// 3 = Document save error
|
||||||
|
// 4 = Document closed with no changes (no action needed)
|
||||||
|
// 6 = Document is being edited but force-save was requested
|
||||||
|
// 7 = Force-save error
|
||||||
|
//
|
||||||
|
// When status === 2 or 6, the payload contains a "url" field with a
|
||||||
|
// direct download link to the updated .docx file. We fetch it and
|
||||||
|
// overwrite the stored template.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.post('/:templateId/callback', async (req, res) => {
|
||||||
|
const templateId = req.params.templateId;
|
||||||
|
|
||||||
|
// ONLYOFFICE sends JWT in the Authorization header
|
||||||
|
// Verify the token for security
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
if (authHeader) {
|
||||||
|
const decoded = verifyToken(authHeader);
|
||||||
|
if (!decoded) {
|
||||||
|
console.error('ONLYOFFICE callback: Invalid JWT token');
|
||||||
|
return res.status(403).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The callback payload from ONLYOFFICE
|
||||||
|
const payload = req.body;
|
||||||
|
const status = payload.status;
|
||||||
|
const downloadUrl = payload.url;
|
||||||
|
const key = payload.key;
|
||||||
|
|
||||||
|
console.log(`ONLYOFFICE callback: templateId=${templateId}, status=${status}, key=${key}`);
|
||||||
|
|
||||||
|
const registry = loadRegistry();
|
||||||
|
const template = registry.find(t => t.id === templateId);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ error: 'Template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status 2 = saved, 6 = force-saved -> download the updated file
|
||||||
|
if ((status === 2 || status === 6) && downloadUrl) {
|
||||||
|
try {
|
||||||
|
console.log(`Downloading updated template from ONLYOFFICE: ${downloadUrl}`);
|
||||||
|
|
||||||
|
// Fetch the updated .docx file from ONLYOFFICE's URL
|
||||||
|
const response = await axios.get(downloadUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overwrite the stored template file
|
||||||
|
const templatePath = template.path;
|
||||||
|
fs.writeFileSync(templatePath, response.data);
|
||||||
|
|
||||||
|
// Update the registry timestamp
|
||||||
|
template.updatedAt = new Date().toISOString();
|
||||||
|
saveRegistry(registry);
|
||||||
|
|
||||||
|
console.log(`Template "${template.name}" updated successfully (${response.data.length} bytes)`);
|
||||||
|
|
||||||
|
// ONLYOFFICE expects a JSON response acknowledging the save
|
||||||
|
return res.json({
|
||||||
|
error: 0,
|
||||||
|
key: key
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to download updated template:', err.message);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 1,
|
||||||
|
message: 'Failed to save: ' + err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status 4 = closed without changes, 0 = still editing, etc.
|
||||||
|
// Just acknowledge
|
||||||
|
return res.json({
|
||||||
|
error: 0,
|
||||||
|
key: key
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ENDPOINT: GET /api/onlyoffice/health
|
||||||
|
// Health check for the ONLYOFFICE integration
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
router.get('/health', async (req, res) => {
|
||||||
|
let onlyofficeStatus = 'unknown';
|
||||||
|
try {
|
||||||
|
const resp = await axios.get(`${ONLYOFFICE_INTERNAL_URL}/healthcheck`, { timeout: 5000 });
|
||||||
|
onlyofficeStatus = resp.status === 200 ? 'healthy' : 'unhealthy';
|
||||||
|
} catch (err) {
|
||||||
|
onlyofficeStatus = 'offline';
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
onlyoffice: onlyofficeStatus,
|
||||||
|
onlyofficePublicUrl: ONLYOFFICE_PUBLIC_URL,
|
||||||
|
onlyofficeInternalUrl: ONLYOFFICE_INTERNAL_URL,
|
||||||
|
backendUrl: BACKEND_PUBLIC_URL,
|
||||||
|
jwtEnabled: !!JWT_SECRET,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
253
renderer/package-lock.json
generated
253
renderer/package-lock.json
generated
@@ -10,9 +10,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^8.0.0",
|
"archiver": "^8.0.0",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"carbone": "^3.8.2",
|
"carbone": "^3.8.2",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"multer": "^2.2.0",
|
"multer": "^2.2.0",
|
||||||
"puppeteer": "^25.3.0",
|
"puppeteer": "^25.3.0",
|
||||||
"uuid": "^14.0.1"
|
"uuid": "^14.0.1"
|
||||||
@@ -96,6 +98,18 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
@@ -177,6 +191,24 @@
|
|||||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.16.0",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
|
||||||
@@ -434,6 +466,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@@ -538,6 +576,18 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/compress-commons": {
|
"node_modules/compress-commons": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz",
|
||||||
@@ -712,6 +762,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -741,6 +800,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -792,6 +860,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -987,6 +1070,42 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.4",
|
||||||
|
"mime-types": "^2.1.35"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1096,6 +1215,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
@@ -1128,6 +1262,19 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||||
@@ -1212,6 +1359,49 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lazystream": {
|
"node_modules/lazystream": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||||
@@ -1266,6 +1456,48 @@
|
|||||||
"url": "https://github.com/sponsors/antonk52"
|
"url": "https://github.com/sponsors/antonk52"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1485,6 +1717,15 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/puppeteer": {
|
"node_modules/puppeteer": {
|
||||||
"version": "25.3.0",
|
"version": "25.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
|
||||||
@@ -1638,6 +1879,18 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^8.0.0",
|
"archiver": "^8.0.0",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"carbone": "^3.8.2",
|
"carbone": "^3.8.2",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"multer": "^2.2.0",
|
"multer": "^2.2.0",
|
||||||
"puppeteer": "^25.3.0",
|
"puppeteer": "^25.3.0",
|
||||||
"uuid": "^14.0.1"
|
"uuid": "^14.0.1"
|
||||||
|
|||||||
@@ -183,4 +183,189 @@ async function viewTagsHelp() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
function downloadHelpPdf() {
|
function downloadHelpPdf() {
|
||||||
window.open(CARBONE_API + '/help-pdf', '_blank');
|
window.open(CARBONE_API + '/help-pdf', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ONLYOFFICE INLINE EDITOR
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const ONLYOFFICE_API = window.location.protocol + '//' + window.location.hostname + ':8771/api/onlyoffice';
|
||||||
|
let onlyofficeEditorInstance = null;
|
||||||
|
|
||||||
|
// Check ONLYOFFICE status when templates page loads
|
||||||
|
async function checkOnlyofficeStatus() {
|
||||||
|
const badge = document.getElementById('onlyoffice-status');
|
||||||
|
const list = document.getElementById('onlyoffice-templates-list');
|
||||||
|
if (!badge) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(ONLYOFFICE_API + '/health');
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.onlyoffice === 'healthy') {
|
||||||
|
badge.className = 'badge badge-green';
|
||||||
|
badge.textContent = 'online';
|
||||||
|
} else {
|
||||||
|
badge.className = 'badge badge-red';
|
||||||
|
badge.textContent = data.onlyoffice === 'offline' ? 'offline' : data.onlyoffice;
|
||||||
|
if (list) list.innerHTML = '<p class="text-muted text-sm">ONLYOFFICE server is not running. Start it with: <code>cd /root/workspace/cv-app && docker compose up -d onlyoffice-server</code></p>';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
badge.className = 'badge badge-red';
|
||||||
|
badge.textContent = 'error';
|
||||||
|
if (list) list.innerHTML = '<p class="text-muted text-sm">Cannot connect to ONLYOFFICE service. Make sure the Carbone+ONLYOFFICE backend is running.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load templates list for ONLYOFFICE editing
|
||||||
|
async function loadOnlyofficeTemplates() {
|
||||||
|
const list = document.getElementById('onlyoffice-templates-list');
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
// Check if ONLYOFFICE is online first
|
||||||
|
try {
|
||||||
|
const healthResp = await fetch(ONLYOFFICE_API + '/health');
|
||||||
|
const health = await healthResp.json();
|
||||||
|
if (health.onlyoffice !== 'healthy') {
|
||||||
|
list.innerHTML = '<p class="text-muted text-sm">ONLYOFFICE server is ' + health.onlyoffice + '. Start the container to enable inline editing.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
list.innerHTML = '<p class="text-muted text-sm">Cannot reach ONLYOFFICE service.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load templates from Carbone registry
|
||||||
|
try {
|
||||||
|
const resp = await fetch(CARBONE_API + '/templates');
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!data.templates || !data.templates.length) {
|
||||||
|
list.innerHTML = '<p class="text-muted text-sm">No templates uploaded yet. Upload a .docx template first to edit it inline.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = data.templates.map(t => `
|
||||||
|
<div class="skill-row">
|
||||||
|
<div>
|
||||||
|
<strong>${t.name}</strong>
|
||||||
|
<div class="text-muted text-sm">${t.originalName} · Updated: ${t.updatedAt ? new Date(t.updatedAt).toLocaleString() : new Date(t.uploadedAt).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<button class="btn btn-sm" onclick="openOnlyofficeEditor('${t.id}', '${t.name.replace(/'/g, "\\'")}')">Edit Online</button>
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="renderSampleCV('${t.id}')">Preview PDF</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteCarboneTemplate('${t.id}')">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
list.innerHTML = '<p class="text-muted text-sm">Error: ' + e.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open ONLYOFFICE editor in a modal
|
||||||
|
async function openOnlyofficeEditor(templateId, templateName) {
|
||||||
|
try {
|
||||||
|
// Get the JWT-signed editor config from our backend
|
||||||
|
const resp = await fetch(ONLYOFFICE_API + '/' + templateId + '/config');
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json();
|
||||||
|
throw new Error(err.error || 'Failed to get editor config');
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
const config = data.config;
|
||||||
|
const onlyofficeUrl = data.onlyofficeUrl;
|
||||||
|
|
||||||
|
// Build the editor modal
|
||||||
|
const body = `
|
||||||
|
<div class="flex-between mb-16">
|
||||||
|
<h3>Editing: ${templateName}</h3>
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="forceSaveOnlyoffice()">Save</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="closeOnlyofficeEditor()">Close Editor</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="onlyoffice-editor-container" style="width:100%;height:600px;border:1px solid var(--border);border-radius:8px;overflow:hidden"></div>
|
||||||
|
<p class="text-muted text-sm mt-8">Changes are saved automatically. Click "Save" to force-save, or "Close Editor" to return to the template list.</p>
|
||||||
|
`;
|
||||||
|
showModal(body, 'ONLYOFFICE Template Editor', 'large');
|
||||||
|
|
||||||
|
// Load ONLYOFFICE API script dynamically
|
||||||
|
const scriptUrl = onlyofficeUrl + '/web-apps/apps/api/documents/api.js';
|
||||||
|
if (!document.getElementById('onlyoffice-api-script')) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = 'onlyoffice-api-script';
|
||||||
|
script.src = scriptUrl;
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = () => reject(new Error('Failed to load ONLYOFFICE API from ' + scriptUrl));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the ONLYOFFICE editor
|
||||||
|
if (typeof DocsAPI === 'undefined' || !DocsAPI.DocEditor) {
|
||||||
|
throw new Error('ONLYOFFICE API not loaded. Check that the server is running at ' + onlyofficeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
onlyofficeEditorInstance = new DocsAPI.DocEditor('onlyoffice-editor-container', {
|
||||||
|
...config,
|
||||||
|
width: '100%',
|
||||||
|
height: '600px',
|
||||||
|
events: {
|
||||||
|
onAppReady: function() {
|
||||||
|
console.log('ONLYOFFICE editor ready');
|
||||||
|
},
|
||||||
|
onDocumentReady: function() {
|
||||||
|
console.log('Document loaded in editor');
|
||||||
|
},
|
||||||
|
onSave: function(event) {
|
||||||
|
toast('Document saved successfully');
|
||||||
|
// Reload the templates list
|
||||||
|
loadOnlyofficeTemplates();
|
||||||
|
},
|
||||||
|
onError: function(event) {
|
||||||
|
console.error('ONLYOFFICE error:', event);
|
||||||
|
toast('Editor error: ' + (event.data?.errorDescription || 'unknown'), 'error');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
toast('Failed to open editor: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force save the document
|
||||||
|
function forceSaveOnlyoffice() {
|
||||||
|
if (onlyofficeEditorInstance) {
|
||||||
|
// The ONLYOFFICE editor's onSave event will trigger the callback
|
||||||
|
// which saves the file to our backend
|
||||||
|
toast('Saving... changes will be stored automatically');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the editor and clean up
|
||||||
|
function closeOnlyofficeEditor() {
|
||||||
|
if (onlyofficeEditorInstance) {
|
||||||
|
try {
|
||||||
|
onlyofficeEditorInstance.destroyEditor();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error closing editor:', e);
|
||||||
|
}
|
||||||
|
onlyofficeEditorInstance = null;
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
// Refresh the templates list to show any changes
|
||||||
|
loadOnlyofficeTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call status check when templates page becomes active
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const templatesPage = document.getElementById('page-templates');
|
||||||
|
if (templatesPage) {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (templatesPage.classList.contains('active')) {
|
||||||
|
checkOnlyofficeStatus();
|
||||||
|
loadOnlyofficeTemplates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(templatesPage, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -76,6 +76,16 @@
|
|||||||
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="viewTagsHelp()">View Tags Help</button>
|
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="viewTagsHelp()">View Tags Help</button>
|
||||||
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="downloadHelpPdf()">Download Help (.pdf)</button>
|
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="downloadHelpPdf()">Download Help (.pdf)</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card mb-16">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">Inline Editor (ONLYOFFICE)</span>
|
||||||
|
<span id="onlyoffice-status" class="badge badge-orange">checking...</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted mb-16">
|
||||||
|
Edit templates directly in your browser with ONLYOFFICE Document Server. No need to download/upload — just click "Edit" and save changes online.
|
||||||
|
</div>
|
||||||
|
<div id="onlyoffice-templates-list"></div>
|
||||||
|
</div>
|
||||||
<div class="card mb-16">
|
<div class="card mb-16">
|
||||||
<div class="card-header"><span class="card-title">Upload Custom Template</span></div>
|
<div class="card-header"><span class="card-title">Upload Custom Template</span></div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user