diff --git a/prisma/migrations/20_add_2fa/migration.sql b/prisma/migrations/20_add_2fa/migration.sql new file mode 100644 index 000000000..b3accc2df --- /dev/null +++ b/prisma/migrations/20_add_2fa/migration.sql @@ -0,0 +1,72 @@ +-- AlterTable +ALTER TABLE "team" ADD COLUMN "two_factor_required" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "user" ADD COLUMN "two_factor_required" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "two_factor_auth" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "secret" TEXT NOT NULL, + "is_enabled" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "two_factor_auth_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_backup_code" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "code_hash" TEXT NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "two_factor_backup_code_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_otp_used" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "otp" TEXT NOT NULL, + "expires_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "two_factor_otp_used_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_rate_limit" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "attempts" INTEGER NOT NULL DEFAULT 0, + "locked_until" TIMESTAMPTZ(6), + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "two_factor_rate_limit_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "app_setting" ( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "app_setting_pkey" PRIMARY KEY ("key") +); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_auth_user_id_key" ON "two_factor_auth"("user_id"); + +-- CreateIndex +CREATE INDEX "two_factor_backup_code_user_id_idx" ON "two_factor_backup_code"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_otp_used_user_id_otp_key" ON "two_factor_otp_used"("user_id", "otp"); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_rate_limit_user_id_key" ON "two_factor_rate_limit"("user_id"); + +-- CreateIndex +CREATE INDEX "session_replay_visit_id_idx" ON "session_replay"("visit_id"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 63c81ec0d..c4c0c0a4f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,15 +18,18 @@ model User { displayName String? @map("display_name") @db.VarChar(255) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + twoFactorRequired Boolean @default(false) @map("two_factor_required") - websites Website[] @relation("user") - createdBy Website[] @relation("createUser") - links Link[] @relation("user") - pixels Pixel[] @relation("user") - teams TeamUser[] - reports Report[] - boards Board[] @relation("user") + websites Website[] @relation("user") + createdBy Website[] @relation("createUser") + links Link[] @relation("user") + pixels Pixel[] @relation("user") + teams TeamUser[] + reports Report[] + boards Board[] @relation("user") + twoFactorAuth TwoFactorAuth? + twoFactorBackupCodes TwoFactorBackupCode[] @@map("user") } @@ -201,13 +204,14 @@ model Team { logoUrl String? @map("logo_url") @db.VarChar(2183) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + twoFactorRequired Boolean @default(false) @map("two_factor_required") websites Website[] members TeamUser[] links Link[] pixels Pixel[] - boards Board[] + boards Board[] @@index([accessCode]) @@map("team") @@ -398,4 +402,55 @@ model SessionReplaySaved { @@index([visitId]) @@index([websiteId, createdAt]) @@map("session_replay_saved") +} + +model TwoFactorAuth { + id String @id @default(cuid()) + userId String @unique @map("user_id") @db.Uuid + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + secret String + isEnabled Boolean @default(false) @map("is_enabled") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + + @@map("two_factor_auth") +} + +model TwoFactorBackupCode { + id String @id @default(cuid()) + userId String @map("user_id") @db.Uuid + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + codeHash String @map("code_hash") + used Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + + @@index([userId]) + @@map("two_factor_backup_code") +} + +model TwoFactorOtpUsed { + id String @id @default(cuid()) + userId String @map("user_id") @db.Uuid + otp String + expiresAt DateTime @map("expires_at") @db.Timestamptz(6) + + @@unique([userId, otp]) + @@map("two_factor_otp_used") +} + +model TwoFactorRateLimit { + id String @id @default(cuid()) + userId String @unique @map("user_id") @db.Uuid + attempts Int @default(0) + lockedUntil DateTime? @map("locked_until") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + + @@map("two_factor_rate_limit") +} + +model AppSetting { + key String @id + value String + + @@map("app_setting") } \ No newline at end of file