feat: email verification + delete confirmation + smtp/cors/prod hardening

This commit is contained in:
2026-02-09 14:46:49 -06:00
parent 27cc7d159b
commit 9856317641
22 changed files with 896 additions and 58 deletions

View File

@@ -0,0 +1,19 @@
ALTER TABLE "User"
ADD COLUMN IF NOT EXISTS "emailVerified" BOOLEAN NOT NULL DEFAULT false;
CREATE TABLE IF NOT EXISTS "EmailToken" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid()::text,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EmailToken_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "EmailToken_userId_type_idx" ON "EmailToken"("userId", "type");
CREATE INDEX IF NOT EXISTS "EmailToken_tokenHash_idx" ON "EmailToken"("tokenHash");
ALTER TABLE "EmailToken"
ADD CONSTRAINT "EmailToken_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,6 @@
-- Harden email token lifecycle (used marker + lookup index)
ALTER TABLE "EmailToken"
ADD COLUMN IF NOT EXISTS "usedAt" TIMESTAMP(3);
CREATE INDEX IF NOT EXISTS "EmailToken_userId_type_expiresAt_idx"
ON "EmailToken"("userId", "type", "expiresAt");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "User"
ADD COLUMN IF NOT EXISTS "seenUpdateVersion" INTEGER NOT NULL DEFAULT 0;

View File

@@ -25,6 +25,8 @@ model User {
email String @unique
passwordHash String?
displayName String?
emailVerified Boolean @default(false)
seenUpdateVersion Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -43,6 +45,7 @@ model User {
allocations Allocation[]
transactions Transaction[]
budgetSessions BudgetSession[]
emailTokens EmailToken[]
}
model VariableCategory {
@@ -161,3 +164,18 @@ model BudgetSession {
@@unique([userId, periodStart])
@@index([userId, periodStart])
}
model EmailToken {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String // "signup" | "delete"
usedAt DateTime?
tokenHash String
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId, type])
@@index([userId, type, expiresAt])
@@index([tokenHash])
}