const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const { db } = require('../db/database');
const appConfig = require('../config');
const { PLATFORM_ROLES, ELEVATED_ROLES } = require('../middleware/auth');
// Phase 2.2d: workspace-aware access. Same pattern as devices.js / content.js.
const { accessContext } = require('../lib/tenancy');
// For preview only: inline /api/content/:id/file and /thumbnail URLs as data URIs,
// scoped to the caller's current workspace. Lets the srcdoc preview iframe show
// logos/bg images before the widget is saved (post-save they're reachable via
// the widget-reference gate).
const MAX_INLINE_BYTES = 10 * 1024 * 1024; // 10MB cap — base64 expands ~1.33x
const MIME_RE = /^image\/[a-zA-Z0-9.+-]+$/;
function inlineUserContent(html, workspaceId) {
if (!workspaceId) return html;
return html.replace(/\/api\/content\/([a-f0-9-]+)\/(file|thumbnail)/gi, (match, id, kind) => {
const c = db.prepare('SELECT filepath, thumbnail_path, mime_type, workspace_id FROM content WHERE id = ?').get(id);
// Inline content only when it lives in the caller's workspace, or is a
// platform-template row (workspace_id IS NULL) shared with everyone.
if (!c) return match;
if (c.workspace_id && c.workspace_id !== workspaceId) return match;
const filename = kind === 'thumbnail' ? c.thumbnail_path : c.filepath;
if (!filename) return match;
const mime = kind === 'thumbnail' ? 'image/jpeg' : c.mime_type;
if (!mime || !MIME_RE.test(mime)) return match;
const safe = path.resolve(appConfig.contentDir, path.basename(filename));
if (!safe.startsWith(path.resolve(appConfig.contentDir))) return match;
try {
const st = fs.statSync(safe);
if (!st.isFile() || st.size > MAX_INLINE_BYTES) return match;
const buf = fs.readFileSync(safe);
return `data:${mime};base64,${buf.toString('base64')}`;
} catch { return match; }
});
}
// Escape HTML to prevent XSS
function escapeHtml(str) {
if (typeof str !== 'string') return str;
return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
// Validate timezone format (e.g. America/New_York, UTC, Etc/GMT+5)
function safeTimezone(tz) {
if (!tz) return 'UTC';
return /^[A-Za-z_\-\/+0-9]+$/.test(tz) ? tz : 'UTC';
}
// Validate ISO date string format
function safeDateString(d) {
if (!d) return '';
return /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?/.test(d) ? d : '';
}
// Validate URL is http/https
function safeUrl(url) {
if (!url) return 'about:blank';
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol) ? url : 'about:blank';
} catch { return 'about:blank'; }
}
// Security: widget render output is public and CSP-exempt, so config values that
// get inlined into