Compare commits

..

6 Commits

Author SHA1 Message Date
Zimeng Xiong 0e8bec949b update nginx config 2025-11-22 21:06:01 -08:00
Zimeng Xiong 888834c8f0 Merge pull request #2 from ZimengXiong/fix-bind-mount-prisma
fix bind mount prisma, auto hydrate empty folder
2025-11-22 20:25:44 -08:00
Zimeng Xiong ae8f6d696e fix bind mount prisma, auto hydrate empty folder 2025-11-22 20:25:07 -08:00
Zimeng Xiong 77c1824b00 add fallback for browsers that do not have crypto.randomUUID 2025-11-22 19:18:05 -08:00
Zimeng Xiong c54a2ae5e7 add CORS fallback 2025-11-22 19:14:55 -08:00
Zimeng Xiong 55162c0b93 fix: add linux-musl-openssl-3.0.x 2025-11-22 19:07:28 -08:00
11 changed files with 115 additions and 13 deletions
+2 -1
View File
@@ -36,8 +36,9 @@ COPY package*.json ./
# Install production dependencies only # Install production dependencies only
RUN npm ci --only=production RUN npm ci --only=production
# Copy prisma schema and migrations # Copy prisma schema and migrations for runtime and hydration template
COPY prisma ./prisma/ COPY prisma ./prisma/
COPY prisma ./prisma_template/
# Copy built application from builder # Copy built application from builder
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
+6
View File
@@ -1,6 +1,12 @@
#!/bin/sh #!/bin/sh
set -e set -e
# Auto-hydrate prisma directory when bind-mounted volume is empty
if [ ! -f "/app/prisma/schema.prisma" ]; then
echo "Mount is empty. Hydrating /app/prisma from /app/prisma_template..."
cp -R /app/prisma_template/. /app/prisma/
fi
# Run migrations # Run migrations
npx prisma migrate deploy npx prisma migrate deploy
+1 -1
View File
@@ -4,7 +4,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
output = "../src/generated/client" output = "../src/generated/client"
binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"] binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
} }
datasource db { datasource db {
+7 -2
View File
@@ -148,6 +148,10 @@ const config = {
{ {
"fromEnvVar": null, "fromEnvVar": null,
"value": "linux-musl-arm64-openssl-3.0.x" "value": "linux-musl-arm64-openssl-3.0.x"
},
{
"fromEnvVar": null,
"value": "linux-musl-openssl-3.0.x"
} }
], ],
"previewFeatures": [], "previewFeatures": [],
@@ -165,6 +169,7 @@ const config = {
"db" "db"
], ],
"activeProvider": "sqlite", "activeProvider": "sqlite",
"postinstall": false,
"inlineDatasources": { "inlineDatasources": {
"db": { "db": {
"url": { "url": {
@@ -173,8 +178,8 @@ const config = {
} }
} }
}, },
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"linux-musl-arm64-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"linux-musl-arm64-openssl-3.0.x\", \"linux-musl-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"inlineSchemaHash": "9864a039193c73ddda01fd51751788fa5729bb0a603a9379a3fa314a4aced64f", "inlineSchemaHash": "30da526c2a5efdf3e5097c3736a52d47246ca4da8e5bd0401a3f28dd46ab5c3e",
"copyEngine": true "copyEngine": true
} }
config.dirname = '/' config.dirname = '/'
+11 -2
View File
@@ -149,6 +149,10 @@ const config = {
{ {
"fromEnvVar": null, "fromEnvVar": null,
"value": "linux-musl-arm64-openssl-3.0.x" "value": "linux-musl-arm64-openssl-3.0.x"
},
{
"fromEnvVar": null,
"value": "linux-musl-openssl-3.0.x"
} }
], ],
"previewFeatures": [], "previewFeatures": [],
@@ -166,6 +170,7 @@ const config = {
"db" "db"
], ],
"activeProvider": "sqlite", "activeProvider": "sqlite",
"postinstall": false,
"inlineDatasources": { "inlineDatasources": {
"db": { "db": {
"url": { "url": {
@@ -174,8 +179,8 @@ const config = {
} }
} }
}, },
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"linux-musl-arm64-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/client\"\n binaryTargets = [\"native\", \"linux-musl-arm64-openssl-3.0.x\", \"linux-musl-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Collection {\n id String @id @default(uuid())\n name String\n drawings Drawing[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Drawing {\n id String @id @default(uuid())\n name String\n elements String // Stored as JSON string\n appState String // Stored as JSON string\n files String @default(\"{}\") // Stored as JSON string\n preview String? // SVG string for thumbnail\n version Int @default(1)\n collectionId String?\n collection Collection? @relation(fields: [collectionId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"inlineSchemaHash": "9864a039193c73ddda01fd51751788fa5729bb0a603a9379a3fa314a4aced64f", "inlineSchemaHash": "30da526c2a5efdf3e5097c3736a52d47246ca4da8e5bd0401a3f28dd46ab5c3e",
"copyEngine": true "copyEngine": true
} }
@@ -219,6 +224,10 @@ path.join(process.cwd(), "src/generated/client/libquery_engine-darwin-arm64.dyli
// file annotations for bundling tools to include these files // file annotations for bundling tools to include these files
path.join(__dirname, "libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node"); path.join(__dirname, "libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node");
path.join(process.cwd(), "src/generated/client/libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node") path.join(process.cwd(), "src/generated/client/libquery_engine-linux-musl-arm64-openssl-3.0.x.so.node")
// file annotations for bundling tools to include these files
path.join(__dirname, "libquery_engine-linux-musl-openssl-3.0.x.so.node");
path.join(process.cwd(), "src/generated/client/libquery_engine-linux-musl-openssl-3.0.x.so.node")
// file annotations for bundling tools to include these files // file annotations for bundling tools to include these files
path.join(__dirname, "schema.prisma"); path.join(__dirname, "schema.prisma");
path.join(process.cwd(), "src/generated/client/schema.prisma") path.join(process.cwd(), "src/generated/client/schema.prisma")
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "prisma-client-04007c5051869a2f5298bd562ab2fb60a423747e0d5699dd1a73a4757b2657b6", "name": "prisma-client-6afe3d9baa793154c8d01c79f8418d91423e5ccaec794547bf848a451459cf53",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "index-browser.js", "browser": "index-browser.js",
+1 -1
View File
@@ -4,7 +4,7 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
output = "../src/generated/client" output = "../src/generated/client"
binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"] binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
} }
datasource db { datasource db {
+45 -4
View File
@@ -38,7 +38,26 @@ const resolveDatabaseUrl = (rawUrl?: string) => {
process.env.DATABASE_URL = resolveDatabaseUrl(process.env.DATABASE_URL); process.env.DATABASE_URL = resolveDatabaseUrl(process.env.DATABASE_URL);
console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL); console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL);
const allowedOrigin = process.env.FRONTEND_URL || "http://localhost:6767"; const normalizeOrigins = (rawOrigins?: string | null): string[] => {
const fallback = "http://localhost:6767";
if (!rawOrigins || rawOrigins.trim().length === 0) {
return [fallback];
}
const ensureProtocol = (origin: string) =>
/^https?:\/\//i.test(origin) ? origin : `http://${origin}`;
const parsed = rawOrigins
.split(",")
.map((origin) => origin.trim())
.filter((origin) => origin.length > 0)
.map(ensureProtocol);
return parsed.length > 0 ? parsed : [fallback];
};
const allowedOrigins = normalizeOrigins(process.env.FRONTEND_URL);
console.log("Allowed origins:", allowedOrigins);
const uploadDir = path.resolve(__dirname, "../uploads"); const uploadDir = path.resolve(__dirname, "../uploads");
if (!fs.existsSync(uploadDir)) { if (!fs.existsSync(uploadDir)) {
@@ -49,7 +68,7 @@ const app = express();
const httpServer = createServer(app); const httpServer = createServer(app);
const io = new Server(httpServer, { const io = new Server(httpServer, {
cors: { cors: {
origin: allowedOrigin, origin: allowedOrigins,
credentials: true, credentials: true,
}, },
maxHttpBufferSize: 1e8, // 100 MB maxHttpBufferSize: 1e8, // 100 MB
@@ -58,17 +77,39 @@ const prisma = new PrismaClient();
const PORT = process.env.PORT || 8000; const PORT = process.env.PORT || 8000;
// Multer setup for file uploads // Multer setup for file uploads
const upload = multer({ dest: uploadDir }); const upload = multer({
dest: uploadDir,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB in bytes
files: 1, // Only one file per upload
},
});
app.use( app.use(
cors({ cors({
origin: allowedOrigin, origin: allowedOrigins,
credentials: true, credentials: true,
}) })
); );
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" }));
// Log large requests for monitoring and debugging
app.use((req, res, next) => {
const contentLength = req.headers["content-length"];
if (contentLength) {
const sizeInMB = parseInt(contentLength) / 1024 / 1024;
if (sizeInMB > 10) {
console.log(
`[LARGE REQUEST] ${req.method} ${req.path} - ${sizeInMB.toFixed(
2
)}MB - Content-Length: ${contentLength} bytes`
);
}
}
next();
});
const elementsSchema = z.array(z.object({}).passthrough()); const elementsSchema = z.array(z.object({}).passthrough());
const appStateSchema = z.object({}).passthrough(); const appStateSchema = z.object({}).passthrough();
+15
View File
@@ -12,6 +12,9 @@ http {
gzip_vary on; gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Set maximum request body size to 50MB to handle large drawings with embedded images
client_max_body_size 50M;
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
@@ -29,6 +32,18 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# Buffer and timeout settings for large payloads
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
client_body_buffer_size 128k;
# Timeouts for large uploads (300 seconds)
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
} }
# WebSocket proxy for Socket.IO # WebSocket proxy for Socket.IO
+26 -1
View File
@@ -80,6 +80,31 @@ const COLORS = [
"#f43f5e", // rose-500 "#f43f5e", // rose-500
]; ];
const generateClientId = (): string => {
const cryptoObj: Crypto | undefined =
typeof globalThis !== "undefined"
? globalThis.crypto || (globalThis as any).msCrypto
: undefined;
if (cryptoObj?.randomUUID) {
return cryptoObj.randomUUID();
}
if (cryptoObj?.getRandomValues) {
const bytes = new Uint8Array(16);
cryptoObj.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40; // RFC 4122 variant
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex
.slice(6, 8)
.join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
}
// Final fallback for very old browsers; uniqueness window-scoped only.
return `id-${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
};
export const getUserIdentity = (): UserIdentity => { export const getUserIdentity = (): UserIdentity => {
const stored = localStorage.getItem("excalidash-user-id"); const stored = localStorage.getItem("excalidash-user-id");
if (stored) { if (stored) {
@@ -91,7 +116,7 @@ export const getUserIdentity = (): UserIdentity => {
const randomColor = COLORS[Math.floor(Math.random() * COLORS.length)]; const randomColor = COLORS[Math.floor(Math.random() * COLORS.length)];
const identity: UserIdentity = { const identity: UserIdentity = {
id: crypto.randomUUID(), id: generateClientId(),
name: randomTransformer.name, name: randomTransformer.name,
initials: randomTransformer.initials, initials: randomTransformer.initials,
color: randomColor, color: randomColor,