Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fe136ae5a | |||
| 888834c8f0 | |||
| ae8f6d696e | |||
| 77c1824b00 | |||
| c54a2ae5e7 | |||
| 55162c0b93 |
+2
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = '/'
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+62
-3
@@ -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
|
||||||
@@ -62,7 +81,7 @@ const upload = multer({ dest: uploadDir });
|
|||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: allowedOrigin,
|
origin: allowedOrigins,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -106,7 +125,47 @@ const respondWithValidationErrors = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateSqliteHeader = (filePath: string): boolean => {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.alloc(16);
|
||||||
|
const fd = fs.openSync(filePath, "r");
|
||||||
|
const bytesRead = fs.readSync(fd, buffer, 0, 16, 0);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
|
||||||
|
if (bytesRead < 16) {
|
||||||
|
console.warn("File too small to be a valid SQLite database");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite format 3 header: "SQLite format 3\0" (16 bytes)
|
||||||
|
// Hex: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
|
||||||
|
const expectedHeader = Buffer.from([
|
||||||
|
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61,
|
||||||
|
0x74, 0x20, 0x33, 0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isValid = buffer.equals(expectedHeader);
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn("Invalid SQLite file header detected", {
|
||||||
|
filePath,
|
||||||
|
header: buffer.toString("hex"),
|
||||||
|
expected: expectedHeader.toString("hex"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to validate SQLite header:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const runIntegrityCheck = (filePath: string): boolean => {
|
const runIntegrityCheck = (filePath: string): boolean => {
|
||||||
|
// First validate the file header to prevent RCE attacks
|
||||||
|
if (!validateSqliteHeader(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let dbInstance: Database.Database | undefined;
|
let dbInstance: Database.Database | undefined;
|
||||||
try {
|
try {
|
||||||
dbInstance = new Database(filePath, {
|
dbInstance = new Database(filePath, {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user