Prisma models per data-model.md: users (status enum, site-admin flag), user_identities (password provider now, OIDC later — subject is the stable user id), sessions (hashed ids), auth_tokens (hashed, single- use), plus rate_limits and mail_outbox for the upcoming M1 stories. UsersService creates accounts transactionally with Argon2id-hashed password identities (OWASP parameters, rehash detection) and maps uniqueness violations to field-level conflicts. Database-backed suites run when TEST_DATABASE_URL is set — locally against the dev db, in CI via a new postgres service container; shared auth schemas (username, password policy incl. common-password blocklist) ship with tests. Closes #10 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
3.8 KiB
SQL
124 lines
3.8 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "UserStatus" AS ENUM ('PENDING_VERIFICATION', 'ACTIVE', 'DISABLED');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "AuthTokenPurpose" AS ENUM ('EMAIL_VERIFICATION', 'PASSWORD_RESET');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "MailStatus" AS ENUM ('PENDING', 'SENT', 'FAILED');
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "users" (
|
|
"id" TEXT NOT NULL,
|
|
"username" TEXT NOT NULL,
|
|
"email" TEXT NOT NULL,
|
|
"display_name" TEXT NOT NULL,
|
|
"locale" TEXT NOT NULL DEFAULT 'en',
|
|
"is_site_admin" BOOLEAN NOT NULL DEFAULT false,
|
|
"status" "UserStatus" NOT NULL DEFAULT 'PENDING_VERIFICATION',
|
|
"email_verified_at" TIMESTAMP(3),
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"last_login_at" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "user_identities" (
|
|
"id" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"provider" TEXT NOT NULL,
|
|
"subject" TEXT NOT NULL,
|
|
"credential" TEXT,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "user_identities_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "sessions" (
|
|
"id" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
"last_seen_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"user_agent" TEXT,
|
|
|
|
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "auth_tokens" (
|
|
"id" TEXT NOT NULL,
|
|
"token_hash" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"purpose" "AuthTokenPurpose" NOT NULL,
|
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
"consumed_at" TIMESTAMP(3),
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "auth_tokens_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "rate_limits" (
|
|
"key" TEXT NOT NULL,
|
|
"window_start" TIMESTAMP(3) NOT NULL,
|
|
"count" INTEGER NOT NULL DEFAULT 0,
|
|
|
|
CONSTRAINT "rate_limits_pkey" PRIMARY KEY ("key")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "mail_outbox" (
|
|
"id" TEXT NOT NULL,
|
|
"to_address" TEXT NOT NULL,
|
|
"subject" TEXT NOT NULL,
|
|
"text_body" TEXT NOT NULL,
|
|
"html_body" TEXT NOT NULL,
|
|
"status" "MailStatus" NOT NULL DEFAULT 'PENDING',
|
|
"attempts" INTEGER NOT NULL DEFAULT 0,
|
|
"next_attempt_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"last_error" TEXT,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"sent_at" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "mail_outbox_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "user_identities_user_id_idx" ON "user_identities"("user_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "user_identities_provider_subject_key" ON "user_identities"("provider", "subject");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "sessions_user_id_idx" ON "sessions"("user_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "sessions_expires_at_idx" ON "sessions"("expires_at");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "auth_tokens_token_hash_key" ON "auth_tokens"("token_hash");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "auth_tokens_user_id_purpose_idx" ON "auth_tokens"("user_id", "purpose");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "mail_outbox_status_next_attempt_at_idx" ON "mail_outbox"("status", "next_attempt_at");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "user_identities" ADD CONSTRAINT "user_identities_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "auth_tokens" ADD CONSTRAINT "auth_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|