Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a366acfedc | |||
| 154dcbb151 | |||
| 2e74d2ad1a | |||
| 173c050f58 | |||
| 8161a563f0 | |||
| 812f1cbf58 | |||
| 26017fa5d2 | |||
| 06f4c0f537 | |||
| bbb23ca661 | |||
| f214e4f7b7 | |||
| 7aa33a1bdf | |||
| ea06cd9175 | |||
| 734f0a292d | |||
| 08135ee36a | |||
| f462b2e288 | |||
| 01fda32bcd | |||
| 94694deb91 | |||
| ef75f9ebdf | |||
| 5e782e4044 |
@@ -7,3 +7,9 @@ dist
|
|||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
backend
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
frontend/coverage
|
||||||
|
frontend/test-results
|
||||||
|
frontend/playwright-report
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Start backend server in background
|
# Start backend server in background
|
||||||
cd backend
|
cd backend
|
||||||
DATABASE_URL="file:${{ github.workspace }}/backend/prisma/e2e-test.db" FRONTEND_URL="http://localhost:5173" npm run dev &
|
DATABASE_URL="file:${{ github.workspace }}/backend/prisma/e2e-test.db" FRONTEND_URL="http://localhost:6767" npm run dev &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
# Wait for frontend to be ready
|
# Wait for frontend to be ready
|
||||||
echo "Waiting for frontend server..."
|
echo "Waiting for frontend server..."
|
||||||
for i in {1..30}; do
|
for i in {1..30}; do
|
||||||
if curl -s http://localhost:5173 > /dev/null; then
|
if curl -s http://localhost:6767 > /dev/null; then
|
||||||
echo "Frontend is ready!"
|
echo "Frontend is ready!"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ docker compose -f docker-compose.prod.yml up -d
|
|||||||
# Access the frontend at localhost:6767
|
# Access the frontend at localhost:6767
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For single-container deployments, `JWT_SECRET` can be omitted and will be auto-generated and persisted in the backend volume on first start. For portability and all multi-instance deployments, set a fixed `JWT_SECRET` explicitly.
|
||||||
|
|
||||||
## Docker Build
|
## Docker Build
|
||||||
|
|
||||||
[Install Docker](https://docs.docker.com/desktop/)
|
[Install Docker](https://docs.docker.com/desktop/)
|
||||||
@@ -141,7 +143,7 @@ frontend:
|
|||||||
|
|
||||||
### Multi-Container / Kubernetes Deployments
|
### Multi-Container / Kubernetes Deployments
|
||||||
|
|
||||||
When running multiple backend replicas (e.g., Kubernetes, Docker Swarm, or load-balanced containers), you **must** set the `CSRF_SECRET` environment variable to the same value across all instances.
|
When running multiple backend replicas (e.g., Kubernetes, Docker Swarm, or load-balanced containers), you **must** set both `JWT_SECRET` and `CSRF_SECRET` to the same values across all instances.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate a secure secret
|
# Generate a secure secret
|
||||||
@@ -152,6 +154,7 @@ openssl rand -base64 32
|
|||||||
# docker-compose.yml or k8s deployment
|
# docker-compose.yml or k8s deployment
|
||||||
backend:
|
backend:
|
||||||
environment:
|
environment:
|
||||||
|
- JWT_SECRET=your-generated-jwt-secret-here
|
||||||
- CSRF_SECRET=your-generated-secret-here
|
- CSRF_SECRET=your-generated-secret-here
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+6
-40
@@ -1,43 +1,9 @@
|
|||||||
CSRF Protection (8a78b2b)
|
Multi user setup is opt-in, single user by default
|
||||||
|
|
||||||
- Implemented comprehensive CSRF (Cross-Site Request Forgery) protection for enhanced security
|
Multi-user support for excalidash
|
||||||
- Added new backend/src/security.ts module for security utilities
|
- Admin dashboard
|
||||||
- Frontend API layer now handles CSRF tokens automatically
|
- Password reset, force user password reset (admin only), account lockout recovery
|
||||||
- Added integration tests for CSRF validation
|
- Rate limits
|
||||||
|
|
||||||
Upload Progress Indicator (8f9b9b4)
|
Deprecates .json and .sqlite database backups in favor of .excalidash archives (user scoped, prevents exporting of senstive information). Legacy import is maintained.
|
||||||
|
|
||||||
- Added a visual upload progress bar when users upload files
|
|
||||||
- New UploadContext for managing upload state across components
|
|
||||||
- New UploadStatus component displaying real-time upload progress
|
|
||||||
- Save status indicator when navigating back from the editor
|
|
||||||
- Improved error handling and recovery for failed uploads
|
|
||||||
|
|
||||||
Bug Fixes
|
|
||||||
|
|
||||||
- Fixed broken e2e tests (cae8f3c)
|
|
||||||
- Replaced deprecated substr() with substring()
|
|
||||||
- Fixed stale state issues in error handling
|
|
||||||
- Fixed missing useEffect dependencies
|
|
||||||
- Fixed CSS class conflicts in progress bar styling
|
|
||||||
- Added error recovery for save state in Editor
|
|
||||||
|
|
||||||
Infrastructure
|
|
||||||
|
|
||||||
- Updated docker-compose configurations with new environment variables
|
|
||||||
- E2E test suite improvements and reliability fixes
|
|
||||||
- Added Kubernetes deployment note in README
|
|
||||||
|
|
||||||
### Kubernetes
|
|
||||||
|
|
||||||
A `CSRF_SECRET` environment variable is now required for CSRF protection. Generate a secure 32+ character random string:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 32
|
|
||||||
|
|
||||||
Add it to your deployment:
|
|
||||||
- Docker Compose: Add CSRF_SECRET=<your-secret> to the backend service environment
|
|
||||||
- Kubernetes: Add to your ConfigMap/Secret and reference in the backend deployment
|
|
||||||
|
|
||||||
If not set, the backend will refuse to start.
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
prisma/dev.db
|
prisma/dev.db
|
||||||
prisma/dev.db-journal
|
prisma/dev.db-journal
|
||||||
|
src/generated
|
||||||
|
coverage
|
||||||
|
*.test.ts
|
||||||
|
*.spec.ts
|
||||||
|
|||||||
+9
-6
@@ -3,12 +3,15 @@ FROM node:20-alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Native build deps for modules that may compile from source (e.g., better-sqlite3 on arm64)
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
COPY tsconfig.json ./
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci
|
RUN npm ci && npm cache clean --force
|
||||||
|
|
||||||
# Copy prisma schema
|
# Copy prisma schema
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
@@ -25,7 +28,7 @@ RUN npx tsc
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install OpenSSL for Prisma and su-exec, create non-root user
|
# Install runtime packages and create non-root user
|
||||||
RUN apk add --no-cache openssl su-exec && \
|
RUN apk add --no-cache openssl su-exec && \
|
||||||
addgroup -g 1001 -S nodejs && \
|
addgroup -g 1001 -S nodejs && \
|
||||||
adduser -S nodejs -u 1001
|
adduser -S nodejs -u 1001
|
||||||
@@ -36,7 +39,10 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install production dependencies only
|
# Install production dependencies only
|
||||||
RUN npm ci --only=production
|
RUN apk add --no-cache --virtual .build-deps python3 make g++ && \
|
||||||
|
npm ci --omit=dev && \
|
||||||
|
npm cache clean --force && \
|
||||||
|
apk del .build-deps
|
||||||
|
|
||||||
# Copy prisma schema and migrations for runtime and hydration template
|
# Copy prisma schema and migrations for runtime and hydration template
|
||||||
COPY prisma ./prisma/
|
COPY prisma ./prisma/
|
||||||
@@ -48,9 +54,6 @@ COPY --from=builder /app/dist ./dist
|
|||||||
# Copy the generated Prisma Client from builder to maintain the same structure
|
# Copy the generated Prisma Client from builder to maintain the same structure
|
||||||
COPY --from=builder /app/src/generated ./dist/generated
|
COPY --from=builder /app/src/generated ./dist/generated
|
||||||
|
|
||||||
# Generate Prisma Client in production (updates node_modules)
|
|
||||||
RUN npx prisma generate
|
|
||||||
|
|
||||||
# Create necessary directories (ownership will be set in entrypoint)
|
# Create necessary directories (ownership will be set in entrypoint)
|
||||||
RUN mkdir -p /app/uploads /app/prisma
|
RUN mkdir -p /app/uploads /app/prisma
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
JWT_SECRET_FILE="/app/prisma/.jwt_secret"
|
||||||
|
|
||||||
|
# Ensure JWT secret exists for production startup.
|
||||||
|
# Backward compatibility: older installs may not have JWT_SECRET configured.
|
||||||
|
if [ -z "${JWT_SECRET:-}" ]; then
|
||||||
|
echo "JWT_SECRET not provided, resolving persisted secret..."
|
||||||
|
if [ -f "${JWT_SECRET_FILE}" ]; then
|
||||||
|
JWT_SECRET="$(tr -d '\r\n' < "${JWT_SECRET_FILE}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${JWT_SECRET}" ]; then
|
||||||
|
echo "No persisted JWT secret found. Generating a new secret..."
|
||||||
|
JWT_SECRET="$(openssl rand -hex 32)"
|
||||||
|
umask 077
|
||||||
|
printf "%s" "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Persist explicitly provided secret to support future restarts without env injection.
|
||||||
|
umask 077
|
||||||
|
printf "%s" "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export JWT_SECRET
|
||||||
|
|
||||||
# 1. Hydrate volume if empty (Running as root)
|
# 1. Hydrate volume if empty (Running as root)
|
||||||
if [ ! -f "/app/prisma/schema.prisma" ]; then
|
if [ ! -f "/app/prisma/schema.prisma" ]; then
|
||||||
echo "Mount is empty. Hydrating /app/prisma..."
|
echo "Mount is empty. Hydrating /app/prisma..."
|
||||||
@@ -18,6 +42,7 @@ echo "Fixing filesystem permissions..."
|
|||||||
chown -R nodejs:nodejs /app/uploads
|
chown -R nodejs:nodejs /app/uploads
|
||||||
chown -R nodejs:nodejs /app/prisma
|
chown -R nodejs:nodejs /app/prisma
|
||||||
chmod 755 /app/uploads
|
chmod 755 /app/uploads
|
||||||
|
chmod 600 "${JWT_SECRET_FILE}"
|
||||||
|
|
||||||
# Ensure database file has proper permissions
|
# Ensure database file has proper permissions
|
||||||
if [ -f "/app/prisma/dev.db" ]; then
|
if [ -f "/app/prisma/dev.db" ]; then
|
||||||
|
|||||||
Generated
+31
-17
@@ -1,23 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.3.2",
|
"version": "0.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.3.2",
|
"version": "0.4.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@types/archiver": "^7.0.0",
|
|
||||||
"@types/bcrypt": "^6.0.0",
|
|
||||||
"@types/jsdom": "^21.1.7",
|
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
|
||||||
"@types/ms": "^2.1.0",
|
|
||||||
"@types/multer": "^2.0.0",
|
|
||||||
"@types/socket.io": "^3.0.1",
|
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
@@ -38,10 +30,18 @@
|
|||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/archiver": "^7.0.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/ms": "^2.1.0",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/socket.io": "^3.0.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"supertest": "^7.1.4",
|
"supertest": "^7.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
@@ -1007,6 +1007,7 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz",
|
||||||
"integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==",
|
"integrity": "sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/readdir-glob": "*"
|
"@types/readdir-glob": "*"
|
||||||
@@ -1016,6 +1017,7 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -1025,6 +1027,7 @@
|
|||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
@@ -1046,6 +1049,7 @@
|
|||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -1085,6 +1089,7 @@
|
|||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
|
||||||
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
|
"integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
@@ -1096,6 +1101,7 @@
|
|||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
|
||||||
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
|
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -1108,12 +1114,14 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/jsdom": {
|
"node_modules/@types/jsdom": {
|
||||||
"version": "21.1.7",
|
"version": "21.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
|
||||||
"integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
|
"integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -1125,6 +1133,7 @@
|
|||||||
"version": "9.0.10",
|
"version": "9.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ms": "*",
|
"@types/ms": "*",
|
||||||
@@ -1142,18 +1151,21 @@
|
|||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/multer": {
|
"node_modules/@types/multer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
|
||||||
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
|
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
@@ -1164,7 +1176,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -1173,18 +1184,21 @@
|
|||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/range-parser": {
|
"node_modules/@types/range-parser": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/readdir-glob": {
|
"node_modules/@types/readdir-glob": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz",
|
||||||
"integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
|
"integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -1194,6 +1208,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -1203,6 +1218,7 @@
|
|||||||
"version": "1.15.10",
|
"version": "1.15.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
||||||
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
@@ -1214,6 +1230,7 @@
|
|||||||
"version": "0.17.6",
|
"version": "0.17.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
||||||
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mime": "^1",
|
"@types/mime": "^1",
|
||||||
@@ -1224,6 +1241,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz",
|
||||||
"integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==",
|
"integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "*"
|
"socket.io": "*"
|
||||||
@@ -1257,6 +1275,7 @@
|
|||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
@@ -1270,6 +1289,7 @@
|
|||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
@@ -2655,7 +2675,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz",
|
||||||
"integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==",
|
"integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -4090,7 +4109,6 @@
|
|||||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/engines": "5.22.0"
|
"@prisma/engines": "5.22.0"
|
||||||
},
|
},
|
||||||
@@ -5103,7 +5121,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5262,7 +5279,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -5353,7 +5369,6 @@
|
|||||||
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -5447,7 +5462,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "0.3.2",
|
"version": "0.4.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,14 +17,6 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@types/archiver": "^7.0.0",
|
|
||||||
"@types/bcrypt": "^6.0.0",
|
|
||||||
"@types/jsdom": "^21.1.7",
|
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
|
||||||
"@types/ms": "^2.1.0",
|
|
||||||
"@types/multer": "^2.0.0",
|
|
||||||
"@types/socket.io": "^3.0.1",
|
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^12.4.6",
|
"better-sqlite3": "^12.4.6",
|
||||||
@@ -45,10 +37,18 @@
|
|||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/archiver": "^7.0.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/ms": "^2.1.0",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/socket.io": "^3.0.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"supertest": "^7.1.4",
|
"supertest": "^7.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Improve dashboard query performance for user-scoped collection and drawing listings.
|
||||||
|
CREATE INDEX IF NOT EXISTS "Collection_userId_updatedAt_idx"
|
||||||
|
ON "Collection" ("userId", "updatedAt");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Drawing_userId_updatedAt_idx"
|
||||||
|
ON "Drawing" ("userId", "updatedAt");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Drawing_userId_collectionId_updatedAt_idx"
|
||||||
|
ON "Drawing" ("userId", "collectionId", "updatedAt");
|
||||||
@@ -49,6 +49,8 @@ model Collection {
|
|||||||
drawings Drawing[]
|
drawings Drawing[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId, updatedAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Drawing {
|
model Drawing {
|
||||||
@@ -65,6 +67,9 @@ model Drawing {
|
|||||||
collection Collection? @relation(fields: [collectionId], references: [id])
|
collection Collection? @relation(fields: [collectionId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId, updatedAt])
|
||||||
|
@@index([userId, collectionId, updatedAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Library {
|
model Library {
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||||
|
import request from "supertest";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { getTestPrisma, setupTestDb, cleanupTestDb } from "./testUtils";
|
||||||
|
|
||||||
|
type LegacyDbOptions = {
|
||||||
|
tableStyle: "prisma" | "plural-lower";
|
||||||
|
includeCollections: boolean;
|
||||||
|
includeMigrationsTable: boolean;
|
||||||
|
includeTrashDrawing: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTempDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "excalidash-legacy-"));
|
||||||
|
|
||||||
|
const openWritableDb = (filePath: string): any => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { DatabaseSync } = require("node:sqlite") as any;
|
||||||
|
return new DatabaseSync(filePath, { enableForeignKeyConstraints: false });
|
||||||
|
} catch (_err) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Database = require("better-sqlite3") as any;
|
||||||
|
return new Database(filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLegacySqliteDb = (opts: LegacyDbOptions): string => {
|
||||||
|
const dir = createTempDir();
|
||||||
|
const filePath = path.join(dir, "legacy-export.db");
|
||||||
|
const db = openWritableDb(filePath);
|
||||||
|
|
||||||
|
const tableDrawing = opts.tableStyle === "plural-lower" ? "drawings" : "Drawing";
|
||||||
|
const tableCollection = opts.tableStyle === "plural-lower" ? "collections" : "Collection";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (opts.includeCollections) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE "${tableCollection}" (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
createdAt TEXT,
|
||||||
|
updatedAt TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
db.prepare(`INSERT INTO "${tableCollection}" (id, name, createdAt, updatedAt) VALUES (?, ?, ?, ?)`).run(
|
||||||
|
"legacy-collection-1",
|
||||||
|
"Legacy Collection",
|
||||||
|
new Date("2024-01-01T00:00:00.000Z").toISOString(),
|
||||||
|
new Date("2024-01-02T00:00:00.000Z").toISOString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE "${tableDrawing}" (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
elements TEXT NOT NULL,
|
||||||
|
appState TEXT NOT NULL,
|
||||||
|
files TEXT,
|
||||||
|
preview TEXT,
|
||||||
|
version INTEGER,
|
||||||
|
collectionId TEXT,
|
||||||
|
collectionName TEXT,
|
||||||
|
createdAt TEXT,
|
||||||
|
updatedAt TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const now = new Date("2024-01-03T00:00:00.000Z").toISOString();
|
||||||
|
const insertDrawing = db.prepare(
|
||||||
|
`INSERT INTO "${tableDrawing}"
|
||||||
|
(id, name, elements, appState, files, preview, version, collectionId, collectionName, createdAt, updatedAt)
|
||||||
|
VALUES
|
||||||
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
);
|
||||||
|
|
||||||
|
insertDrawing.run(
|
||||||
|
"legacy-drawing-1",
|
||||||
|
"Legacy Drawing 1",
|
||||||
|
JSON.stringify([]),
|
||||||
|
JSON.stringify({}),
|
||||||
|
JSON.stringify({}),
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
opts.includeCollections ? "legacy-collection-1" : null,
|
||||||
|
opts.includeCollections ? "Legacy Collection" : null,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
|
||||||
|
insertDrawing.run(
|
||||||
|
"legacy-drawing-2",
|
||||||
|
"Legacy Drawing 2 (unorganized)",
|
||||||
|
JSON.stringify([]),
|
||||||
|
JSON.stringify({}),
|
||||||
|
JSON.stringify({}),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opts.includeTrashDrawing) {
|
||||||
|
insertDrawing.run(
|
||||||
|
"legacy-drawing-trash",
|
||||||
|
"Legacy Trash Drawing",
|
||||||
|
JSON.stringify([]),
|
||||||
|
JSON.stringify({}),
|
||||||
|
JSON.stringify({}),
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
"trash",
|
||||||
|
"Trash",
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.includeMigrationsTable) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE "_prisma_migrations" (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
checksum TEXT NOT NULL,
|
||||||
|
finished_at TEXT,
|
||||||
|
migration_name TEXT NOT NULL,
|
||||||
|
logs TEXT,
|
||||||
|
rolled_back_at TEXT,
|
||||||
|
started_at TEXT NOT NULL,
|
||||||
|
applied_steps_count INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO "_prisma_migrations"
|
||||||
|
(id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count)
|
||||||
|
VALUES
|
||||||
|
(?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
).run(
|
||||||
|
"m1",
|
||||||
|
"checksum",
|
||||||
|
new Date("2024-01-04T00:00:00.000Z").toISOString(),
|
||||||
|
"20240104000000_initial",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Date("2024-01-04T00:00:00.000Z").toISOString(),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Import compatibility (legacy exports)", () => {
|
||||||
|
const uploadsDir = path.resolve(__dirname, "../../uploads");
|
||||||
|
const userAgent = "vitest-import-compat";
|
||||||
|
let prisma: ReturnType<typeof getTestPrisma>;
|
||||||
|
let app: any;
|
||||||
|
let csrfHeaderName: string;
|
||||||
|
let csrfToken: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
setupTestDb();
|
||||||
|
prisma = getTestPrisma();
|
||||||
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||||
|
|
||||||
|
// Import the server AFTER DATABASE_URL is set by setupTestDb/getTestPrisma.
|
||||||
|
({ app } = await import("../index"));
|
||||||
|
|
||||||
|
const csrfRes = await request(app).get("/csrf-token").set("User-Agent", userAgent);
|
||||||
|
csrfHeaderName = csrfRes.body.header;
|
||||||
|
csrfToken = csrfRes.body.token;
|
||||||
|
expect(typeof csrfHeaderName).toBe("string");
|
||||||
|
expect(typeof csrfToken).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cleanupTestDb(prisma);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies a v0.1.x–v0.3.2-style SQLite export (Drawing/Collection tables) and returns migration info when present", async () => {
|
||||||
|
const legacyDb = createLegacySqliteDb({
|
||||||
|
tableStyle: "prisma",
|
||||||
|
includeCollections: true,
|
||||||
|
includeMigrationsTable: true,
|
||||||
|
includeTrashDrawing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/import/sqlite/legacy/verify")
|
||||||
|
.set("User-Agent", userAgent)
|
||||||
|
.set(csrfHeaderName, csrfToken)
|
||||||
|
.attach("db", legacyDb);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.valid).toBe(true);
|
||||||
|
expect(res.body.drawings).toBe(2);
|
||||||
|
expect(res.body.collections).toBe(1);
|
||||||
|
expect(res.body.latestMigration).toBe("20240104000000_initial");
|
||||||
|
expect(typeof res.body.currentLatestMigration === "string").toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merge-imports a legacy SQLite export into the current account without replacing the database", async () => {
|
||||||
|
const legacyDb = createLegacySqliteDb({
|
||||||
|
tableStyle: "prisma",
|
||||||
|
includeCollections: true,
|
||||||
|
includeMigrationsTable: false,
|
||||||
|
includeTrashDrawing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/import/sqlite/legacy")
|
||||||
|
.set("User-Agent", userAgent)
|
||||||
|
.set(csrfHeaderName, csrfToken)
|
||||||
|
.attach("db", legacyDb);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.success).toBe(true);
|
||||||
|
expect(res.body.collections?.created).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(res.body.drawings?.created).toBeGreaterThanOrEqual(3);
|
||||||
|
|
||||||
|
const importedDrawings = await prisma.drawing.findMany({
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
select: { id: true, name: true, collectionId: true, userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// In single-user mode, imports land on the bootstrap acting user.
|
||||||
|
expect(importedDrawings.every((d) => d.userId === "bootstrap-admin")).toBe(true);
|
||||||
|
expect(importedDrawings.map((d) => d.id)).toEqual(
|
||||||
|
expect.arrayContaining(["legacy-drawing-1", "legacy-drawing-2", "legacy-drawing-trash"])
|
||||||
|
);
|
||||||
|
|
||||||
|
const trash = await prisma.collection.findUnique({ where: { id: "trash" } });
|
||||||
|
expect(trash).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports older exports with plural/lowercase table names (drawings/collections)", async () => {
|
||||||
|
const legacyDb = createLegacySqliteDb({
|
||||||
|
tableStyle: "plural-lower",
|
||||||
|
includeCollections: true,
|
||||||
|
includeMigrationsTable: false,
|
||||||
|
includeTrashDrawing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const verify = await request(app)
|
||||||
|
.post("/import/sqlite/legacy/verify")
|
||||||
|
.set("User-Agent", userAgent)
|
||||||
|
.set(csrfHeaderName, csrfToken)
|
||||||
|
.attach("db", legacyDb);
|
||||||
|
|
||||||
|
expect(verify.status).toBe(200);
|
||||||
|
expect(verify.body.drawings).toBe(2);
|
||||||
|
expect(verify.body.collections).toBe(1);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/import/sqlite/legacy")
|
||||||
|
.set("User-Agent", userAgent)
|
||||||
|
.set(csrfHeaderName, csrfToken)
|
||||||
|
.attach("db", legacyDb);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails verification if the legacy DB is missing a Drawing table", async () => {
|
||||||
|
const dir = createTempDir();
|
||||||
|
const filePath = path.join(dir, "invalid.db");
|
||||||
|
const db = openWritableDb(filePath);
|
||||||
|
db.exec(`CREATE TABLE "NotDrawing" (id TEXT PRIMARY KEY NOT NULL);`);
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/import/sqlite/legacy/verify")
|
||||||
|
.set("User-Agent", userAgent)
|
||||||
|
.set(csrfHeaderName, csrfToken)
|
||||||
|
.attach("db", filePath);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body.error).toBe("Invalid legacy DB");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,9 +11,7 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import {
|
import {
|
||||||
getTestPrisma,
|
getTestPrisma,
|
||||||
cleanupTestDb,
|
|
||||||
setupTestDb,
|
setupTestDb,
|
||||||
createTestDrawingPayload,
|
|
||||||
} from "./testUtils";
|
} from "./testUtils";
|
||||||
import { PrismaClient } from "../generated/client";
|
import { PrismaClient } from "../generated/client";
|
||||||
|
|
||||||
|
|||||||
+114
-2067
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const registerSchema = z.object({
|
||||||
|
username: z.string().trim().min(3).max(50).optional(),
|
||||||
|
email: z.string().email().toLowerCase().trim(),
|
||||||
|
password: z.string().min(8).max(100),
|
||||||
|
name: z.string().trim().min(1).max(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loginSchema = z
|
||||||
|
.object({
|
||||||
|
identifier: z.string().trim().min(1).max(255).optional(),
|
||||||
|
email: z.string().email().toLowerCase().trim().optional(),
|
||||||
|
username: z.string().trim().min(1).max(255).optional(),
|
||||||
|
password: z.string(),
|
||||||
|
})
|
||||||
|
.refine((data) => Boolean(data.identifier || data.email || data.username), {
|
||||||
|
message: "identifier/email/username is required",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registrationToggleSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adminRoleUpdateSchema = z.object({
|
||||||
|
identifier: z.string().trim().min(1).max(255),
|
||||||
|
role: z.enum(["ADMIN", "USER"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const authEnabledToggleSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adminCreateUserSchema = z.object({
|
||||||
|
username: z.string().trim().min(3).max(50).optional(),
|
||||||
|
email: z.string().email().toLowerCase().trim(),
|
||||||
|
password: z.string().min(8).max(100),
|
||||||
|
name: z.string().trim().min(1).max(100),
|
||||||
|
role: z.enum(["ADMIN", "USER"]).optional(),
|
||||||
|
mustResetPassword: z.boolean().optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adminUpdateUserSchema = z.object({
|
||||||
|
username: z.string().trim().min(3).max(50).nullable().optional(),
|
||||||
|
name: z.string().trim().min(1).max(100).optional(),
|
||||||
|
role: z.enum(["ADMIN", "USER"]).optional(),
|
||||||
|
mustResetPassword: z.boolean().optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const impersonateSchema = z
|
||||||
|
.object({
|
||||||
|
userId: z.string().trim().min(1).optional(),
|
||||||
|
identifier: z.string().trim().min(1).optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => Boolean(data.userId || data.identifier), {
|
||||||
|
message: "userId/identifier is required",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loginRateLimitUpdateSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
windowMs: z.number().int().min(10_000).max(24 * 60 * 60 * 1000),
|
||||||
|
max: z.number().int().min(1).max(10_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loginRateLimitResetSchema = z.object({
|
||||||
|
identifier: z.string().trim().min(1).max(255),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const passwordResetRequestSchema = z.object({
|
||||||
|
email: z.string().email().toLowerCase().trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const passwordResetConfirmSchema = z.object({
|
||||||
|
token: z.string().min(1),
|
||||||
|
password: z.string().min(8).max(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateProfileSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateEmailSchema = z.object({
|
||||||
|
email: z.string().email().toLowerCase().trim(),
|
||||||
|
currentPassword: z.string().min(1).max(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const changePasswordSchema = z.object({
|
||||||
|
currentPassword: z.string(),
|
||||||
|
newPassword: z.string().min(8).max(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mustResetPasswordSchema = z.object({
|
||||||
|
newPassword: z.string().min(8).max(100),
|
||||||
|
});
|
||||||
@@ -24,14 +24,6 @@ interface Config {
|
|||||||
enableAuditLogging: boolean;
|
enableAuditLogging: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRequiredEnv = (key: string): string => {
|
|
||||||
const value = process.env[key];
|
|
||||||
if (!value || value.trim().length === 0) {
|
|
||||||
throw new Error(`Missing required environment variable: ${key}`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOptionalEnv = (key: string, defaultValue: string): string => {
|
const getOptionalEnv = (key: string, defaultValue: string): string => {
|
||||||
return process.env[key] || defaultValue;
|
return process.env[key] || defaultValue;
|
||||||
};
|
};
|
||||||
|
|||||||
+116
-1348
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
|||||||
/**
|
|
||||||
* Authentication middleware for protecting routes
|
|
||||||
*/
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
@@ -16,7 +13,7 @@ type AuthEnabledCache = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let authEnabledCache: AuthEnabledCache | null = null;
|
let authEnabledCache: AuthEnabledCache | null = null;
|
||||||
const AUTH_ENABLED_TTL_MS = 0;
|
const AUTH_ENABLED_TTL_MS = 5000;
|
||||||
|
|
||||||
const getAuthEnabled = async (): Promise<boolean> => {
|
const getAuthEnabled = async (): Promise<boolean> => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -24,16 +21,32 @@ const getAuthEnabled = async (): Promise<boolean> => {
|
|||||||
return authEnabledCache.value;
|
return authEnabledCache.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemConfig = await prisma.systemConfig.upsert({
|
let systemConfig = await prisma.systemConfig.findUnique({
|
||||||
where: { id: DEFAULT_SYSTEM_CONFIG_ID },
|
where: { id: DEFAULT_SYSTEM_CONFIG_ID },
|
||||||
update: {},
|
select: { authEnabled: true },
|
||||||
create: {
|
});
|
||||||
|
|
||||||
|
if (!systemConfig) {
|
||||||
|
try {
|
||||||
|
systemConfig = await prisma.systemConfig.create({
|
||||||
|
data: {
|
||||||
id: DEFAULT_SYSTEM_CONFIG_ID,
|
id: DEFAULT_SYSTEM_CONFIG_ID,
|
||||||
authEnabled: false,
|
authEnabled: false,
|
||||||
registrationEnabled: false,
|
registrationEnabled: false,
|
||||||
},
|
},
|
||||||
select: { authEnabled: true },
|
select: { authEnabled: true },
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
// Handle race from concurrent initialization.
|
||||||
|
systemConfig = await prisma.systemConfig.findUnique({
|
||||||
|
where: { id: DEFAULT_SYSTEM_CONFIG_ID },
|
||||||
|
select: { authEnabled: true },
|
||||||
|
});
|
||||||
|
if (!systemConfig) {
|
||||||
|
throw new Error("Failed to initialize system config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
authEnabledCache = { value: systemConfig.authEnabled, fetchedAt: now };
|
authEnabledCache = { value: systemConfig.authEnabled, fetchedAt: now };
|
||||||
return systemConfig.authEnabled;
|
return systemConfig.authEnabled;
|
||||||
@@ -102,9 +115,6 @@ interface JwtPayload {
|
|||||||
impersonatorId?: string;
|
impersonatorId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to check if decoded JWT is our expected payload structure
|
|
||||||
*/
|
|
||||||
const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
|
const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
|
||||||
if (typeof decoded !== "object" || decoded === null) {
|
if (typeof decoded !== "object" || decoded === null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -120,9 +130,6 @@ const isJwtPayload = (decoded: unknown): decoded is JwtPayload => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract JWT token from Authorization header
|
|
||||||
*/
|
|
||||||
const extractToken = (req: Request): string | null => {
|
const extractToken = (req: Request): string | null => {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (!authHeader || typeof authHeader !== "string") return null;
|
if (!authHeader || typeof authHeader !== "string") return null;
|
||||||
@@ -135,9 +142,6 @@ const extractToken = (req: Request): string | null => {
|
|||||||
return parts[1];
|
return parts[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify and decode JWT token
|
|
||||||
*/
|
|
||||||
const verifyToken = (token: string): JwtPayload | null => {
|
const verifyToken = (token: string): JwtPayload | null => {
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, config.jwtSecret);
|
const decoded = jwt.verify(token, config.jwtSecret);
|
||||||
@@ -170,10 +174,6 @@ const isAllowedWhileMustResetPassword = (req: Request): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Require authentication middleware
|
|
||||||
* Protects routes that require a valid JWT token
|
|
||||||
*/
|
|
||||||
export const requireAuth = async (
|
export const requireAuth = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -276,10 +276,6 @@ export const requireAuth = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional authentication middleware
|
|
||||||
* Attaches user to request if token is present, but doesn't require it
|
|
||||||
*/
|
|
||||||
export const optionalAuth = async (
|
export const optionalAuth = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-31
@@ -552,21 +552,6 @@ const CSRF_TOKEN_FUTURE_SKEW_MS = 5 * 60 * 1000; // 5 minutes clock skew toleran
|
|||||||
const CSRF_NONCE_BYTES = 16;
|
const CSRF_NONCE_BYTES = 16;
|
||||||
const CSRF_TOKEN_MAX_LENGTH = 2048; // sanity limit against abuse
|
const CSRF_TOKEN_MAX_LENGTH = 2048; // sanity limit against abuse
|
||||||
|
|
||||||
/**
|
|
||||||
* IMPORTANT (Horizontal Scaling / K8s)
|
|
||||||
* -----------------------------------
|
|
||||||
* CSRF tokens must validate across multiple stateless instances.
|
|
||||||
*
|
|
||||||
* The prior in-memory Map-based token store breaks under horizontal scaling
|
|
||||||
* because each pod has its own memory. This implementation is stateless:
|
|
||||||
*
|
|
||||||
* - Token payload: { ts, nonce }
|
|
||||||
* - Signature: HMAC_SHA256(secret, `${clientId}|${ts}|${nonce}`)
|
|
||||||
*
|
|
||||||
* As long as all pods share the same `CSRF_SECRET`, any pod can validate
|
|
||||||
* any token without shared state (works on Kubernetes).
|
|
||||||
*/
|
|
||||||
|
|
||||||
let cachedCsrfSecret: Buffer | null = null;
|
let cachedCsrfSecret: Buffer | null = null;
|
||||||
const getCsrfSecret = (): Buffer => {
|
const getCsrfSecret = (): Buffer => {
|
||||||
if (cachedCsrfSecret) return cachedCsrfSecret;
|
if (cachedCsrfSecret) return cachedCsrfSecret;
|
||||||
@@ -577,9 +562,7 @@ const getCsrfSecret = (): Buffer => {
|
|||||||
return cachedCsrfSecret;
|
return cachedCsrfSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not configured, generate an ephemeral secret for this process.
|
// Fallback for local/single-instance setups.
|
||||||
// This keeps single-instance deployments working out of the box, but:
|
|
||||||
// - Horizontal scaling will BREAK unless CSRF_SECRET is set and shared.
|
|
||||||
cachedCsrfSecret = crypto.randomBytes(32);
|
cachedCsrfSecret = crypto.randomBytes(32);
|
||||||
const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : "";
|
const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : "";
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -609,9 +592,7 @@ const base64UrlDecode = (input: string): Buffer => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CsrfTokenPayload = {
|
type CsrfTokenPayload = {
|
||||||
/** Issued-at timestamp (ms since epoch) */
|
|
||||||
ts: number;
|
ts: number;
|
||||||
/** Random nonce (base64url) */
|
|
||||||
nonce: string;
|
nonce: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -621,10 +602,6 @@ const signCsrfToken = (clientId: string, payload: CsrfTokenPayload): Buffer => {
|
|||||||
return crypto.createHmac("sha256", secret).update(data, "utf8").digest();
|
return crypto.createHmac("sha256", secret).update(data, "utf8").digest();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new CSRF token for a client
|
|
||||||
* Returns the token to be sent to the client
|
|
||||||
*/
|
|
||||||
export const createCsrfToken = (clientId: string): string => {
|
export const createCsrfToken = (clientId: string): string => {
|
||||||
const payload: CsrfTokenPayload = {
|
const payload: CsrfTokenPayload = {
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
@@ -638,10 +615,6 @@ export const createCsrfToken = (clientId: string): string => {
|
|||||||
return `${payloadB64}.${sigB64}`;
|
return `${payloadB64}.${sigB64}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a CSRF token for a client
|
|
||||||
* Uses timing-safe comparison to prevent timing attacks
|
|
||||||
*/
|
|
||||||
export const validateCsrfToken = (clientId: string, token: string): boolean => {
|
export const validateCsrfToken = (clientId: string, token: string): boolean => {
|
||||||
if (!token || typeof token !== "string") {
|
if (!token || typeof token !== "string") {
|
||||||
return false;
|
return false;
|
||||||
@@ -688,9 +661,6 @@ export const validateCsrfToken = (clientId: string, token: string): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke a CSRF token (e.g., on logout or token refresh)
|
|
||||||
*/
|
|
||||||
export const revokeCsrfToken = (clientId: string): void => {
|
export const revokeCsrfToken = (clientId: string): void => {
|
||||||
// Stateless CSRF tokens cannot be selectively revoked without shared state.
|
// Stateless CSRF tokens cannot be selectively revoked without shared state.
|
||||||
// If revocation is required, implement token blacklisting in a shared store
|
// If revocation is required, implement token blacklisting in a shared store
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user