diff --git a/backend/Dockerfile b/backend/Dockerfile index 53589ec..dea8931 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,7 +8,10 @@ COPY package*.json ./ COPY tsconfig.json ./ # Install dependencies -RUN npm ci +# Install build deps required for compiling native modules like better-sqlite3 +RUN apk add --no-cache python3 make g++ build-base sqlite-dev && \ + npm ci +ENV PYTHON=/usr/bin/python3 # Copy prisma schema COPY prisma ./prisma/ @@ -26,7 +29,7 @@ RUN npx tsc FROM node:20-alpine # Install OpenSSL for Prisma and su-exec, create non-root user -RUN apk add --no-cache openssl su-exec && \ +RUN apk add --no-cache openssl su-exec sqlite-libs && \ addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 diff --git a/backend/prisma/dev.db.backup b/backend/prisma/dev.db.backup index 9f71e00..00c5908 100644 Binary files a/backend/prisma/dev.db.backup and b/backend/prisma/dev.db.backup differ diff --git a/backend/prisma/dev.db.pre-migrate b/backend/prisma/dev.db.pre-migrate new file mode 100644 index 0000000..a64cf60 Binary files /dev/null and b/backend/prisma/dev.db.pre-migrate differ diff --git a/backend/prisma/migrations/20251126185156_add_private_vault/migration.sql b/backend/prisma/migrations/20251126185156_add_private_vault/migration.sql new file mode 100644 index 0000000..615330c --- /dev/null +++ b/backend/prisma/migrations/20251126185156_add_private_vault/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "PrivateVault" ( + "id" TEXT NOT NULL PRIMARY KEY DEFAULT 'vault', + "passwordHash" TEXT NOT NULL, + "salt" TEXT NOT NULL, + "hint" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Drawing" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "elements" TEXT NOT NULL, + "appState" TEXT NOT NULL, + "files" TEXT NOT NULL DEFAULT '{}', + "preview" TEXT, + "version" INTEGER NOT NULL DEFAULT 1, + "collectionId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + "encryptedData" TEXT, + "iv" TEXT, + CONSTRAINT "Drawing_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Drawing" ("appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version") SELECT "appState", "collectionId", "createdAt", "elements", "files", "id", "name", "preview", "updatedAt", "version" FROM "Drawing"; +DROP TABLE "Drawing"; +ALTER TABLE "new_Drawing" RENAME TO "Drawing"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/backend/prisma/prisma/dev.db b/backend/prisma/prisma/dev.db deleted file mode 100644 index 6d2ea4c..0000000 Binary files a/backend/prisma/prisma/dev.db and /dev/null differ diff --git a/backend/prisma/prisma/dev.db-journal b/backend/prisma/prisma/dev.db-journal deleted file mode 100644 index d187e71..0000000 Binary files a/backend/prisma/prisma/dev.db-journal and /dev/null differ diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 23da027..8fa63e9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -32,6 +32,21 @@ model Drawing { collection Collection? @relation(fields: [collectionId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + // Privacy/Encryption fields + isPrivate Boolean @default(false) + encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true + iv String? // Initialization vector for AES-GCM decryption +} + +// Singleton model for storing vault password hash and settings +model PrivateVault { + id String @id @default("vault") // Singleton pattern + passwordHash String // bcrypt hash for password verification + salt String // Salt for client-side key derivation (hex encoded) + hint String? // Optional password hint + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Library { diff --git a/backend/src/generated/client/edge.js b/backend/src/generated/client/edge.js index ded6d83..917782d 100644 --- a/backend/src/generated/client/edge.js +++ b/backend/src/generated/client/edge.js @@ -104,6 +104,18 @@ exports.Prisma.DrawingScalarFieldEnum = { version: 'version', collectionId: 'collectionId', createdAt: 'createdAt', + updatedAt: 'updatedAt', + isPrivate: 'isPrivate', + encryptedData: 'encryptedData', + iv: 'iv' +}; + +exports.Prisma.PrivateVaultScalarFieldEnum = { + id: 'id', + passwordHash: 'passwordHash', + salt: 'salt', + hint: 'hint', + createdAt: 'createdAt', updatedAt: 'updatedAt' }; @@ -128,6 +140,7 @@ exports.Prisma.NullsOrder = { exports.Prisma.ModelName = { Collection: 'Collection', Drawing: 'Drawing', + PrivateVault: 'PrivateVault', Library: 'Library' }; /** @@ -177,6 +190,7 @@ const config = { "db" ], "activeProvider": "sqlite", + "postinstall": false, "inlineDatasources": { "db": { "url": { @@ -185,13 +199,13 @@ 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\", \"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\nmodel Library {\n id String @id @default(\"default\") // Singleton pattern - use \"default\" ID\n items String @default(\"[]\") // Stored as JSON string array of library items\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", - "inlineSchemaHash": "a6a8e55dadb0695c66246575cd1a63af3afd912ce070b4798d2e56269d371c87", + "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 // Privacy/Encryption fields\n isPrivate Boolean @default(false)\n encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true\n iv String? // Initialization vector for AES-GCM decryption\n}\n\n// Singleton model for storing vault password hash and settings\nmodel PrivateVault {\n id String @id @default(\"vault\") // Singleton pattern\n passwordHash String // bcrypt hash for password verification\n salt String // Salt for client-side key derivation (hex encoded)\n hint String? // Optional password hint\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Library {\n id String @id @default(\"default\") // Singleton pattern - use \"default\" ID\n items String @default(\"[]\") // Stored as JSON string array of library items\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", + "inlineSchemaHash": "3cc99802d40f98b2ff8d3cb0c4f62ff9ae197917aed7f91357cb4cb402df6a7c", "copyEngine": true } config.dirname = '/' -config.runtimeDataModel = JSON.parse("{\"models\":{\"Collection\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"drawings\",\"kind\":\"object\",\"isList\":true,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Drawing\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[],\"relationToFields\":[],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Drawing\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"elements\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"appState\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"files\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"{}\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"preview\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"version\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":1,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collectionId\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":true,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collection\",\"kind\":\"object\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Collection\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[\"collectionId\"],\"relationToFields\":[\"id\"],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Library\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"default\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"items\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"[]\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Collection\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"drawings\",\"kind\":\"object\",\"isList\":true,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Drawing\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[],\"relationToFields\":[],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Drawing\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"elements\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"appState\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"files\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"{}\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"preview\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"version\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":1,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collectionId\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":true,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collection\",\"kind\":\"object\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Collection\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[\"collectionId\"],\"relationToFields\":[\"id\"],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true},{\"name\":\"isPrivate\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Boolean\",\"default\":false,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"encryptedData\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"iv\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"PrivateVault\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"vault\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"passwordHash\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"salt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"hint\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Library\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"default\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"items\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"[]\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}") defineDmmfProperty(exports.Prisma, config.runtimeDataModel) config.engineWasm = undefined diff --git a/backend/src/generated/client/index-browser.js b/backend/src/generated/client/index-browser.js index d303df9..9f4d295 100644 --- a/backend/src/generated/client/index-browser.js +++ b/backend/src/generated/client/index-browser.js @@ -136,6 +136,18 @@ exports.Prisma.DrawingScalarFieldEnum = { version: 'version', collectionId: 'collectionId', createdAt: 'createdAt', + updatedAt: 'updatedAt', + isPrivate: 'isPrivate', + encryptedData: 'encryptedData', + iv: 'iv' +}; + +exports.Prisma.PrivateVaultScalarFieldEnum = { + id: 'id', + passwordHash: 'passwordHash', + salt: 'salt', + hint: 'hint', + createdAt: 'createdAt', updatedAt: 'updatedAt' }; @@ -160,6 +172,7 @@ exports.Prisma.NullsOrder = { exports.Prisma.ModelName = { Collection: 'Collection', Drawing: 'Drawing', + PrivateVault: 'PrivateVault', Library: 'Library' }; diff --git a/backend/src/generated/client/index.d.ts b/backend/src/generated/client/index.d.ts index bca9643..4ff0c08 100644 --- a/backend/src/generated/client/index.d.ts +++ b/backend/src/generated/client/index.d.ts @@ -23,6 +23,11 @@ export type Collection = $Result.DefaultSelection * */ export type Drawing = $Result.DefaultSelection +/** + * Model PrivateVault + * + */ +export type PrivateVault = $Result.DefaultSelection /** * Model Library * @@ -172,6 +177,16 @@ export class PrismaClient< */ get drawing(): Prisma.DrawingDelegate; + /** + * `prisma.privateVault`: Exposes CRUD operations for the **PrivateVault** model. + * Example usage: + * ```ts + * // Fetch zero or more PrivateVaults + * const privateVaults = await prisma.privateVault.findMany() + * ``` + */ + get privateVault(): Prisma.PrivateVaultDelegate; + /** * `prisma.library`: Exposes CRUD operations for the **Library** model. * Example usage: @@ -624,6 +639,7 @@ export namespace Prisma { export const ModelName: { Collection: 'Collection', Drawing: 'Drawing', + PrivateVault: 'PrivateVault', Library: 'Library' }; @@ -640,7 +656,7 @@ export namespace Prisma { export type TypeMap = { meta: { - modelProps: "collection" | "drawing" | "library" + modelProps: "collection" | "drawing" | "privateVault" | "library" txIsolationLevel: Prisma.TransactionIsolationLevel } model: { @@ -784,6 +800,76 @@ export namespace Prisma { } } } + PrivateVault: { + payload: Prisma.$PrivateVaultPayload + fields: Prisma.PrivateVaultFieldRefs + operations: { + findUnique: { + args: Prisma.PrivateVaultFindUniqueArgs + result: $Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.PrivateVaultFindUniqueOrThrowArgs + result: $Utils.PayloadToResult + } + findFirst: { + args: Prisma.PrivateVaultFindFirstArgs + result: $Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.PrivateVaultFindFirstOrThrowArgs + result: $Utils.PayloadToResult + } + findMany: { + args: Prisma.PrivateVaultFindManyArgs + result: $Utils.PayloadToResult[] + } + create: { + args: Prisma.PrivateVaultCreateArgs + result: $Utils.PayloadToResult + } + createMany: { + args: Prisma.PrivateVaultCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.PrivateVaultCreateManyAndReturnArgs + result: $Utils.PayloadToResult[] + } + delete: { + args: Prisma.PrivateVaultDeleteArgs + result: $Utils.PayloadToResult + } + update: { + args: Prisma.PrivateVaultUpdateArgs + result: $Utils.PayloadToResult + } + deleteMany: { + args: Prisma.PrivateVaultDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.PrivateVaultUpdateManyArgs + result: BatchPayload + } + upsert: { + args: Prisma.PrivateVaultUpsertArgs + result: $Utils.PayloadToResult + } + aggregate: { + args: Prisma.PrivateVaultAggregateArgs + result: $Utils.Optional + } + groupBy: { + args: Prisma.PrivateVaultGroupByArgs + result: $Utils.Optional[] + } + count: { + args: Prisma.PrivateVaultCountArgs + result: $Utils.Optional | number + } + } + } Library: { payload: Prisma.$LibraryPayload fields: Prisma.LibraryFieldRefs @@ -2010,6 +2096,9 @@ export namespace Prisma { collectionId: string | null createdAt: Date | null updatedAt: Date | null + isPrivate: boolean | null + encryptedData: string | null + iv: string | null } export type DrawingMaxAggregateOutputType = { @@ -2023,6 +2112,9 @@ export namespace Prisma { collectionId: string | null createdAt: Date | null updatedAt: Date | null + isPrivate: boolean | null + encryptedData: string | null + iv: string | null } export type DrawingCountAggregateOutputType = { @@ -2036,6 +2128,9 @@ export namespace Prisma { collectionId: number createdAt: number updatedAt: number + isPrivate: number + encryptedData: number + iv: number _all: number } @@ -2059,6 +2154,9 @@ export namespace Prisma { collectionId?: true createdAt?: true updatedAt?: true + isPrivate?: true + encryptedData?: true + iv?: true } export type DrawingMaxAggregateInputType = { @@ -2072,6 +2170,9 @@ export namespace Prisma { collectionId?: true createdAt?: true updatedAt?: true + isPrivate?: true + encryptedData?: true + iv?: true } export type DrawingCountAggregateInputType = { @@ -2085,6 +2186,9 @@ export namespace Prisma { collectionId?: true createdAt?: true updatedAt?: true + isPrivate?: true + encryptedData?: true + iv?: true _all?: true } @@ -2185,6 +2289,9 @@ export namespace Prisma { collectionId: string | null createdAt: Date updatedAt: Date + isPrivate: boolean + encryptedData: string | null + iv: string | null _count: DrawingCountAggregateOutputType | null _avg: DrawingAvgAggregateOutputType | null _sum: DrawingSumAggregateOutputType | null @@ -2217,6 +2324,9 @@ export namespace Prisma { collectionId?: boolean createdAt?: boolean updatedAt?: boolean + isPrivate?: boolean + encryptedData?: boolean + iv?: boolean collection?: boolean | Drawing$collectionArgs }, ExtArgs["result"]["drawing"]> @@ -2231,6 +2341,9 @@ export namespace Prisma { collectionId?: boolean createdAt?: boolean updatedAt?: boolean + isPrivate?: boolean + encryptedData?: boolean + iv?: boolean collection?: boolean | Drawing$collectionArgs }, ExtArgs["result"]["drawing"]> @@ -2245,6 +2358,9 @@ export namespace Prisma { collectionId?: boolean createdAt?: boolean updatedAt?: boolean + isPrivate?: boolean + encryptedData?: boolean + iv?: boolean } export type DrawingInclude = { @@ -2270,6 +2386,9 @@ export namespace Prisma { collectionId: string | null createdAt: Date updatedAt: Date + isPrivate: boolean + encryptedData: string | null + iv: string | null }, ExtArgs["result"]["drawing"]> composites: {} } @@ -2674,6 +2793,9 @@ export namespace Prisma { readonly collectionId: FieldRef<"Drawing", 'String'> readonly createdAt: FieldRef<"Drawing", 'DateTime'> readonly updatedAt: FieldRef<"Drawing", 'DateTime'> + readonly isPrivate: FieldRef<"Drawing", 'Boolean'> + readonly encryptedData: FieldRef<"Drawing", 'String'> + readonly iv: FieldRef<"Drawing", 'String'> } @@ -3019,6 +3141,894 @@ export namespace Prisma { } + /** + * Model PrivateVault + */ + + export type AggregatePrivateVault = { + _count: PrivateVaultCountAggregateOutputType | null + _min: PrivateVaultMinAggregateOutputType | null + _max: PrivateVaultMaxAggregateOutputType | null + } + + export type PrivateVaultMinAggregateOutputType = { + id: string | null + passwordHash: string | null + salt: string | null + hint: string | null + createdAt: Date | null + updatedAt: Date | null + } + + export type PrivateVaultMaxAggregateOutputType = { + id: string | null + passwordHash: string | null + salt: string | null + hint: string | null + createdAt: Date | null + updatedAt: Date | null + } + + export type PrivateVaultCountAggregateOutputType = { + id: number + passwordHash: number + salt: number + hint: number + createdAt: number + updatedAt: number + _all: number + } + + + export type PrivateVaultMinAggregateInputType = { + id?: true + passwordHash?: true + salt?: true + hint?: true + createdAt?: true + updatedAt?: true + } + + export type PrivateVaultMaxAggregateInputType = { + id?: true + passwordHash?: true + salt?: true + hint?: true + createdAt?: true + updatedAt?: true + } + + export type PrivateVaultCountAggregateInputType = { + id?: true + passwordHash?: true + salt?: true + hint?: true + createdAt?: true + updatedAt?: true + _all?: true + } + + export type PrivateVaultAggregateArgs = { + /** + * Filter which PrivateVault to aggregate. + */ + where?: PrivateVaultWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of PrivateVaults to fetch. + */ + orderBy?: PrivateVaultOrderByWithRelationInput | PrivateVaultOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: PrivateVaultWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` PrivateVaults from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` PrivateVaults. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned PrivateVaults + **/ + _count?: true | PrivateVaultCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: PrivateVaultMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: PrivateVaultMaxAggregateInputType + } + + export type GetPrivateVaultAggregateType = { + [P in keyof T & keyof AggregatePrivateVault]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : GetScalarType + : GetScalarType + } + + + + + export type PrivateVaultGroupByArgs = { + where?: PrivateVaultWhereInput + orderBy?: PrivateVaultOrderByWithAggregationInput | PrivateVaultOrderByWithAggregationInput[] + by: PrivateVaultScalarFieldEnum[] | PrivateVaultScalarFieldEnum + having?: PrivateVaultScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: PrivateVaultCountAggregateInputType | true + _min?: PrivateVaultMinAggregateInputType + _max?: PrivateVaultMaxAggregateInputType + } + + export type PrivateVaultGroupByOutputType = { + id: string + passwordHash: string + salt: string + hint: string | null + createdAt: Date + updatedAt: Date + _count: PrivateVaultCountAggregateOutputType | null + _min: PrivateVaultMinAggregateOutputType | null + _max: PrivateVaultMaxAggregateOutputType | null + } + + type GetPrivateVaultGroupByPayload = Prisma.PrismaPromise< + Array< + PickEnumerable & + { + [P in ((keyof T) & (keyof PrivateVaultGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : GetScalarType + : GetScalarType + } + > + > + + + export type PrivateVaultSelect = $Extensions.GetSelect<{ + id?: boolean + passwordHash?: boolean + salt?: boolean + hint?: boolean + createdAt?: boolean + updatedAt?: boolean + }, ExtArgs["result"]["privateVault"]> + + export type PrivateVaultSelectCreateManyAndReturn = $Extensions.GetSelect<{ + id?: boolean + passwordHash?: boolean + salt?: boolean + hint?: boolean + createdAt?: boolean + updatedAt?: boolean + }, ExtArgs["result"]["privateVault"]> + + export type PrivateVaultSelectScalar = { + id?: boolean + passwordHash?: boolean + salt?: boolean + hint?: boolean + createdAt?: boolean + updatedAt?: boolean + } + + + export type $PrivateVaultPayload = { + name: "PrivateVault" + objects: {} + scalars: $Extensions.GetPayloadResult<{ + id: string + passwordHash: string + salt: string + hint: string | null + createdAt: Date + updatedAt: Date + }, ExtArgs["result"]["privateVault"]> + composites: {} + } + + type PrivateVaultGetPayload = $Result.GetResult + + type PrivateVaultCountArgs = + Omit & { + select?: PrivateVaultCountAggregateInputType | true + } + + export interface PrivateVaultDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['PrivateVault'], meta: { name: 'PrivateVault' } } + /** + * Find zero or one PrivateVault that matches the filter. + * @param {PrivateVaultFindUniqueArgs} args - Arguments to find a PrivateVault + * @example + * // Get one PrivateVault + * const privateVault = await prisma.privateVault.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "findUnique"> | null, null, ExtArgs> + + /** + * Find one PrivateVault that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {PrivateVaultFindUniqueOrThrowArgs} args - Arguments to find a PrivateVault + * @example + * // Get one PrivateVault + * const privateVault = await prisma.privateVault.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "findUniqueOrThrow">, never, ExtArgs> + + /** + * Find the first PrivateVault that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultFindFirstArgs} args - Arguments to find a PrivateVault + * @example + * // Get one PrivateVault + * const privateVault = await prisma.privateVault.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "findFirst"> | null, null, ExtArgs> + + /** + * Find the first PrivateVault that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultFindFirstOrThrowArgs} args - Arguments to find a PrivateVault + * @example + * // Get one PrivateVault + * const privateVault = await prisma.privateVault.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "findFirstOrThrow">, never, ExtArgs> + + /** + * Find zero or more PrivateVaults that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all PrivateVaults + * const privateVaults = await prisma.privateVault.findMany() + * + * // Get first 10 PrivateVaults + * const privateVaults = await prisma.privateVault.findMany({ take: 10 }) + * + * // Only select the `id` + * const privateVaultWithIdOnly = await prisma.privateVault.findMany({ select: { id: true } }) + * + */ + findMany(args?: SelectSubset>): Prisma.PrismaPromise<$Result.GetResult, T, "findMany">> + + /** + * Create a PrivateVault. + * @param {PrivateVaultCreateArgs} args - Arguments to create a PrivateVault. + * @example + * // Create one PrivateVault + * const PrivateVault = await prisma.privateVault.create({ + * data: { + * // ... data to create a PrivateVault + * } + * }) + * + */ + create(args: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "create">, never, ExtArgs> + + /** + * Create many PrivateVaults. + * @param {PrivateVaultCreateManyArgs} args - Arguments to create many PrivateVaults. + * @example + * // Create many PrivateVaults + * const privateVault = await prisma.privateVault.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: SelectSubset>): Prisma.PrismaPromise + + /** + * Create many PrivateVaults and returns the data saved in the database. + * @param {PrivateVaultCreateManyAndReturnArgs} args - Arguments to create many PrivateVaults. + * @example + * // Create many PrivateVaults + * const privateVault = await prisma.privateVault.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many PrivateVaults and only return the `id` + * const privateVaultWithIdOnly = await prisma.privateVault.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: SelectSubset>): Prisma.PrismaPromise<$Result.GetResult, T, "createManyAndReturn">> + + /** + * Delete a PrivateVault. + * @param {PrivateVaultDeleteArgs} args - Arguments to delete one PrivateVault. + * @example + * // Delete one PrivateVault + * const PrivateVault = await prisma.privateVault.delete({ + * where: { + * // ... filter to delete one PrivateVault + * } + * }) + * + */ + delete(args: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "delete">, never, ExtArgs> + + /** + * Update one PrivateVault. + * @param {PrivateVaultUpdateArgs} args - Arguments to update one PrivateVault. + * @example + * // Update one PrivateVault + * const privateVault = await prisma.privateVault.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "update">, never, ExtArgs> + + /** + * Delete zero or more PrivateVaults. + * @param {PrivateVaultDeleteManyArgs} args - Arguments to filter PrivateVaults to delete. + * @example + * // Delete a few PrivateVaults + * const { count } = await prisma.privateVault.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more PrivateVaults. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many PrivateVaults + * const privateVault = await prisma.privateVault.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: SelectSubset>): Prisma.PrismaPromise + + /** + * Create or update one PrivateVault. + * @param {PrivateVaultUpsertArgs} args - Arguments to update or create a PrivateVault. + * @example + * // Update or create a PrivateVault + * const privateVault = await prisma.privateVault.upsert({ + * create: { + * // ... data to create a PrivateVault + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the PrivateVault we want to update + * } + * }) + */ + upsert(args: SelectSubset>): Prisma__PrivateVaultClient<$Result.GetResult, T, "upsert">, never, ExtArgs> + + + /** + * Count the number of PrivateVaults. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultCountArgs} args - Arguments to filter PrivateVaults to count. + * @example + * // Count the number of PrivateVaults + * const count = await prisma.privateVault.count({ + * where: { + * // ... the filter for the PrivateVaults we want to count + * } + * }) + **/ + count( + args?: Subset, + ): Prisma.PrismaPromise< + T extends $Utils.Record<'select', any> + ? T['select'] extends true + ? number + : GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a PrivateVault. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Subset): Prisma.PrismaPromise> + + /** + * Group by PrivateVault. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PrivateVaultGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends PrivateVaultGroupByArgs, + HasSelectOrTake extends Or< + Extends<'skip', Keys>, + Extends<'take', Keys> + >, + OrderByArg extends True extends HasSelectOrTake + ? { orderBy: PrivateVaultGroupByArgs['orderBy'] } + : { orderBy?: PrivateVaultGroupByArgs['orderBy'] }, + OrderFields extends ExcludeUnderscoreKeys>>, + ByFields extends MaybeTupleToUnion, + ByValid extends Has, + HavingFields extends GetHavingFields, + HavingValid extends Has, + ByEmpty extends T['by'] extends never[] ? True : False, + InputErrors extends ByEmpty extends True + ? `Error: "by" must not be empty.` + : HavingValid extends False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Keys + ? 'orderBy' extends Keys + ? ByValid extends True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Keys + ? 'orderBy' extends Keys + ? ByValid extends True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: SubsetIntersection & InputErrors): {} extends InputErrors ? GetPrivateVaultGroupByPayload : Prisma.PrismaPromise + /** + * Fields of the PrivateVault model + */ + readonly fields: PrivateVaultFieldRefs; + } + + /** + * The delegate class that acts as a "Promise-like" for PrivateVault. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ + export interface Prisma__PrivateVaultClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): $Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): $Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): $Utils.JsPromise + } + + + + + /** + * Fields of the PrivateVault model + */ + interface PrivateVaultFieldRefs { + readonly id: FieldRef<"PrivateVault", 'String'> + readonly passwordHash: FieldRef<"PrivateVault", 'String'> + readonly salt: FieldRef<"PrivateVault", 'String'> + readonly hint: FieldRef<"PrivateVault", 'String'> + readonly createdAt: FieldRef<"PrivateVault", 'DateTime'> + readonly updatedAt: FieldRef<"PrivateVault", 'DateTime'> + } + + + // Custom InputTypes + /** + * PrivateVault findUnique + */ + export type PrivateVaultFindUniqueArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * Filter, which PrivateVault to fetch. + */ + where: PrivateVaultWhereUniqueInput + } + + /** + * PrivateVault findUniqueOrThrow + */ + export type PrivateVaultFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * Filter, which PrivateVault to fetch. + */ + where: PrivateVaultWhereUniqueInput + } + + /** + * PrivateVault findFirst + */ + export type PrivateVaultFindFirstArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * Filter, which PrivateVault to fetch. + */ + where?: PrivateVaultWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of PrivateVaults to fetch. + */ + orderBy?: PrivateVaultOrderByWithRelationInput | PrivateVaultOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for PrivateVaults. + */ + cursor?: PrivateVaultWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` PrivateVaults from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` PrivateVaults. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of PrivateVaults. + */ + distinct?: PrivateVaultScalarFieldEnum | PrivateVaultScalarFieldEnum[] + } + + /** + * PrivateVault findFirstOrThrow + */ + export type PrivateVaultFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * Filter, which PrivateVault to fetch. + */ + where?: PrivateVaultWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of PrivateVaults to fetch. + */ + orderBy?: PrivateVaultOrderByWithRelationInput | PrivateVaultOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for PrivateVaults. + */ + cursor?: PrivateVaultWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` PrivateVaults from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` PrivateVaults. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of PrivateVaults. + */ + distinct?: PrivateVaultScalarFieldEnum | PrivateVaultScalarFieldEnum[] + } + + /** + * PrivateVault findMany + */ + export type PrivateVaultFindManyArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * Filter, which PrivateVaults to fetch. + */ + where?: PrivateVaultWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of PrivateVaults to fetch. + */ + orderBy?: PrivateVaultOrderByWithRelationInput | PrivateVaultOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing PrivateVaults. + */ + cursor?: PrivateVaultWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` PrivateVaults from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` PrivateVaults. + */ + skip?: number + distinct?: PrivateVaultScalarFieldEnum | PrivateVaultScalarFieldEnum[] + } + + /** + * PrivateVault create + */ + export type PrivateVaultCreateArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * The data needed to create a PrivateVault. + */ + data: XOR + } + + /** + * PrivateVault createMany + */ + export type PrivateVaultCreateManyArgs = { + /** + * The data used to create many PrivateVaults. + */ + data: PrivateVaultCreateManyInput | PrivateVaultCreateManyInput[] + } + + /** + * PrivateVault createManyAndReturn + */ + export type PrivateVaultCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelectCreateManyAndReturn | null + /** + * The data used to create many PrivateVaults. + */ + data: PrivateVaultCreateManyInput | PrivateVaultCreateManyInput[] + } + + /** + * PrivateVault update + */ + export type PrivateVaultUpdateArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * The data needed to update a PrivateVault. + */ + data: XOR + /** + * Choose, which PrivateVault to update. + */ + where: PrivateVaultWhereUniqueInput + } + + /** + * PrivateVault updateMany + */ + export type PrivateVaultUpdateManyArgs = { + /** + * The data used to update PrivateVaults. + */ + data: XOR + /** + * Filter which PrivateVaults to update + */ + where?: PrivateVaultWhereInput + } + + /** + * PrivateVault upsert + */ + export type PrivateVaultUpsertArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * The filter to search for the PrivateVault to update in case it exists. + */ + where: PrivateVaultWhereUniqueInput + /** + * In case the PrivateVault found by the `where` argument doesn't exist, create a new PrivateVault with this data. + */ + create: XOR + /** + * In case the PrivateVault was found with the provided `where` argument, update it with this data. + */ + update: XOR + } + + /** + * PrivateVault delete + */ + export type PrivateVaultDeleteArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + /** + * Filter which PrivateVault to delete. + */ + where: PrivateVaultWhereUniqueInput + } + + /** + * PrivateVault deleteMany + */ + export type PrivateVaultDeleteManyArgs = { + /** + * Filter which PrivateVaults to delete + */ + where?: PrivateVaultWhereInput + } + + /** + * PrivateVault without action + */ + export type PrivateVaultDefaultArgs = { + /** + * Select specific fields to fetch from the PrivateVault + */ + select?: PrivateVaultSelect | null + } + + /** * Model Library */ @@ -3914,12 +4924,27 @@ export namespace Prisma { version: 'version', collectionId: 'collectionId', createdAt: 'createdAt', - updatedAt: 'updatedAt' + updatedAt: 'updatedAt', + isPrivate: 'isPrivate', + encryptedData: 'encryptedData', + iv: 'iv' }; export type DrawingScalarFieldEnum = (typeof DrawingScalarFieldEnum)[keyof typeof DrawingScalarFieldEnum] + export const PrivateVaultScalarFieldEnum: { + id: 'id', + passwordHash: 'passwordHash', + salt: 'salt', + hint: 'hint', + createdAt: 'createdAt', + updatedAt: 'updatedAt' + }; + + export type PrivateVaultScalarFieldEnum = (typeof PrivateVaultScalarFieldEnum)[keyof typeof PrivateVaultScalarFieldEnum] + + export const LibraryScalarFieldEnum: { id: 'id', items: 'items', @@ -3972,6 +4997,13 @@ export namespace Prisma { + /** + * Reference to a field of type 'Boolean' + */ + export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'> + + + /** * Reference to a field of type 'Float' */ @@ -4046,6 +5078,9 @@ export namespace Prisma { collectionId?: StringNullableFilter<"Drawing"> | string | null createdAt?: DateTimeFilter<"Drawing"> | Date | string updatedAt?: DateTimeFilter<"Drawing"> | Date | string + isPrivate?: BoolFilter<"Drawing"> | boolean + encryptedData?: StringNullableFilter<"Drawing"> | string | null + iv?: StringNullableFilter<"Drawing"> | string | null collection?: XOR | null } @@ -4060,6 +5095,9 @@ export namespace Prisma { collectionId?: SortOrderInput | SortOrder createdAt?: SortOrder updatedAt?: SortOrder + isPrivate?: SortOrder + encryptedData?: SortOrderInput | SortOrder + iv?: SortOrderInput | SortOrder collection?: CollectionOrderByWithRelationInput } @@ -4077,6 +5115,9 @@ export namespace Prisma { collectionId?: StringNullableFilter<"Drawing"> | string | null createdAt?: DateTimeFilter<"Drawing"> | Date | string updatedAt?: DateTimeFilter<"Drawing"> | Date | string + isPrivate?: BoolFilter<"Drawing"> | boolean + encryptedData?: StringNullableFilter<"Drawing"> | string | null + iv?: StringNullableFilter<"Drawing"> | string | null collection?: XOR | null }, "id"> @@ -4091,6 +5132,9 @@ export namespace Prisma { collectionId?: SortOrderInput | SortOrder createdAt?: SortOrder updatedAt?: SortOrder + isPrivate?: SortOrder + encryptedData?: SortOrderInput | SortOrder + iv?: SortOrderInput | SortOrder _count?: DrawingCountOrderByAggregateInput _avg?: DrawingAvgOrderByAggregateInput _max?: DrawingMaxOrderByAggregateInput @@ -4112,6 +5156,66 @@ export namespace Prisma { collectionId?: StringNullableWithAggregatesFilter<"Drawing"> | string | null createdAt?: DateTimeWithAggregatesFilter<"Drawing"> | Date | string updatedAt?: DateTimeWithAggregatesFilter<"Drawing"> | Date | string + isPrivate?: BoolWithAggregatesFilter<"Drawing"> | boolean + encryptedData?: StringNullableWithAggregatesFilter<"Drawing"> | string | null + iv?: StringNullableWithAggregatesFilter<"Drawing"> | string | null + } + + export type PrivateVaultWhereInput = { + AND?: PrivateVaultWhereInput | PrivateVaultWhereInput[] + OR?: PrivateVaultWhereInput[] + NOT?: PrivateVaultWhereInput | PrivateVaultWhereInput[] + id?: StringFilter<"PrivateVault"> | string + passwordHash?: StringFilter<"PrivateVault"> | string + salt?: StringFilter<"PrivateVault"> | string + hint?: StringNullableFilter<"PrivateVault"> | string | null + createdAt?: DateTimeFilter<"PrivateVault"> | Date | string + updatedAt?: DateTimeFilter<"PrivateVault"> | Date | string + } + + export type PrivateVaultOrderByWithRelationInput = { + id?: SortOrder + passwordHash?: SortOrder + salt?: SortOrder + hint?: SortOrderInput | SortOrder + createdAt?: SortOrder + updatedAt?: SortOrder + } + + export type PrivateVaultWhereUniqueInput = Prisma.AtLeast<{ + id?: string + AND?: PrivateVaultWhereInput | PrivateVaultWhereInput[] + OR?: PrivateVaultWhereInput[] + NOT?: PrivateVaultWhereInput | PrivateVaultWhereInput[] + passwordHash?: StringFilter<"PrivateVault"> | string + salt?: StringFilter<"PrivateVault"> | string + hint?: StringNullableFilter<"PrivateVault"> | string | null + createdAt?: DateTimeFilter<"PrivateVault"> | Date | string + updatedAt?: DateTimeFilter<"PrivateVault"> | Date | string + }, "id"> + + export type PrivateVaultOrderByWithAggregationInput = { + id?: SortOrder + passwordHash?: SortOrder + salt?: SortOrder + hint?: SortOrderInput | SortOrder + createdAt?: SortOrder + updatedAt?: SortOrder + _count?: PrivateVaultCountOrderByAggregateInput + _max?: PrivateVaultMaxOrderByAggregateInput + _min?: PrivateVaultMinOrderByAggregateInput + } + + export type PrivateVaultScalarWhereWithAggregatesInput = { + AND?: PrivateVaultScalarWhereWithAggregatesInput | PrivateVaultScalarWhereWithAggregatesInput[] + OR?: PrivateVaultScalarWhereWithAggregatesInput[] + NOT?: PrivateVaultScalarWhereWithAggregatesInput | PrivateVaultScalarWhereWithAggregatesInput[] + id?: StringWithAggregatesFilter<"PrivateVault"> | string + passwordHash?: StringWithAggregatesFilter<"PrivateVault"> | string + salt?: StringWithAggregatesFilter<"PrivateVault"> | string + hint?: StringNullableWithAggregatesFilter<"PrivateVault"> | string | null + createdAt?: DateTimeWithAggregatesFilter<"PrivateVault"> | Date | string + updatedAt?: DateTimeWithAggregatesFilter<"PrivateVault"> | Date | string } export type LibraryWhereInput = { @@ -4224,6 +5328,9 @@ export namespace Prisma { version?: number createdAt?: Date | string updatedAt?: Date | string + isPrivate?: boolean + encryptedData?: string | null + iv?: string | null collection?: CollectionCreateNestedOneWithoutDrawingsInput } @@ -4238,6 +5345,9 @@ export namespace Prisma { collectionId?: string | null createdAt?: Date | string updatedAt?: Date | string + isPrivate?: boolean + encryptedData?: string | null + iv?: string | null } export type DrawingUpdateInput = { @@ -4250,6 +5360,9 @@ export namespace Prisma { version?: IntFieldUpdateOperationsInput | number createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null collection?: CollectionUpdateOneWithoutDrawingsNestedInput } @@ -4264,6 +5377,9 @@ export namespace Prisma { collectionId?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null } export type DrawingCreateManyInput = { @@ -4277,6 +5393,9 @@ export namespace Prisma { collectionId?: string | null createdAt?: Date | string updatedAt?: Date | string + isPrivate?: boolean + encryptedData?: string | null + iv?: string | null } export type DrawingUpdateManyMutationInput = { @@ -4289,6 +5408,9 @@ export namespace Prisma { version?: IntFieldUpdateOperationsInput | number createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null } export type DrawingUncheckedUpdateManyInput = { @@ -4302,6 +5424,72 @@ export namespace Prisma { collectionId?: NullableStringFieldUpdateOperationsInput | string | null createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null + } + + export type PrivateVaultCreateInput = { + id?: string + passwordHash: string + salt: string + hint?: string | null + createdAt?: Date | string + updatedAt?: Date | string + } + + export type PrivateVaultUncheckedCreateInput = { + id?: string + passwordHash: string + salt: string + hint?: string | null + createdAt?: Date | string + updatedAt?: Date | string + } + + export type PrivateVaultUpdateInput = { + id?: StringFieldUpdateOperationsInput | string + passwordHash?: StringFieldUpdateOperationsInput | string + salt?: StringFieldUpdateOperationsInput | string + hint?: NullableStringFieldUpdateOperationsInput | string | null + createdAt?: DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + } + + export type PrivateVaultUncheckedUpdateInput = { + id?: StringFieldUpdateOperationsInput | string + passwordHash?: StringFieldUpdateOperationsInput | string + salt?: StringFieldUpdateOperationsInput | string + hint?: NullableStringFieldUpdateOperationsInput | string | null + createdAt?: DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + } + + export type PrivateVaultCreateManyInput = { + id?: string + passwordHash: string + salt: string + hint?: string | null + createdAt?: Date | string + updatedAt?: Date | string + } + + export type PrivateVaultUpdateManyMutationInput = { + id?: StringFieldUpdateOperationsInput | string + passwordHash?: StringFieldUpdateOperationsInput | string + salt?: StringFieldUpdateOperationsInput | string + hint?: NullableStringFieldUpdateOperationsInput | string | null + createdAt?: DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + } + + export type PrivateVaultUncheckedUpdateManyInput = { + id?: StringFieldUpdateOperationsInput | string + passwordHash?: StringFieldUpdateOperationsInput | string + salt?: StringFieldUpdateOperationsInput | string + hint?: NullableStringFieldUpdateOperationsInput | string | null + createdAt?: DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string } export type LibraryCreateInput = { @@ -4465,6 +5653,11 @@ export namespace Prisma { not?: NestedIntFilter<$PrismaModel> | number } + export type BoolFilter<$PrismaModel = never> = { + equals?: boolean | BooleanFieldRefInput<$PrismaModel> + not?: NestedBoolFilter<$PrismaModel> | boolean + } + export type CollectionNullableRelationFilter = { is?: CollectionWhereInput | null isNot?: CollectionWhereInput | null @@ -4486,6 +5679,9 @@ export namespace Prisma { collectionId?: SortOrder createdAt?: SortOrder updatedAt?: SortOrder + isPrivate?: SortOrder + encryptedData?: SortOrder + iv?: SortOrder } export type DrawingAvgOrderByAggregateInput = { @@ -4503,6 +5699,9 @@ export namespace Prisma { collectionId?: SortOrder createdAt?: SortOrder updatedAt?: SortOrder + isPrivate?: SortOrder + encryptedData?: SortOrder + iv?: SortOrder } export type DrawingMinOrderByAggregateInput = { @@ -4516,6 +5715,9 @@ export namespace Prisma { collectionId?: SortOrder createdAt?: SortOrder updatedAt?: SortOrder + isPrivate?: SortOrder + encryptedData?: SortOrder + iv?: SortOrder } export type DrawingSumOrderByAggregateInput = { @@ -4555,6 +5757,41 @@ export namespace Prisma { _max?: NestedIntFilter<$PrismaModel> } + export type BoolWithAggregatesFilter<$PrismaModel = never> = { + equals?: boolean | BooleanFieldRefInput<$PrismaModel> + not?: NestedBoolWithAggregatesFilter<$PrismaModel> | boolean + _count?: NestedIntFilter<$PrismaModel> + _min?: NestedBoolFilter<$PrismaModel> + _max?: NestedBoolFilter<$PrismaModel> + } + + export type PrivateVaultCountOrderByAggregateInput = { + id?: SortOrder + passwordHash?: SortOrder + salt?: SortOrder + hint?: SortOrder + createdAt?: SortOrder + updatedAt?: SortOrder + } + + export type PrivateVaultMaxOrderByAggregateInput = { + id?: SortOrder + passwordHash?: SortOrder + salt?: SortOrder + hint?: SortOrder + createdAt?: SortOrder + updatedAt?: SortOrder + } + + export type PrivateVaultMinOrderByAggregateInput = { + id?: SortOrder + passwordHash?: SortOrder + salt?: SortOrder + hint?: SortOrder + createdAt?: SortOrder + updatedAt?: SortOrder + } + export type LibraryCountOrderByAggregateInput = { id?: SortOrder items?: SortOrder @@ -4644,6 +5881,10 @@ export namespace Prisma { divide?: number } + export type BoolFieldUpdateOperationsInput = { + set?: boolean + } + export type CollectionUpdateOneWithoutDrawingsNestedInput = { create?: XOR connectOrCreate?: CollectionCreateOrConnectWithoutDrawingsInput @@ -4735,6 +5976,11 @@ export namespace Prisma { not?: NestedStringNullableFilter<$PrismaModel> | string | null } + export type NestedBoolFilter<$PrismaModel = never> = { + equals?: boolean | BooleanFieldRefInput<$PrismaModel> + not?: NestedBoolFilter<$PrismaModel> | boolean + } + export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { equals?: string | StringFieldRefInput<$PrismaModel> | null in?: string[] | null @@ -4790,6 +6036,14 @@ export namespace Prisma { not?: NestedFloatFilter<$PrismaModel> | number } + export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = { + equals?: boolean | BooleanFieldRefInput<$PrismaModel> + not?: NestedBoolWithAggregatesFilter<$PrismaModel> | boolean + _count?: NestedIntFilter<$PrismaModel> + _min?: NestedBoolFilter<$PrismaModel> + _max?: NestedBoolFilter<$PrismaModel> + } + export type DrawingCreateWithoutCollectionInput = { id?: string name: string @@ -4800,6 +6054,9 @@ export namespace Prisma { version?: number createdAt?: Date | string updatedAt?: Date | string + isPrivate?: boolean + encryptedData?: string | null + iv?: string | null } export type DrawingUncheckedCreateWithoutCollectionInput = { @@ -4812,6 +6069,9 @@ export namespace Prisma { version?: number createdAt?: Date | string updatedAt?: Date | string + isPrivate?: boolean + encryptedData?: string | null + iv?: string | null } export type DrawingCreateOrConnectWithoutCollectionInput = { @@ -4853,6 +6113,9 @@ export namespace Prisma { collectionId?: StringNullableFilter<"Drawing"> | string | null createdAt?: DateTimeFilter<"Drawing"> | Date | string updatedAt?: DateTimeFilter<"Drawing"> | Date | string + isPrivate?: BoolFilter<"Drawing"> | boolean + encryptedData?: StringNullableFilter<"Drawing"> | string | null + iv?: StringNullableFilter<"Drawing"> | string | null } export type CollectionCreateWithoutDrawingsInput = { @@ -4909,6 +6172,9 @@ export namespace Prisma { version?: number createdAt?: Date | string updatedAt?: Date | string + isPrivate?: boolean + encryptedData?: string | null + iv?: string | null } export type DrawingUpdateWithoutCollectionInput = { @@ -4921,6 +6187,9 @@ export namespace Prisma { version?: IntFieldUpdateOperationsInput | number createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null } export type DrawingUncheckedUpdateWithoutCollectionInput = { @@ -4933,6 +6202,9 @@ export namespace Prisma { version?: IntFieldUpdateOperationsInput | number createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null } export type DrawingUncheckedUpdateManyWithoutCollectionInput = { @@ -4945,6 +6217,9 @@ export namespace Prisma { version?: IntFieldUpdateOperationsInput | number createdAt?: DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: DateTimeFieldUpdateOperationsInput | Date | string + isPrivate?: BoolFieldUpdateOperationsInput | boolean + encryptedData?: NullableStringFieldUpdateOperationsInput | string | null + iv?: NullableStringFieldUpdateOperationsInput | string | null } @@ -4964,6 +6239,10 @@ export namespace Prisma { * @deprecated Use DrawingDefaultArgs instead */ export type DrawingArgs = DrawingDefaultArgs + /** + * @deprecated Use PrivateVaultDefaultArgs instead + */ + export type PrivateVaultArgs = PrivateVaultDefaultArgs /** * @deprecated Use LibraryDefaultArgs instead */ diff --git a/backend/src/generated/client/index.js b/backend/src/generated/client/index.js index 96defd5..f17f403 100644 --- a/backend/src/generated/client/index.js +++ b/backend/src/generated/client/index.js @@ -105,6 +105,18 @@ exports.Prisma.DrawingScalarFieldEnum = { version: 'version', collectionId: 'collectionId', createdAt: 'createdAt', + updatedAt: 'updatedAt', + isPrivate: 'isPrivate', + encryptedData: 'encryptedData', + iv: 'iv' +}; + +exports.Prisma.PrivateVaultScalarFieldEnum = { + id: 'id', + passwordHash: 'passwordHash', + salt: 'salt', + hint: 'hint', + createdAt: 'createdAt', updatedAt: 'updatedAt' }; @@ -129,6 +141,7 @@ exports.Prisma.NullsOrder = { exports.Prisma.ModelName = { Collection: 'Collection', Drawing: 'Drawing', + PrivateVault: 'PrivateVault', Library: 'Library' }; /** @@ -178,6 +191,7 @@ const config = { "db" ], "activeProvider": "sqlite", + "postinstall": false, "inlineDatasources": { "db": { "url": { @@ -186,8 +200,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\", \"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\nmodel Library {\n id String @id @default(\"default\") // Singleton pattern - use \"default\" ID\n items String @default(\"[]\") // Stored as JSON string array of library items\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", - "inlineSchemaHash": "a6a8e55dadb0695c66246575cd1a63af3afd912ce070b4798d2e56269d371c87", + "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 // Privacy/Encryption fields\n isPrivate Boolean @default(false)\n encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true\n iv String? // Initialization vector for AES-GCM decryption\n}\n\n// Singleton model for storing vault password hash and settings\nmodel PrivateVault {\n id String @id @default(\"vault\") // Singleton pattern\n passwordHash String // bcrypt hash for password verification\n salt String // Salt for client-side key derivation (hex encoded)\n hint String? // Optional password hint\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Library {\n id String @id @default(\"default\") // Singleton pattern - use \"default\" ID\n items String @default(\"[]\") // Stored as JSON string array of library items\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", + "inlineSchemaHash": "3cc99802d40f98b2ff8d3cb0c4f62ff9ae197917aed7f91357cb4cb402df6a7c", "copyEngine": true } @@ -208,7 +222,7 @@ if (!fs.existsSync(path.join(__dirname, 'schema.prisma'))) { config.isBundled = true } -config.runtimeDataModel = JSON.parse("{\"models\":{\"Collection\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"drawings\",\"kind\":\"object\",\"isList\":true,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Drawing\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[],\"relationToFields\":[],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Drawing\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"elements\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"appState\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"files\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"{}\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"preview\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"version\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":1,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collectionId\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":true,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collection\",\"kind\":\"object\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Collection\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[\"collectionId\"],\"relationToFields\":[\"id\"],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Library\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"default\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"items\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"[]\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Collection\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"drawings\",\"kind\":\"object\",\"isList\":true,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Drawing\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[],\"relationToFields\":[],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Drawing\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":{\"name\":\"uuid(4)\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"name\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"elements\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"appState\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"files\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"{}\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"preview\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"version\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Int\",\"default\":1,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collectionId\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":true,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"collection\",\"kind\":\"object\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"Collection\",\"relationName\":\"CollectionToDrawing\",\"relationFromFields\":[\"collectionId\"],\"relationToFields\":[\"id\"],\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true},{\"name\":\"isPrivate\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"Boolean\",\"default\":false,\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"encryptedData\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"iv\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"PrivateVault\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"vault\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"passwordHash\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"salt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"hint\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":false,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"String\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false},\"Library\":{\"dbName\":null,\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":true,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"default\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"items\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"String\",\"default\":\"[]\",\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":true,\"type\":\"DateTime\",\"default\":{\"name\":\"now\",\"args\":[]},\"isGenerated\":false,\"isUpdatedAt\":false},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"isList\":false,\"isRequired\":true,\"isUnique\":false,\"isId\":false,\"isReadOnly\":false,\"hasDefaultValue\":false,\"type\":\"DateTime\",\"isGenerated\":false,\"isUpdatedAt\":true}],\"primaryKey\":null,\"uniqueFields\":[],\"uniqueIndexes\":[],\"isGenerated\":false}},\"enums\":{},\"types\":{}}") defineDmmfProperty(exports.Prisma, config.runtimeDataModel) config.engineWasm = undefined diff --git a/backend/src/generated/client/package.json b/backend/src/generated/client/package.json index b653243..57b1d9f 100644 --- a/backend/src/generated/client/package.json +++ b/backend/src/generated/client/package.json @@ -1,5 +1,5 @@ { - "name": "prisma-client-8eed3ee5004eaec649fc60571177778f25acb4a3cdc2c238bbb8e70dd820d0ff", + "name": "prisma-client-2894d2d911743eb6e6a7a673efae7513c10b6ce609edeb9caf76ed42f4728682", "main": "index.js", "types": "index.d.ts", "browser": "index-browser.js", diff --git a/backend/src/generated/client/schema.prisma b/backend/src/generated/client/schema.prisma index 23da027..8fa63e9 100644 --- a/backend/src/generated/client/schema.prisma +++ b/backend/src/generated/client/schema.prisma @@ -32,6 +32,21 @@ model Drawing { collection Collection? @relation(fields: [collectionId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + // Privacy/Encryption fields + isPrivate Boolean @default(false) + encryptedData String? // Encrypted blob containing elements, appState, files when isPrivate=true + iv String? // Initialization vector for AES-GCM decryption +} + +// Singleton model for storing vault password hash and settings +model PrivateVault { + id String @id @default("vault") // Singleton pattern + passwordHash String // bcrypt hash for password verification + salt String // Salt for client-side key derivation (hex encoded) + hint String? // Optional password hint + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Library { diff --git a/backend/src/generated/client/wasm.js b/backend/src/generated/client/wasm.js index d303df9..9f4d295 100644 --- a/backend/src/generated/client/wasm.js +++ b/backend/src/generated/client/wasm.js @@ -136,6 +136,18 @@ exports.Prisma.DrawingScalarFieldEnum = { version: 'version', collectionId: 'collectionId', createdAt: 'createdAt', + updatedAt: 'updatedAt', + isPrivate: 'isPrivate', + encryptedData: 'encryptedData', + iv: 'iv' +}; + +exports.Prisma.PrivateVaultScalarFieldEnum = { + id: 'id', + passwordHash: 'passwordHash', + salt: 'salt', + hint: 'hint', + createdAt: 'createdAt', updatedAt: 'updatedAt' }; @@ -160,6 +172,7 @@ exports.Prisma.NullsOrder = { exports.Prisma.ModelName = { Collection: 'Collection', Drawing: 'Drawing', + PrivateVault: 'PrivateVault', Library: 'Library' }; diff --git a/backend/src/index.ts b/backend/src/index.ts index b7b3bf4..a0bf802 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -10,6 +10,7 @@ import { Worker } from "worker_threads"; import multer from "multer"; import archiver from "archiver"; import { z } from "zod"; +import * as crypto from "crypto"; // @ts-ignore import { PrismaClient } from "./generated/client"; import { @@ -481,13 +482,350 @@ app.get("/health", (req, res) => { res.status(200).json({ status: "ok" }); }); +// --- Private Vault --- + +// Hash password using scrypt (similar to bcrypt but built-in to Node.js) +const hashPasswordServer = ( + password: string, + salt?: string +): Promise<{ hash: string; salt: string }> => { + return new Promise((resolve, reject) => { + const useSalt = salt || crypto.randomBytes(16).toString("hex"); + crypto.scrypt(password, useSalt, 64, (err, derivedKey) => { + if (err) reject(err); + resolve({ hash: derivedKey.toString("hex"), salt: useSalt }); + }); + }); +}; + +const verifyPasswordServer = ( + password: string, + hash: string, + salt: string +): Promise => { + return new Promise((resolve, reject) => { + crypto.scrypt(password, salt, 64, (err, derivedKey) => { + if (err) reject(err); + resolve(derivedKey.toString("hex") === hash); + }); + }); +}; + +// GET /vault/status - Check if vault is set up +app.get("/vault/status", async (req, res) => { + try { + const vault = await prisma.privateVault.findUnique({ + where: { id: "vault" }, + }); + + const privateDrawingsCount = await prisma.drawing.count({ + where: { isPrivate: true }, + }); + + if (!vault) { + return res.json({ + isSetup: false, + privateDrawingsCount, + }); + } + + res.json({ + isSetup: true, + salt: vault.salt, + hint: vault.hint, + privateDrawingsCount, + }); + } catch (error) { + console.error("Failed to get vault status:", error); + res.status(500).json({ error: "Failed to get vault status" }); + } +}); + +// POST /vault/setup - Create vault with password +app.post("/vault/setup", async (req, res) => { + try { + const { passwordHash, salt, hint } = req.body; + + if (!passwordHash || !salt) { + return res + .status(400) + .json({ error: "Password hash and salt are required" }); + } + + // Check if vault already exists + const existing = await prisma.privateVault.findUnique({ + where: { id: "vault" }, + }); + + if (existing) { + return res.status(400).json({ error: "Vault already exists" }); + } + + // Hash the client-side hash again for extra security + const { hash: serverHash, salt: serverSalt } = await hashPasswordServer( + passwordHash + ); + + await prisma.privateVault.create({ + data: { + id: "vault", + passwordHash: `${serverHash}:${serverSalt}`, // Store both server hash and server salt + salt, // Client-side salt for key derivation + hint: hint || null, + }, + }); + + res.json({ success: true }); + } catch (error) { + console.error("Failed to setup vault:", error); + res.status(500).json({ error: "Failed to setup vault" }); + } +}); + +// POST /vault/verify - Verify password and return salt for decryption +app.post("/vault/verify", async (req, res) => { + try { + const { password } = req.body; + + if (!password) { + return res.status(400).json({ error: "Password is required" }); + } + + const vault = await prisma.privateVault.findUnique({ + where: { id: "vault" }, + }); + + if (!vault) { + return res.status(404).json({ error: "Vault not set up" }); + } + + // Parse stored hash format: "serverHash:serverSalt" + const [storedHash, serverSalt] = vault.passwordHash.split(":"); + + // Client sends SHA-256 hash of password, we verify against our scrypt hash + const isValid = await verifyPasswordServer( + password, + storedHash, + serverSalt + ); + + if (!isValid) { + return res + .status(401) + .json({ success: false, error: "Invalid password" }); + } + + res.json({ + success: true, + salt: vault.salt, // Return client-side salt for key derivation + }); + } catch (error) { + console.error("Failed to verify password:", error); + res.status(500).json({ error: "Failed to verify password" }); + } +}); + +// PUT /vault/password - Change vault password +app.put("/vault/password", async (req, res) => { + try { + const { passwordHash, salt } = req.body; + + if (!passwordHash || !salt) { + return res + .status(400) + .json({ error: "Password hash and salt are required" }); + } + + const vault = await prisma.privateVault.findUnique({ + where: { id: "vault" }, + }); + + if (!vault) { + return res.status(404).json({ error: "Vault not set up" }); + } + + // Hash the new client-side hash + const { hash: serverHash, salt: serverSalt } = await hashPasswordServer( + passwordHash + ); + + await prisma.privateVault.update({ + where: { id: "vault" }, + data: { + passwordHash: `${serverHash}:${serverSalt}`, + salt, + }, + }); + + res.json({ success: true }); + } catch (error) { + console.error("Failed to change password:", error); + res.status(500).json({ error: "Failed to change password" }); + } +}); + +// GET /vault/hint - Get password hint +app.get("/vault/hint", async (req, res) => { + try { + const vault = await prisma.privateVault.findUnique({ + where: { id: "vault" }, + }); + + if (!vault) { + return res.status(404).json({ error: "Vault not set up" }); + } + + res.json({ hint: vault.hint }); + } catch (error) { + console.error("Failed to get hint:", error); + res.status(500).json({ error: "Failed to get hint" }); + } +}); + +// PUT /vault/hint - Update password hint +app.put("/vault/hint", async (req, res) => { + try { + const { hint } = req.body; + + const vault = await prisma.privateVault.findUnique({ + where: { id: "vault" }, + }); + + if (!vault) { + return res.status(404).json({ error: "Vault not set up" }); + } + + await prisma.privateVault.update({ + where: { id: "vault" }, + data: { hint }, + }); + + res.json({ success: true }); + } catch (error) { + console.error("Failed to update hint:", error); + res.status(500).json({ error: "Failed to update hint" }); + } +}); + +// GET /drawings/private - Get all private drawings +app.get("/drawings/private", async (req, res) => { + try { + const drawings = await prisma.drawing.findMany({ + where: { isPrivate: true }, + orderBy: { updatedAt: "desc" }, + }); + + res.json(drawings); + } catch (error) { + console.error("Failed to get private drawings:", error); + res.status(500).json({ error: "Failed to get private drawings" }); + } +}); + +// PUT /drawings/:id/lock - Move drawing to private vault +app.put("/drawings/:id/lock", async (req, res) => { + try { + const { id } = req.params; + const { encryptedData, iv } = req.body; + + if (!encryptedData || !iv) { + return res + .status(400) + .json({ error: "Encrypted data and IV are required" }); + } + + const drawing = await prisma.drawing.findUnique({ + where: { id }, + }); + + if (!drawing) { + return res.status(404).json({ error: "Drawing not found" }); + } + + // Generate a locked preview + const lockedPreview = ` + + + + + + Private Drawing + `; + + await prisma.drawing.update({ + where: { id }, + data: { + isPrivate: true, + encryptedData, + iv, + elements: "[]", // Clear plaintext data + appState: "{}", + files: "{}", + preview: lockedPreview, + collectionId: null, // Remove from any collection + }, + }); + + res.json({ success: true }); + } catch (error) { + console.error("Failed to lock drawing:", error); + res.status(500).json({ error: "Failed to lock drawing" }); + } +}); + +// PUT /drawings/:id/unlock - Remove drawing from private vault +app.put("/drawings/:id/unlock", async (req, res) => { + try { + const { id } = req.params; + const { elements, appState, files, preview } = req.body; + + if (!elements || !appState) { + return res + .status(400) + .json({ error: "Elements and appState are required" }); + } + + const drawing = await prisma.drawing.findUnique({ + where: { id }, + }); + + if (!drawing) { + return res.status(404).json({ error: "Drawing not found" }); + } + + if (!drawing.isPrivate) { + return res.status(400).json({ error: "Drawing is not private" }); + } + + await prisma.drawing.update({ + where: { id }, + data: { + isPrivate: false, + encryptedData: null, + iv: null, + elements: JSON.stringify(elements), + appState: JSON.stringify(appState), + files: JSON.stringify(files || {}), + preview: preview || null, + }, + }); + + res.json({ success: true }); + } catch (error) { + console.error("Failed to unlock drawing:", error); + res.status(500).json({ error: "Failed to unlock drawing" }); + } +}); + // --- Drawings --- // GET /drawings app.get("/drawings", async (req, res) => { try { const { search, collectionId } = req.query; - const where: any = {}; + const where: any = { + isPrivate: false, // Exclude private drawings from regular listings + }; if (search) { where.name = { contains: String(search) }; @@ -534,6 +872,18 @@ app.get("/drawings/:id", async (req, res) => { return res.status(404).json({ error: "Drawing not found" }); } + // For private drawings, return encrypted data instead of parsed content + if (drawing.isPrivate) { + console.log("[API] Returning private drawing", { id }); + return res.json({ + ...drawing, + elements: [], // Empty for private drawings + appState: {}, + files: {}, + // encryptedData, iv, and isPrivate are already included + }); + } + console.log("[API] Returning drawing", { id, elementCount: (() => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9543943..9c1e993 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,19 +2,24 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { Dashboard } from './pages/Dashboard'; import { Editor } from './pages/Editor'; import { Settings } from './pages/Settings'; +import { PrivateDrawings } from './pages/PrivateDrawings'; import { ThemeProvider } from './context/ThemeContext'; +import { VaultProvider } from './context/VaultContext'; function App() { return ( - - - } /> - } /> - } /> - } /> - - + + + + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 0bd9856..9cf57f0 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,5 +1,10 @@ import axios from "axios"; -import type { Drawing, Collection } from "../types"; +import type { + Drawing, + Collection, + VaultStatus, + VaultVerifyResult, +} from "../types"; export const API_URL = import.meta.env.VITE_API_URL || "/api"; @@ -96,3 +101,91 @@ export const updateLibrary = async (items: any[]) => { const response = await api.put<{ items: any[] }>("/library", { items }); return response.data.items; }; + +// --- Private Vault --- + +export const getVaultStatus = async (): Promise => { + const response = await api.get("/vault/status"); + return response.data; +}; + +export const setupVault = async ( + passwordHash: string, + salt: string, + hint?: string +): Promise => { + await api.post("/vault/setup", { passwordHash, salt, hint }); +}; + +export const verifyVaultPassword = async ( + password: string +): Promise => { + const response = await api.post("/vault/verify", { + password, + }); + return response.data; +}; + +export const updateVaultHint = async (hint: string): Promise => { + await api.put("/vault/hint", { hint }); +}; + +export const getVaultHint = async (): Promise => { + const response = await api.get<{ hint: string | null }>("/vault/hint"); + return response.data.hint; +}; + +export const changeVaultPassword = async ( + newPasswordHash: string, + newSalt: string, + _oldKey: CryptoKey, + _newKey: CryptoKey +): Promise => { + // Note: The actual re-encryption of drawings happens client-side + // This endpoint just updates the password hash and salt + await api.put("/vault/password", { + passwordHash: newPasswordHash, + salt: newSalt, + }); +}; + +// --- Private Drawings --- + +export const getPrivateDrawings = async (): Promise => { + const response = await api.get("/drawings/private"); + return response.data.map(deserializeDrawing); +}; + +export const lockDrawing = async ( + id: string, + encryptedData: string, + iv: string +): Promise => { + await api.put(`/drawings/${id}/lock`, { encryptedData, iv }); +}; + +export const lockDrawingWithPreview = async ( + id: string, + encryptedData: string, + iv: string, + preview?: string +): Promise => { + const body: any = { encryptedData, iv }; + if (preview !== undefined) body.preview = preview; + await api.put(`/drawings/${id}/lock`, body); +}; + +export const unlockDrawing = async ( + id: string, + elements: any[], + appState: any, + files: any, + preview?: string +): Promise => { + await api.put(`/drawings/${id}/unlock`, { + elements, + appState, + files, + preview, + }); +}; diff --git a/frontend/src/components/ChangeVaultPassword.tsx b/frontend/src/components/ChangeVaultPassword.tsx new file mode 100644 index 0000000..323ce55 --- /dev/null +++ b/frontend/src/components/ChangeVaultPassword.tsx @@ -0,0 +1,222 @@ +import React, { useState } from 'react'; +import { X, Key, Eye, EyeOff, AlertCircle } from 'lucide-react'; +import { validatePasswordStrength } from '../utils/crypto'; + +interface ChangeVaultPasswordProps { + isOpen: boolean; + onClose: () => void; + onChangePassword: (oldPassword: string, newPassword: string) => Promise; +} + +export const ChangeVaultPassword: React.FC = ({ + isOpen, + onClose, + onChangePassword, +}) => { + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showOld, setShowOld] = useState(false); + const [showNew, setShowNew] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const strength = validatePasswordStrength(newPassword); + const passwordsMatch = confirmPassword === newPassword; + const canSubmit = oldPassword.length > 0 && newPassword.length > 0 && passwordsMatch && strength.isValid; + + const handleClose = () => { + setOldPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setShowOld(false); + setShowNew(false); + setShowConfirm(false); + setError(null); + onClose(); + }; + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!canSubmit) return; + + setIsLoading(true); + setError(null); + + try { + await onChangePassword(oldPassword, newPassword); + handleClose(); + } catch (err: any) { + setError(err instanceof Error ? err.message : 'Failed to change password'); + } finally { + setIsLoading(false); + } + }; + + const getStrengthColor = (score: number) => { + if (score <= 1) return 'bg-red-500'; + if (score === 2) return 'bg-orange-500'; + if (score === 3) return 'bg-yellow-500'; + return 'bg-green-500'; + }; + + const getStrengthText = (score: number) => { + if (score <= 1) return 'Weak'; + if (score === 2) return 'Fair'; + if (score === 3) return 'Good'; + return 'Strong'; + }; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Change Vault Password

+

Update your vault password

+
+
+ +
+ + {/* Content */} +
+ {error && ( +
+ + {error} +
+ )} + + {/* Old Password */} +
+ +
+ setOldPassword(e.target.value)} + placeholder="Enter your current password" + autoFocus + className="w-full px-4 py-3 pr-12 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-amber-500" + /> + +
+
+ + {/* New Password */} +
+ +
+ setNewPassword(e.target.value)} + placeholder="Enter your new password" + className="w-full px-4 py-3 pr-12 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-amber-500" + /> + +
+ + {/* Strength Indicator */} + {newPassword.length > 0 && ( +
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+

+ {getStrengthText(strength.score)} +

+ {strength.feedback.length > 0 && ( +
    + {strength.feedback.map((fb, i) => ( +
  • • {fb}
  • + ))} +
+ )} +
+ )} +
+ + {/* Confirm Password */} +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="Confirm your new password" + className={`w-full px-4 py-3 pr-12 bg-white dark:bg-neutral-800 border-2 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-amber-500 ${ + confirmPassword.length > 0 && !passwordsMatch ? 'border-red-500' : 'border-black dark:border-neutral-700' + }`} + /> + +
+
+ + {/* Actions */} +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/components/DrawingCard.tsx b/frontend/src/components/DrawingCard.tsx index b383d2d..497b041 100644 --- a/frontend/src/components/DrawingCard.tsx +++ b/frontend/src/components/DrawingCard.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; -import { PenTool, Trash2, FolderInput, ArrowRight, Check, Clock, Copy, Download } from 'lucide-react'; +import { PenTool, Trash2, FolderInput, ArrowRight, Check, Clock, Copy, Download, Lock } from 'lucide-react'; import type { Drawing, Collection } from '../types'; import { formatDistanceToNow } from 'date-fns'; import clsx from 'clsx'; @@ -24,6 +24,8 @@ interface DrawingCardProps { onDragStart?: (e: React.DragEvent, id: string) => void; onMouseDown?: (e: React.MouseEvent, id: string) => void; onPreviewGenerated?: (id: string, preview: string) => void; + onMoveToVault?: (id: string) => void; + isVaultSetup?: boolean; } const ContextMenuPortal: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -44,6 +46,8 @@ export const DrawingCard: React.FC = ({ onDragStart, onMouseDown, onPreviewGenerated, + onMoveToVault, + isVaultSetup = false, }) => { const [isRenaming, setIsRenaming] = useState(false); const [showMoveSubmenu, setShowMoveSubmenu] = useState(false); @@ -336,6 +340,18 @@ export const DrawingCard: React.FC = ({ Export + {isVaultSetup && onMoveToVault && ( + + )} +
+
+ + {/* Content */} +
+ {error && ( +
+ + {error} +
+ )} + + {/* Password */} +
+ +
+ setPassword(e.target.value)} + placeholder="Enter your password" + className="w-full px-4 py-3 pr-12 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + +
+ + {/* Strength Indicator */} + {password.length > 0 && ( +
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+

+ {getStrengthText(passwordStrength.score)} +

+ {passwordStrength.feedback.length > 0 && ( +
    + {passwordStrength.feedback.map((feedback, i) => ( +
  • • {feedback}
  • + ))} +
+ )} +
+ )} +
+ + {/* Confirm Password */} +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="Confirm your password" + className={`w-full px-4 py-3 pr-12 bg-white dark:bg-neutral-800 border-2 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 ${ + confirmPassword.length > 0 && !passwordsMatch + ? 'border-red-500' + : 'border-black dark:border-neutral-700' + }`} + /> + +
+ {confirmPassword.length > 0 && ( +
+ {passwordsMatch ? ( + <> + + Passwords match + + ) : ( + <> + + Passwords do not match + + )} +
+ )} +
+ + {/* Password Hint (Optional) */} +
+ + setHint(e.target.value)} + placeholder="A hint to help you remember" + className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+ + {/* Warning */} +
+

+ Important: There is no way to recover your password. If you forget it, + all private drawings will be permanently inaccessible. +

+
+ + {/* Actions */} +
+ + +
+ +
+ + ); +}; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 07dcd06..256a193 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,10 +1,11 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { LayoutGrid, Folder, Plus, Trash2, Edit2, Archive, FolderOpen, Settings as SettingsIcon } from 'lucide-react'; +import { LayoutGrid, Folder, Plus, Trash2, Edit2, Archive, FolderOpen, Settings as SettingsIcon, Lock, Unlock } from 'lucide-react'; import type { Collection } from '../types'; import clsx from 'clsx'; import { ConfirmModal } from './ConfirmModal'; import { Logo } from './Logo'; +import { useVault } from '../context/VaultContext'; interface SidebarProps { collections: Collection[]; @@ -14,6 +15,7 @@ interface SidebarProps { onEditCollection: (id: string, name: string) => void; onDeleteCollection: (id: string) => void; onDrop?: (e: React.DragEvent, collectionId: string | null) => void; + onDropToVault?: (e: React.DragEvent) => void; } interface SidebarItemProps { @@ -109,6 +111,98 @@ const SidebarItem: React.FC = ({ ); }; +// Private Folder Item Component +const PrivateFolderItem: React.FC<{ + isActive: boolean; + onSelectCollection: (id: string | null | undefined) => void; + onDropToVault?: (e: React.DragEvent) => void; +}> = ({ isActive, onSelectCollection, onDropToVault }) => { + const vault = useVault(); + const navigate = useNavigate(); + const [isDragOver, setIsDragOver] = useState(false); + + // Don't show if still loading + if (vault.isLoading) { + return null; + } + + const handleClick = () => { + // Always navigate to /private - that page handles setup/unlock flow + onSelectCollection('private'); + navigate('/private'); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // Show drag over state for any drag (the actual check happens on drop) + setIsDragOver(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + onDropToVault?.(e); + }; + + // Show different badge based on vault state + const getBadge = () => { + if (isDragOver) { + return ( + + Drop + + ); + } + if (!vault.isSetup) { + return ( + + Setup + + ); + } + if (vault.privateDrawingsCount > 0) { + return ( + + {vault.privateDrawingsCount} + + ); + } + return null; + }; + + return ( +
+ +
+ ); +}; export const Sidebar: React.FC = ({ @@ -118,7 +212,8 @@ export const Sidebar: React.FC = ({ onCreateCollection, onEditCollection, onDeleteCollection, - onDrop + onDrop, + onDropToVault }) => { const [isCreating, setIsCreating] = useState(false); const [newCollectionName, setNewCollectionName] = useState(''); @@ -206,6 +301,12 @@ export const Sidebar: React.FC = ({ onClick={() => onSelectCollection(null)} onDrop={onDrop} /> + +
diff --git a/frontend/src/components/UnlockVaultModal.tsx b/frontend/src/components/UnlockVaultModal.tsx new file mode 100644 index 0000000..db39a60 --- /dev/null +++ b/frontend/src/components/UnlockVaultModal.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import { X, Lock, Eye, EyeOff, AlertCircle, HelpCircle } from 'lucide-react'; + +interface UnlockVaultModalProps { + isOpen: boolean; + onClose: () => void; + onUnlock: (password: string) => Promise; + passwordHint?: string | null; +} + +export const UnlockVaultModal: React.FC = ({ + isOpen, + onClose, + onUnlock, + passwordHint, +}) => { + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showHint, setShowHint] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!password) return; + + setIsLoading(true); + setError(null); + + try { + const success = await onUnlock(password); + if (success) { + setPassword(''); + onClose(); + } else { + setError('Incorrect password'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to unlock vault'); + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + setPassword(''); + setError(null); + setShowHint(false); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Unlock Vault

+

Enter your password

+
+
+ +
+ + {/* Content */} +
+ {error && ( +
+ + {error} +
+ )} + + {/* Password */} +
+ +
+ setPassword(e.target.value)} + placeholder="Enter your password" + autoFocus + className="w-full px-4 py-3 pr-12 bg-white dark:bg-neutral-800 border-2 border-black dark:border-neutral-700 rounded-lg text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-amber-500" + /> + +
+
+ + {/* Password Hint */} + {passwordHint && ( +
+ + {showHint && ( +
+

{passwordHint}

+
+ )} +
+ )} + + {/* Actions */} +
+ + +
+
+
+
+ ); +}; diff --git a/frontend/src/context/VaultContext.tsx b/frontend/src/context/VaultContext.tsx new file mode 100644 index 0000000..b0fbbfd --- /dev/null +++ b/frontend/src/context/VaultContext.tsx @@ -0,0 +1,240 @@ +import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'; +import { deriveKey, hexToBytes, generateSalt, bytesToHex, hashPassword } from '../utils/crypto'; +import * as api from '../api'; + +interface VaultState { + isSetup: boolean; + isUnlocked: boolean; + isLoading: boolean; + passwordHint: string | null; + privateDrawingsCount: number; +} + +interface VaultContextType extends VaultState { + sessionKey: CryptoKey | null; + salt: Uint8Array | null; + checkVaultStatus: () => Promise; + unlock: (password: string) => Promise; + lock: () => void; + setupVault: (password: string, hint?: string) => Promise; + changePassword: (oldPassword: string, newPassword: string) => Promise; + updateHint: (hint: string) => Promise; +} + +const VaultContext = createContext(null); + +// Auto-lock timeout in milliseconds (15 minutes) +const AUTO_LOCK_TIMEOUT = 15 * 60 * 1000; + +export const VaultProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [state, setState] = useState({ + isSetup: false, + isUnlocked: false, + isLoading: true, + passwordHint: null, + privateDrawingsCount: 0, + }); + + const [sessionKey, setSessionKey] = useState(null); + const [salt, setSalt] = useState(null); + const autoLockTimer = useRef | null>(null); + + // Reset auto-lock timer on activity + const resetAutoLockTimer = useCallback(() => { + if (autoLockTimer.current) { + clearTimeout(autoLockTimer.current); + } + if (state.isUnlocked) { + autoLockTimer.current = setTimeout(() => { + lock(); + }, AUTO_LOCK_TIMEOUT); + } + }, [state.isUnlocked]); + + // Set up activity listeners for auto-lock + useEffect(() => { + if (state.isUnlocked) { + const handleActivity = () => resetAutoLockTimer(); + window.addEventListener('mousemove', handleActivity); + window.addEventListener('keydown', handleActivity); + window.addEventListener('click', handleActivity); + resetAutoLockTimer(); + + return () => { + window.removeEventListener('mousemove', handleActivity); + window.removeEventListener('keydown', handleActivity); + window.removeEventListener('click', handleActivity); + if (autoLockTimer.current) { + clearTimeout(autoLockTimer.current); + } + }; + } + }, [state.isUnlocked, resetAutoLockTimer]); + + // Check vault status on mount + const checkVaultStatus = useCallback(async () => { + try { + setState(prev => ({ ...prev, isLoading: true })); + const status = await api.getVaultStatus(); + setState(prev => ({ + ...prev, + isSetup: status.isSetup, + passwordHint: status.hint || null, + privateDrawingsCount: status.privateDrawingsCount || 0, + isLoading: false, + })); + if (status.salt) { + setSalt(hexToBytes(status.salt)); + } + } catch (error) { + console.error('Failed to check vault status:', error); + setState(prev => ({ ...prev, isLoading: false })); + } + }, []); + + useEffect(() => { + checkVaultStatus(); + }, [checkVaultStatus]); + + // Lock the vault + const lock = useCallback(() => { + setSessionKey(null); + setState(prev => ({ ...prev, isUnlocked: false })); + if (autoLockTimer.current) { + clearTimeout(autoLockTimer.current); + autoLockTimer.current = null; + } + }, []); + + // Unlock the vault with password + const unlock = useCallback(async (password: string): Promise => { + try { + // Hash the password the same way we did during setup + const passwordHash = await hashPassword(password); + + // First verify the password with the server + const result = await api.verifyVaultPassword(passwordHash); + + if (!result.success) { + return false; + } + + // Derive the encryption key client-side + const saltBytes = hexToBytes(result.salt); + const key = await deriveKey(password, saltBytes); + + setSalt(saltBytes); + setSessionKey(key); + setState(prev => ({ ...prev, isUnlocked: true })); + resetAutoLockTimer(); + + return true; + } catch (error) { + console.error('Failed to unlock vault:', error); + return false; + } + }, [resetAutoLockTimer]); + + // Setup the vault with initial password + const setupVault = useCallback(async (password: string, hint?: string): Promise => { + try { + // Generate a new salt + const newSalt = generateSalt(); + const saltHex = bytesToHex(newSalt); + + // Hash the password for server storage + const passwordHash = await hashPassword(password); + + // Create vault on server + await api.setupVault(passwordHash, saltHex, hint); + + // Derive the encryption key + const key = await deriveKey(password, newSalt); + + setSalt(newSalt); + setSessionKey(key); + setState(prev => ({ + ...prev, + isSetup: true, + isUnlocked: true, + passwordHint: hint || null, + })); + resetAutoLockTimer(); + } catch (error) { + console.error('Failed to setup vault:', error); + throw error; + } + }, [resetAutoLockTimer]); + + // Change vault password (requires re-encrypting all private drawings) + const changePassword = useCallback(async (oldPassword: string, newPassword: string): Promise => { + try { + // Hash the old password the same way we did during setup + const oldPasswordHash = await hashPassword(oldPassword); + + // Verify old password first + const verifyResult = await api.verifyVaultPassword(oldPasswordHash); + if (!verifyResult.success) { + throw new Error('Invalid current password'); + } + + // Derive old key for decryption + const oldSalt = hexToBytes(verifyResult.salt); + const oldKey = await deriveKey(oldPassword, oldSalt); + + // Generate new salt and derive new key + const newSalt = generateSalt(); + const newSaltHex = bytesToHex(newSalt); + const newKey = await deriveKey(newPassword, newSalt); + const newPasswordHash = await hashPassword(newPassword); + + // Re-encrypt all private drawings + await api.changeVaultPassword(newPasswordHash, newSaltHex, oldKey, newKey); + + // Update local state + setSalt(newSalt); + setSessionKey(newKey); + + } catch (error) { + console.error('Failed to change password:', error); + throw error; + } + }, []); + + // Update password hint + const updateHint = useCallback(async (hint: string): Promise => { + try { + await api.updateVaultHint(hint); + setState(prev => ({ ...prev, passwordHint: hint })); + } catch (error) { + console.error('Failed to update hint:', error); + throw error; + } + }, []); + + return ( + + {children} + + ); +}; + +export const useVault = (): VaultContextType => { + const context = useContext(VaultContext); + if (!context) { + throw new Error('useVault must be used within a VaultProvider'); + } + return context; +}; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 2fd1be5..79e06f7 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -10,6 +10,9 @@ import { useDebounce } from '../hooks/useDebounce'; import clsx from 'clsx'; import { ConfirmModal } from '../components/ConfirmModal'; import { importDrawings } from '../utils/importUtils'; +import { useVault } from '../context/VaultContext'; +import { encryptDrawing, generateLockedPreview } from '../utils/crypto'; +import { UnlockVaultModal } from '../components/UnlockVaultModal'; type Point = { x: number; y: number }; @@ -102,6 +105,11 @@ export const Dashboard: React.FC = () => { }); const [isLoading, setIsLoading] = useState(false); + + // Vault state + const vault = useVault(); + const [drawingToMoveToVault, setDrawingToMoveToVault] = useState(null); + const [showVaultUnlockModal, setShowVaultUnlockModal] = useState(false); // navigate is already declared at the top const refreshData = useCallback(async () => { @@ -472,6 +480,106 @@ export const Dashboard: React.FC = () => { } }; + // Move drawing to private vault + const handleMoveToVault = useCallback(async (id: string) => { + // If vault isn't set up, redirect to settings + if (!vault.isSetup) { + navigate('/settings'); + return; + } + + // If vault isn't unlocked, show unlock modal + if (!vault.isUnlocked || !vault.sessionKey) { + setDrawingToMoveToVault(id); + setShowVaultUnlockModal(true); + return; + } + + // Proceed with encryption + await encryptAndMoveToVault(id); + }, [vault.isSetup, vault.isUnlocked, vault.sessionKey, navigate]); + + const encryptAndMoveToVault = useCallback(async (id: string) => { + if (!vault.sessionKey) return; + + const drawing = drawings.find(d => d.id === id); + if (!drawing) return; + + try { + // Encrypt the drawing data + const dataToEncrypt = { + elements: drawing.elements || [], + appState: drawing.appState || {}, + files: drawing.files || {}, + }; + + const { encryptedData, iv } = await encryptDrawing(dataToEncrypt, vault.sessionKey); + const lockedPreview = generateLockedPreview(); + + // Update drawing to be private with encrypted data (include locked preview) + await api.lockDrawingWithPreview(id, encryptedData, iv, lockedPreview); + + // Remove from current view + setDrawings(prev => prev.filter(d => d.id !== id)); + vault.checkVaultStatus(); // Refresh vault status to update count + } catch (err) { + console.error("Failed to move to vault:", err); + } + }, [vault.sessionKey, drawings, vault]); + + const handleVaultUnlockForMove = useCallback(async (password: string) => { + const success = await vault.unlock(password); + if (success && drawingToMoveToVault) { + setShowVaultUnlockModal(false); + await encryptAndMoveToVault(drawingToMoveToVault); + setDrawingToMoveToVault(null); + } + return success; + }, [vault, drawingToMoveToVault, encryptAndMoveToVault]); + + // Handle dropping drawings to the vault + const handleDropToVault = useCallback(async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const draggedDrawingId = e.dataTransfer.getData('drawingId'); + if (!draggedDrawingId) return; + + // Collect IDs to move - if dragged item is selected, move all selected + let idsToMove: string[] = []; + if (selectedIds.has(draggedDrawingId)) { + idsToMove = Array.from(selectedIds); + } else { + idsToMove = [draggedDrawingId]; + } + + // If vault isn't set up, redirect to settings + if (!vault.isSetup) { + navigate('/settings'); + return; + } + + // If vault isn't unlocked, show unlock modal for the first drawing + if (!vault.isUnlocked || !vault.sessionKey) { + // Store first ID and show unlock modal + setDrawingToMoveToVault(idsToMove[0]); + setShowVaultUnlockModal(true); + // Note: For bulk moves when vault is locked, we only move the first one after unlock + // A more sophisticated approach would store all IDs, but this keeps it simple + return; + } + + // Move all drawings to vault + for (const id of idsToMove) { + await encryptAndMoveToVault(id); + } + + // Clear selection if we moved selected items + if (selectedIds.has(draggedDrawingId)) { + setSelectedIds(new Set()); + } + }, [selectedIds, vault.isSetup, vault.isUnlocked, vault.sessionKey, navigate, encryptAndMoveToVault]); + const handleBulkDuplicate = async () => { if (selectedIds.size === 0) return; @@ -635,6 +743,7 @@ export const Dashboard: React.FC = () => { onEditCollection={handleEditCollection} onDeleteCollection={handleDeleteCollection} onDrop={handleDrop} + onDropToVault={handleDropToVault} > {/* Drag Preview */}
{ drawing={drawing} collections={collections} isSelected={selectedIds.has(drawing.id)} + isTrash={isTrashView} onToggleSelection={(e) => handleToggleSelection(drawing.id, e)} onRename={handleRenameDrawing} onDelete={handleDeleteDrawing} @@ -927,6 +1037,8 @@ export const Dashboard: React.FC = () => { onMouseDown={handleCardMouseDown} onDragStart={handleCardDragStart} onPreviewGenerated={handlePreviewGenerated} + onMoveToVault={!isTrashView ? handleMoveToVault : undefined} + isVaultSetup={vault.isSetup} /> )) )} @@ -975,6 +1087,17 @@ export const Dashboard: React.FC = () => { onConfirm={() => setShowImportSuccess(false)} onCancel={() => setShowImportSuccess(false)} /> + + {/* Vault Unlock Modal for Move to Vault */} + { + setShowVaultUnlockModal(false); + setDrawingToMoveToVault(null); + }} + onUnlock={handleVaultUnlockForMove} + passwordHint={vault.passwordHint} + /> ); }; diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index b245534..4ed98fd 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, Download } from 'lucide-react'; +import { ArrowLeft, Download, Lock } from 'lucide-react'; import { Excalidraw, exportToSvg } from '@excalidraw/excalidraw'; import '@excalidraw/excalidraw/index.css'; import debounce from 'lodash/debounce'; @@ -10,9 +10,12 @@ import { io, Socket } from 'socket.io-client'; import { getUserIdentity } from '../utils/identity'; import { reconcileElements } from '../utils/sync'; import { exportFromEditor } from '../utils/exportUtils'; +import { encryptDrawing, decryptDrawing, generateLockedPreview } from '../utils/crypto'; import type { UserIdentity } from '../utils/identity'; import * as api from '../api'; import { useTheme } from '../context/ThemeContext'; +import { useVault } from '../context/VaultContext'; +import { UnlockVaultModal } from '../components/UnlockVaultModal'; interface Peer extends UserIdentity { isActive: boolean; @@ -51,12 +54,16 @@ export const Editor: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { theme } = useTheme(); + const vault = useVault(); const [drawingName, setDrawingName] = useState('Drawing Editor'); const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const [initialData, setInitialData] = useState(null); const [isSceneLoading, setIsSceneLoading] = useState(true); + const [isPrivateDrawing, setIsPrivateDrawing] = useState(false); + const [showUnlockModal, setShowUnlockModal] = useState(false); + const [pendingPrivateData, setPendingPrivateData] = useState(null); useEffect(() => { document.title = `${drawingName} - ExcaliDash`; @@ -334,13 +341,35 @@ export const Editor: React.FC = () => { elementCount: persistableElements.length, hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted), appState: persistableAppState, + isPrivate: isPrivateDrawing, }); - await api.updateDrawing(id, { - elements: persistableElements, - appState: persistableAppState, - files: latestFilesRef.current || {}, - }); + // Handle private drawings - encrypt before saving + if (isPrivateDrawing && vault.sessionKey) { + const dataToEncrypt = { + elements: persistableElements, + appState: persistableAppState, + files: latestFilesRef.current || {}, + }; + + const { encryptedData, iv } = await encryptDrawing(dataToEncrypt, vault.sessionKey); + + await api.updateDrawing(id, { + encryptedData, + iv, + // Don't save plaintext data for private drawings + elements: [], + appState: {}, + files: {}, + }); + } else { + // Normal drawing - save plaintext + await api.updateDrawing(id, { + elements: persistableElements, + appState: persistableAppState, + files: latestFilesRef.current || {}, + }); + } console.log("[Editor] Save complete", { drawingId: id }); } catch (err) { @@ -356,6 +385,14 @@ export const Editor: React.FC = () => { const currentSnapshot = latestElementsRef.current ?? elements; const currentFiles = latestFilesRef.current ?? files; + // For private drawings, generate a locked preview instead + if (isPrivateDrawing) { + const lockedPreview = generateLockedPreview(); + await api.updateDrawing(id, { preview: lockedPreview }); + console.log("[Editor] Locked preview saved", { drawingId: id }); + return; + } + // Generate preview const svg = await exportToSvg({ elements: currentSnapshot, @@ -448,6 +485,50 @@ export const Editor: React.FC = () => { // ------------------------------------------------------------------ // 2. DATA LOADING // ------------------------------------------------------------------ + + // Helper to decrypt and load private drawing data + const loadDecryptedDrawing = useCallback(async (data: any, libraryItems: any[]) => { + if (!vault.sessionKey || !data.encryptedData || !data.iv) { + toast.error("Cannot decrypt drawing - vault not unlocked"); + navigate('/'); + return; + } + + try { + const decrypted = await decryptDrawing(data.encryptedData, data.iv, vault.sessionKey); + + const elements = decrypted.elements || []; + const files = decrypted.files || {}; + latestElementsRef.current = elements; + latestFilesRef.current = files; + + elements.forEach((el: any) => { + recordElementVersion(el); + }); + + const persistedAppState = decrypted.appState || {}; + const hydratedAppState = { + ...persistedAppState, + viewBackgroundColor: persistedAppState.viewBackgroundColor ?? '#ffffff', + gridSize: persistedAppState.gridSize ?? null, + collaborators: new Map(), + }; + + setInitialData({ + elements, + appState: hydratedAppState, + files, + scrollToContent: true, + libraryItems, + }); + setIsSceneLoading(false); + } catch (err) { + console.error('Failed to decrypt drawing', err); + toast.error("Failed to decrypt drawing"); + navigate('/private'); + } + }, [vault.sessionKey, navigate, recordElementVersion]); + useEffect(() => { isBootstrappingScene.current = true; hasHydratedInitialScene.current = false; @@ -458,6 +539,8 @@ export const Editor: React.FC = () => { setIsReady(false); setIsSceneLoading(true); setInitialData(null); + setIsPrivateDrawing(false); + setPendingPrivateData(null); const loadData = async () => { if (!id) { @@ -476,7 +559,23 @@ export const Editor: React.FC = () => { ]); setDrawingName(data.name); - // Use elements directly without converting - they're already normalized during import + // Check if this is a private drawing + if (data.isPrivate) { + setIsPrivateDrawing(true); + + // If vault is not unlocked, show unlock modal + if (!vault.isUnlocked || !vault.sessionKey) { + setPendingPrivateData({ data, libraryItems }); + setShowUnlockModal(true); + return; + } + + // Decrypt and load the drawing + await loadDecryptedDrawing(data, libraryItems); + return; + } + + // Normal (non-private) drawing loading const elements = data.elements || []; const files = data.files || {}; latestElementsRef.current = elements; @@ -508,11 +607,24 @@ export const Editor: React.FC = () => { latestFilesRef.current = {}; setInitialData(buildEmptyScene()); } finally { - setIsSceneLoading(false); + if (!isPrivateDrawing) { + setIsSceneLoading(false); + } } }; loadData(); - }, [id, recordElementVersion, buildEmptyScene]); + }, [id, recordElementVersion, buildEmptyScene, vault.isUnlocked, vault.sessionKey, loadDecryptedDrawing]); + + // Handle vault unlock for pending private drawing + const handleVaultUnlock = useCallback(async (password: string) => { + const success = await vault.unlock(password); + if (success && pendingPrivateData) { + setShowUnlockModal(false); + await loadDecryptedDrawing(pendingPrivateData.data, pendingPrivateData.libraryItems); + setPendingPrivateData(null); + } + return success; + }, [vault, pendingPrivateData, loadDecryptedDrawing]); // ------------------------------------------------------------------ // 3. HANDLERS @@ -660,6 +772,14 @@ export const Editor: React.FC = () => { + {/* Private indicator */} + {isPrivateDrawing && ( +
+ + Private +
+ )} + {isRenaming ? (
{ )}
+ + {/* Unlock Modal for Private Drawings */} + { + setShowUnlockModal(false); + navigate('/'); + }} + onUnlock={handleVaultUnlock} + passwordHint={vault.passwordHint} + />
); }; diff --git a/frontend/src/pages/PrivateDrawings.tsx b/frontend/src/pages/PrivateDrawings.tsx new file mode 100644 index 0000000..21e9e6d --- /dev/null +++ b/frontend/src/pages/PrivateDrawings.tsx @@ -0,0 +1,532 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { createPortal } from 'react-dom'; +import { Layout } from '../components/Layout'; +import { useNavigate } from 'react-router-dom'; +import * as api from '../api'; +import type { Drawing, Collection } from '../types'; +import { Lock, Unlock, Loader2, Inbox, AlertCircle, MoreVertical, Trash2, UnlockKeyhole, PenTool, Clock, ShieldCheck } from 'lucide-react'; +import { ConfirmModal } from '../components/ConfirmModal'; +import { UnlockVaultModal } from '../components/UnlockVaultModal'; +import { PrivateVaultSetup } from '../components/PrivateVaultSetup'; +import { useVault } from '../context/VaultContext'; +import { decryptDrawing } from '../utils/crypto'; +import { formatDistanceToNow } from 'date-fns'; + +const ContextMenuPortal: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return createPortal(children, document.body); +}; + +// Simple private drawing card component +const PrivateDrawingCard: React.FC<{ + drawing: Drawing; + onClick: () => void; + onDelete: () => void; + onRemoveFromVault: () => void; + onRename: (id: string, name: string) => void; +}> = ({ drawing, onClick, onDelete, onRemoveFromVault, onRename }) => { + const [showMenu, setShowMenu] = useState(false); + const [isRenaming, setIsRenaming] = useState(false); + const [newName, setNewName] = useState(drawing.name); + const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); + + // Close context menu on click outside + useEffect(() => { + const handleClick = () => setContextMenu(null); + document.addEventListener('click', handleClick); + return () => document.removeEventListener('click', handleClick); + }, []); + + const handleRenameSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (newName.trim()) { + onRename(drawing.id, newName); + setIsRenaming(false); + } + }; + + const handleContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenu({ x: e.clientX, y: e.clientY }); + setShowMenu(false); + }; + + return ( + <> +
+ {/* Preview - Dark/black background for private drawings (no preview shown for privacy) */} +
+ {/* Lock indicator badge */} +
+ +
+ + {/* Menu button */} + + + {/* Dropdown menu */} + {showMenu && ( +
e.stopPropagation()} + > + + + +
+ )} +
+ + {/* Info */} +
+ {isRenaming ? ( + e.stopPropagation()} + onPointerDown={e => e.stopPropagation()} + onMouseDown={e => e.stopPropagation()} + > + setNewName(e.target.value)} + onBlur={() => setIsRenaming(false)} + onDragStart={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + className="w-full px-2 py-1 -ml-2 text-base font-bold text-slate-900 dark:text-white border-2 border-black dark:border-neutral-600 rounded-lg focus:outline-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] bg-white dark:bg-neutral-800" + /> + + ) : ( +

{ + e.stopPropagation(); + setIsRenaming(true); + }} + > + {drawing.name} +

+ )} +

+ + {formatDistanceToNow(drawing.updatedAt, { addSuffix: true })} +

+
+
+ + {/* Context Menu Portal */} + {contextMenu && ( + +
setContextMenu(null)} + onContextMenu={(e) => { e.preventDefault(); setContextMenu(null); }} + > +
e.stopPropagation()} + > + + + + +
+ + +
+
+
+ )} + + ); +}; + +export const PrivateDrawings: React.FC = () => { + const navigate = useNavigate(); + const vault = useVault(); + const [drawings, setDrawings] = useState([]); + const [collections, setCollections] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [showUnlockModal, setShowUnlockModal] = useState(false); + const [showSetupModal, setShowSetupModal] = useState(false); + const [modalDismissed, setModalDismissed] = useState(false); + const [error, setError] = useState(null); + const [drawingToUnlock, setDrawingToUnlock] = useState(null); + + // Fetch collections for sidebar + useEffect(() => { + const fetchCollections = async () => { + try { + const data = await api.getCollections(); + setCollections(data); + } catch (err) { + console.error('Failed to fetch collections:', err); + } + }; + fetchCollections(); + }, []); + + // Fetch private drawings when vault is unlocked + const fetchPrivateDrawings = useCallback(async () => { + if (!vault.isUnlocked || !vault.sessionKey) return; + + setIsLoading(true); + setError(null); + + try { + const privateDrawings = await api.getPrivateDrawings(); + + // Decrypt names for display (the actual content is decrypted when opening) + const decryptedDrawings = await Promise.all( + privateDrawings.map(async (drawing) => { + // For now, just use the stored name (names aren't encrypted yet) + // In a full implementation, you'd decrypt the name here + return drawing; + }) + ); + + setDrawings(decryptedDrawings); + } catch (err) { + console.error('Failed to fetch private drawings:', err); + setError('Failed to load private drawings'); + } finally { + setIsLoading(false); + } + }, [vault.isUnlocked, vault.sessionKey]); + + // Handle vault state changes + useEffect(() => { + if (vault.isUnlocked) { + fetchPrivateDrawings(); + setShowUnlockModal(false); + setModalDismissed(false); // Reset dismissed state when unlocked + } else if (!vault.isLoading && vault.isSetup && !modalDismissed) { + // Vault is setup but locked - auto-show unlock modal on first visit only + setShowUnlockModal(true); + } + }, [vault.isUnlocked, vault.isLoading, vault.isSetup, fetchPrivateDrawings, modalDismissed]); + + // Handle unlock modal close + const handleUnlockModalClose = useCallback(() => { + setShowUnlockModal(false); + setModalDismissed(true); // Prevent auto-reopening + }, []); + + const handleSelectCollection = (id: string | null | undefined) => { + if (id === undefined) navigate('/'); + else if (id === null) navigate('/collections?id=unorganized'); + else if (id === 'private') navigate('/private'); + else navigate(`/collections?id=${id}`); + }; + + const handleCreateCollection = async (name: string) => { + await api.createCollection(name); + const newCollections = await api.getCollections(); + setCollections(newCollections); + }; + + const handleEditCollection = async (id: string, name: string) => { + setCollections(prev => prev.map(c => c.id === id ? { ...c, name } : c)); + await api.updateCollection(id, name); + }; + + const handleDeleteCollection = async (id: string) => { + setCollections(prev => prev.filter(c => c.id !== id)); + await api.deleteCollection(id); + }; + + const handleOpenDrawing = (id: string) => { + navigate(`/editor/${id}`); + }; + + const handleRemoveFromVault = async (id: string) => { + if (!vault.sessionKey) return; + + const drawing = drawings.find(d => d.id === id); + if (!drawing || !drawing.encryptedData || !drawing.iv) return; + + try { + // Decrypt the drawing data + const decrypted = await decryptDrawing( + drawing.encryptedData, + drawing.iv, + vault.sessionKey + ); + + // Unlock the drawing (move it out of private vault) + await api.unlockDrawing( + id, + decrypted.elements, + decrypted.appState, + decrypted.files + ); + + // Refresh the list + fetchPrivateDrawings(); + } catch (err) { + console.error('Failed to remove drawing from vault:', err); + setError('Failed to remove drawing from vault'); + } + }; + + const handleDelete = async (id: string) => { + try { + await api.deleteDrawing(id); + setDrawings(prev => prev.filter(d => d.id !== id)); + } catch (err) { + console.error('Failed to delete drawing:', err); + } + }; + + const handleRename = async (id: string, name: string) => { + try { + await api.updateDrawing(id, { name }); + setDrawings(prev => prev.map(d => d.id === id ? { ...d, name } : d)); + } catch (err) { + console.error('Failed to rename drawing:', err); + } + }; + + if (vault.isLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+
+
+ {vault.isUnlocked ? ( + + ) : ( + + )} +
+
+

+ Private Vault +

+

+ {!vault.isSetup + ? 'Set up your private vault to protect sensitive drawings' + : vault.isUnlocked + ? 'End-to-end encrypted drawings' + : 'Unlock vault to view drawings'} +

+
+
+ + {vault.isUnlocked && ( + + )} +
+ + {error && ( +
+ + {error} +
+ )} + + {/* Not Set Up State */} + {!vault.isSetup ? ( +
+
+ +
+

+ Set Up Private Vault +

+

+ Protect sensitive drawings with end-to-end encryption. + Only you can access them with your password. +

+ +
+ ) : !vault.isUnlocked ? ( + /* Locked State */ +
+
+ +
+

+ Vault is Locked +

+

+ Enter your password to access your private drawings. +

+ +
+ ) : isLoading ? ( + /* Loading State */ +
+ +
+ ) : drawings.length === 0 ? ( + /* Empty State */ +
+
+ +
+

+ No Private Drawings +

+

+ Move drawings to your private vault from the dashboard context menu. +

+
+ ) : ( + /* Drawings Grid */ +
+ {drawings.map((drawing) => ( + handleOpenDrawing(drawing.id)} + onDelete={() => handleDelete(drawing.id)} + onRemoveFromVault={() => setDrawingToUnlock(drawing.id)} + onRename={handleRename} + /> + ))} +
+ )} + + {/* Setup Modal */} + setShowSetupModal(false)} + onSetup={vault.setupVault} + /> + + {/* Unlock Modal */} + + + {/* Confirm remove from vault */} + { + if (drawingToUnlock) { + handleRemoveFromVault(drawingToUnlock); + } + setDrawingToUnlock(null); + }} + onCancel={() => setDrawingToUnlock(null)} + /> +
+ ); +}; diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 314d0c3..97d7a6a 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -3,21 +3,31 @@ import { Layout } from '../components/Layout'; import { useNavigate } from 'react-router-dom'; import * as api from '../api'; import type { Collection } from '../types'; -import { Database, FileJson, Upload, Moon, Sun, Info, HardDrive } from 'lucide-react'; +import { Database, FileJson, Upload, Moon, Sun, Info, HardDrive, Lock, Key, ShieldCheck, Unlock } from 'lucide-react'; import { ConfirmModal } from '../components/ConfirmModal'; import { importDrawings } from '../utils/importUtils'; import { useTheme } from '../context/ThemeContext'; +import { useVault } from '../context/VaultContext'; +import { PrivateVaultSetup } from '../components/PrivateVaultSetup'; +import { UnlockVaultModal } from '../components/UnlockVaultModal'; +import { ChangeVaultPassword } from '../components/ChangeVaultPassword'; export const Settings: React.FC = () => { const [collections, setCollections] = useState([]); const navigate = useNavigate(); const { theme, toggleTheme } = useTheme(); + const vault = useVault(); // Import state const [importConfirmation, setImportConfirmation] = useState<{ isOpen: boolean; file: File | null }>({ isOpen: false, file: null }); const [importError, setImportError] = useState<{ isOpen: boolean; message: string }>({ isOpen: false, message: '' }); const [importSuccess, setImportSuccess] = useState(false); + // Vault modal state + const [showVaultSetup, setShowVaultSetup] = useState(false); + const [showUnlockModal, setShowUnlockModal] = useState(false); + const [showChangePassword, setShowChangePassword] = useState(false); + const appVersion = import.meta.env.VITE_APP_VERSION || 'Unknown version'; const buildLabel = import.meta.env.VITE_APP_BUILD_LABEL; @@ -71,6 +81,114 @@ export const Settings: React.FC = () => { Settings + {/* Private Vault Section */} +
+

Private Vault

+
+ {vault.isLoading ? ( +
+
+
+ ) : !vault.isSetup ? ( +
+
+ +
+

+ Set Up Private Vault +

+

+ Protect sensitive drawings with end-to-end encryption. + Only you can access them with your password. +

+ +
+ ) : ( +
+ {/* Vault Status */} +
+
+
+ {vault.isUnlocked ? ( + + ) : ( + + )} +
+
+

+ {vault.isUnlocked ? 'Vault Unlocked' : 'Vault Locked'} +

+

+ {vault.privateDrawingsCount} private drawing{vault.privateDrawingsCount !== 1 ? 's' : ''} +

+
+
+ +
+ + {/* Vault Actions */} +
+ {/* View Private Drawings */} + + + {/* Change Password */} + +
+ + {/* Password Hint */} + {vault.passwordHint && ( +
+

Password Hint

+

{vault.passwordHint}

+
+ )} +
+ )} +
+
+ +

General

{/* Theme Toggle */}