From d9013b8f7ae75155b17a749f96fe16e06816a495 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sat, 24 Jan 2026 17:11:40 +0100 Subject: [PATCH] feat(auth): add user authentication database schema - Add User model with email, passwordHash, and name fields - Add userId foreign key to Drawing and Collection models - Create initial migration for user authentication --- .../migration.sql | 64 +++++++++++++++++++ backend/prisma/schema.prisma | 53 ++++++++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 backend/prisma/migrations/20260124145151_add_user_auth/migration.sql diff --git a/backend/prisma/migrations/20260124145151_add_user_auth/migration.sql b/backend/prisma/migrations/20260124145151_add_user_auth/migration.sql new file mode 100644 index 0000000..753f64e --- /dev/null +++ b/backend/prisma/migrations/20260124145151_add_user_auth/migration.sql @@ -0,0 +1,64 @@ +/* + Warnings: + + - Added the required column `userId` to the `Collection` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `Drawing` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Collection" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Collection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Collection" ("createdAt", "id", "name", "updatedAt") SELECT "createdAt", "id", "name", "updatedAt" FROM "Collection"; +DROP TABLE "Collection"; +ALTER TABLE "new_Collection" RENAME TO "Collection"; +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, + "userId" TEXT NOT NULL, + "collectionId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Drawing_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + 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"; +CREATE TABLE "new_Library" ( + "id" TEXT NOT NULL PRIMARY KEY, + "items" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Library" ("createdAt", "id", "items", "updatedAt") SELECT "createdAt", "id", "items", "updatedAt" FROM "Library"; +DROP TABLE "Library"; +ALTER TABLE "new_Library" RENAME TO "Library"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 23da027..467a0bb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -12,9 +12,26 @@ datasource db { url = env("DATABASE_URL") } +model User { + id String @id @default(uuid()) + email String @unique + passwordHash String + name String + isActive Boolean @default(true) + drawings Drawing[] + collections Collection[] + passwordResetTokens PasswordResetToken[] + refreshTokens RefreshToken[] + auditLogs AuditLog[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model Collection { id String @id @default(uuid()) name String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) drawings Drawing[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -28,6 +45,8 @@ model Drawing { files String @default("{}") // Stored as JSON string preview String? // SVG string for thumbnail version Int @default(1) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) collectionId String? collection Collection? @relation(fields: [collectionId], references: [id]) createdAt DateTime @default(now()) @@ -35,8 +54,40 @@ model Drawing { } model Library { - id String @id @default("default") // Singleton pattern - use "default" ID + id String @id // User-specific library ID (e.g., "user_") items String @default("[]") // Stored as JSON string array of library items createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model PasswordResetToken { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + token String @unique + expiresAt DateTime + used Boolean @default(false) + createdAt DateTime @default(now()) +} + +model RefreshToken { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + token String @unique + expiresAt DateTime + revoked Boolean @default(false) + createdAt DateTime @default(now()) +} + +model AuditLog { + id String @id @default(uuid()) + userId String? + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + action String // e.g., "login", "login_failed", "password_reset", "password_changed", "drawing_deleted" + resource String? // e.g., "drawing:123", "collection:456" + ipAddress String? + userAgent String? + details String? // JSON string for additional details + createdAt DateTime @default(now()) +}