fix export source and verisoning

This commit is contained in:
Zimeng Xiong
2026-01-30 14:57:27 -08:00
parent 81918b00cd
commit 7dfa69de2a
+55 -40
View File
@@ -48,12 +48,14 @@ const resolveDatabaseUrl = (rawUrl?: string) => {
const prismaDir = path.resolve(backendRoot, "prisma"); const prismaDir = path.resolve(backendRoot, "prisma");
const normalizedRelative = filePath.replace(/^\.\/?/, ""); const normalizedRelative = filePath.replace(/^\.\/?/, "");
const hasLeadingPrismaDir = const hasLeadingPrismaDir =
normalizedRelative === "prisma" || normalizedRelative === "prisma" || normalizedRelative.startsWith("prisma/");
normalizedRelative.startsWith("prisma/");
const absolutePath = path.isAbsolute(filePath) const absolutePath = path.isAbsolute(filePath)
? filePath ? filePath
: path.resolve(hasLeadingPrismaDir ? backendRoot : prismaDir, normalizedRelative); : path.resolve(
hasLeadingPrismaDir ? backendRoot : prismaDir,
normalizedRelative,
);
return `file:${absolutePath}`; return `file:${absolutePath}`;
}; };
@@ -146,7 +148,7 @@ const io = new Server(httpServer, {
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const parseJsonField = <T>( const parseJsonField = <T>(
rawValue: string | null | undefined, rawValue: string | null | undefined,
fallback: T fallback: T,
): T => { ): T => {
if (!rawValue) return fallback; if (!rawValue) return fallback;
try { try {
@@ -240,7 +242,7 @@ app.use(
credentials: true, credentials: true,
allowedHeaders: ["Content-Type", "Authorization", "x-csrf-token"], allowedHeaders: ["Content-Type", "Authorization", "x-csrf-token"],
exposedHeaders: ["x-csrf-token"], exposedHeaders: ["x-csrf-token"],
}) }),
); );
app.use(express.json({ limit: "50mb" })); app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" })); app.use(express.urlencoded({ extended: true, limit: "50mb" }));
@@ -252,8 +254,8 @@ app.use((req, res, next) => {
if (sizeInMB > 10) { if (sizeInMB > 10) {
console.log( console.log(
`[LARGE REQUEST] ${req.method} ${req.path} - ${sizeInMB.toFixed( `[LARGE REQUEST] ${req.method} ${req.path} - ${sizeInMB.toFixed(
2 2,
)}MB - Content-Length: ${contentLength} bytes` )}MB - Content-Length: ${contentLength} bytes`,
); );
} }
} }
@@ -267,18 +269,18 @@ app.use((req, res, next) => {
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
res.setHeader( res.setHeader(
"Permissions-Policy", "Permissions-Policy",
"geolocation=(), microphone=(), camera=()" "geolocation=(), microphone=(), camera=()",
); );
res.setHeader( res.setHeader(
"Content-Security-Policy", "Content-Security-Policy",
"default-src 'self'; " + "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " + "font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: blob: https:; " + "img-src 'self' data: blob: https:; " +
"connect-src 'self' ws: wss:; " + "connect-src 'self' ws: wss:; " +
"frame-ancestors 'none';" "frame-ancestors 'none';",
); );
next(); next();
@@ -287,14 +289,17 @@ app.use((req, res, next) => {
const requestCounts = new Map<string, { count: number; resetTime: number }>(); const requestCounts = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 15 * 60 * 1000; const RATE_LIMIT_WINDOW = 15 * 60 * 1000;
setInterval(() => { setInterval(
const now = Date.now(); () => {
for (const [ip, data] of requestCounts.entries()) { const now = Date.now();
if (now > data.resetTime) { for (const [ip, data] of requestCounts.entries()) {
requestCounts.delete(ip); if (now > data.resetTime) {
requestCounts.delete(ip);
}
} }
} },
}, 5 * 60 * 1000).unref(); 5 * 60 * 1000,
).unref();
const RATE_LIMIT_MAX_REQUESTS = (() => { const RATE_LIMIT_MAX_REQUESTS = (() => {
const parsed = Number(process.env.RATE_LIMIT_MAX_REQUESTS); const parsed = Number(process.env.RATE_LIMIT_MAX_REQUESTS);
@@ -361,7 +366,10 @@ app.get("/csrf-token", (req, res) => {
} }
clientLimit.count++; clientLimit.count++;
} else { } else {
csrfRateLimit.set(ip, { count: 1, resetTime: now + CSRF_RATE_LIMIT_WINDOW }); csrfRateLimit.set(ip, {
count: 1,
resetTime: now + CSRF_RATE_LIMIT_WINDOW,
});
} }
// Cleanup old rate limit entries occasionally // Cleanup old rate limit entries occasionally
@@ -376,7 +384,7 @@ app.get("/csrf-token", (req, res) => {
res.json({ res.json({
token, token,
header: getCsrfTokenHeader() header: getCsrfTokenHeader(),
}); });
}); });
@@ -384,7 +392,7 @@ app.get("/csrf-token", (req, res) => {
const csrfProtectionMiddleware = ( const csrfProtectionMiddleware = (
req: express.Request, req: express.Request,
res: express.Response, res: express.Response,
next: express.NextFunction next: express.NextFunction,
) => { ) => {
// Skip CSRF validation for safe methods (GET, HEAD, OPTIONS) // Skip CSRF validation for safe methods (GET, HEAD, OPTIONS)
// Note: /csrf-token is a GET endpoint, so it's automatically exempt // Note: /csrf-token is a GET endpoint, so it's automatically exempt
@@ -477,7 +485,7 @@ const drawingCreateSchema = drawingBaseSchema
}, },
{ {
message: "Invalid or malicious drawing data detected", message: "Invalid or malicious drawing data detected",
} },
); );
const drawingUpdateSchema = drawingBaseSchema const drawingUpdateSchema = drawingBaseSchema
@@ -527,12 +535,12 @@ const drawingUpdateSchema = drawingBaseSchema
}, },
{ {
message: "Invalid or malicious drawing data detected", message: "Invalid or malicious drawing data detected",
} },
); );
const respondWithValidationErrors = ( const respondWithValidationErrors = (
res: express.Response, res: express.Response,
issues: z.ZodIssue[] issues: z.ZodIssue[],
) => { ) => {
res.status(400).json({ res.status(400).json({
error: "Invalid drawing payload", error: "Invalid drawing payload",
@@ -582,7 +590,7 @@ const verifyDatabaseIntegrityAsync = (filePath: string): Promise<boolean> => {
path.resolve(__dirname, "./workers/db-verify.js"), path.resolve(__dirname, "./workers/db-verify.js"),
{ {
workerData: { filePath }, workerData: { filePath },
} },
); );
let timeoutHandle: NodeJS.Timeout; let timeoutHandle: NodeJS.Timeout;
let settled = false; let settled = false;
@@ -657,7 +665,7 @@ io.on("connection", (socket) => {
roomUsers.set(roomId, filteredUsers); roomUsers.set(roomId, filteredUsers);
io.to(roomId).emit("presence-update", filteredUsers); io.to(roomId).emit("presence-update", filteredUsers);
} },
); );
socket.on("cursor-move", (data) => { socket.on("cursor-move", (data) => {
@@ -682,7 +690,7 @@ io.on("connection", (socket) => {
io.to(roomId).emit("presence-update", users); io.to(roomId).emit("presence-update", users);
} }
} }
} },
); );
socket.on("disconnect", () => { socket.on("disconnect", () => {
@@ -1073,8 +1081,9 @@ app.get("/export", async (req, res) => {
res.setHeader("Content-Type", "application/octet-stream"); res.setHeader("Content-Type", "application/octet-stream");
res.setHeader( res.setHeader(
"Content-Disposition", "Content-Disposition",
`attachment; filename="excalidash-db-${new Date().toISOString().split("T")[0] `attachment; filename="excalidash-db-${
}.${extension}"` new Date().toISOString().split("T")[0]
}.${extension}"`,
); );
const fileStream = fs.createReadStream(dbPath); const fileStream = fs.createReadStream(dbPath);
@@ -1096,8 +1105,9 @@ app.get("/export/json", async (req, res) => {
res.setHeader("Content-Type", "application/zip"); res.setHeader("Content-Type", "application/zip");
res.setHeader( res.setHeader(
"Content-Disposition", "Content-Disposition",
`attachment; filename="excalidraw-drawings-${new Date().toISOString().split("T")[0] `attachment; filename="excalidraw-drawings-${
}.zip"` new Date().toISOString().split("T")[0]
}.zip"`,
); );
const archive = archiver("zip", { zlib: { level: 9 } }); const archive = archiver("zip", { zlib: { level: 9 } });
@@ -1111,6 +1121,8 @@ app.get("/export/json", async (req, res) => {
const drawingsByCollection: { [key: string]: any[] } = {}; const drawingsByCollection: { [key: string]: any[] } = {};
const exportSource = `${req.protocol}://${req.get("host")}`;
drawings.forEach((drawing: any) => { drawings.forEach((drawing: any) => {
const collectionName = drawing.collection?.name || "Unorganized"; const collectionName = drawing.collection?.name || "Unorganized";
if (!drawingsByCollection[collectionName]) { if (!drawingsByCollection[collectionName]) {
@@ -1118,6 +1130,9 @@ app.get("/export/json", async (req, res) => {
} }
const drawingData = { const drawingData = {
type: "excalidraw",
version: 2,
source: exportSource,
elements: JSON.parse(drawing.elements), elements: JSON.parse(drawing.elements),
appState: JSON.parse(drawing.appState), appState: JSON.parse(drawing.appState),
files: JSON.parse(drawing.files || "{}"), files: JSON.parse(drawing.files || "{}"),
@@ -1135,7 +1150,7 @@ app.get("/export/json", async (req, res) => {
collectionDrawings.forEach((drawing, index) => { collectionDrawings.forEach((drawing, index) => {
const fileName = `${drawing.name.replace( const fileName = `${drawing.name.replace(
/[<>:"/\\|?*]/g, /[<>:"/\\|?*]/g,
"_" "_",
)}.excalidraw`; )}.excalidraw`;
const filePath = `${folderName}/${fileName}`; const filePath = `${folderName}/${fileName}`;
@@ -1143,7 +1158,7 @@ app.get("/export/json", async (req, res) => {
name: filePath, name: filePath,
}); });
}); });
} },
); );
const readmeContent = `ExcaliDash Export const readmeContent = `ExcaliDash Export
@@ -1161,8 +1176,8 @@ Total Drawings: ${drawings.length}
Collections: Collections:
${Object.entries(drawingsByCollection) ${Object.entries(drawingsByCollection)
.map(([name, drawings]) => `- ${name}: ${drawings.length} drawings`) .map(([name, drawings]) => `- ${name}: ${drawings.length} drawings`)
.join("\n")} .join("\n")}
`; `;
archive.append(readmeContent, { name: "README.txt" }); archive.append(readmeContent, { name: "README.txt" });
@@ -1207,7 +1222,7 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
const originalPath = req.file.path; const originalPath = req.file.path;
const stagedPath = path.join( const stagedPath = path.join(
uploadDir, uploadDir,
`temp-${Date.now()}-${Math.random().toString(36).slice(2)}.db` `temp-${Date.now()}-${Math.random().toString(36).slice(2)}.db`,
); );
try { try {
@@ -1234,7 +1249,7 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
try { try {
await fsPromises.access(dbPath); await fsPromises.access(dbPath);
await fsPromises.copyFile(dbPath, backupPath); await fsPromises.copyFile(dbPath, backupPath);
} catch { } } catch {}
await moveFile(stagedPath, dbPath); await moveFile(stagedPath, dbPath);
} catch (error) { } catch (error) {