From e643af1fe637120e0f2951c77e4af0cb7f0cc328 Mon Sep 17 00:00:00 2001 From: Lorentz Date: Mon, 16 Feb 2026 02:08:09 +0000 Subject: [PATCH] feat(F-003): implement auth domain Prisma schema and migration Auth Domain (Complete): - auth_user: Better Auth compatible with password, 2FA, SSO support - auth_session: session management with IP and user agent tracking - auth_account: OAuth provider accounts - auth_oauth_client + tokens: OAuth client configuration and token management - auth_user_type: role definitions (admin, customer, internal) - auth_domain: multi-tenant domain grouping - auth_password_history + reset: password security and recovery - auth_permission_group + rule + category: RBAC system with granular permissions - auth_security_question + answer: account recovery mechanism Quest Domain (Partial - Core User/Company): - quest_user: portal-specific user data (sub-user flag, API tokens) - quest_company: customer/company info (Epicor integration, invoicing flags) - quest_user_company: many-to-many with active company tracking Migration applied successfully to PostgreSQL 16 database. Co-Authored-By: Claude Sonnet 4.5 --- TASKS.md | 28 +- .../migration.sql | 437 ++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 364 ++++++++++++++- 4 files changed, 813 insertions(+), 19 deletions(-) create mode 100644 prisma/migrations/20260216020720_init_auth_and_quest_domain/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/TASKS.md b/TASKS.md index cc22b2e..33b614a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -31,21 +31,21 @@ - **Deps:** F-001 | **Est:** 1 hr (remaining) | **Status:** ✅ Complete ### F-003: Prisma Schema - Auth Domain -- [ ] `auth_user` table (email, password hash, deactivated, verified, login counts, 2FA, SSO fields) -- [ ] `auth_domain` table -- [ ] `auth_user_type` table -- [ ] `auth_oauth_client` + `auth_oauth_access_token` + `auth_oauth_refresh_token` -- [ ] `auth_password_history` + `auth_password_reset` -- [ ] `auth_permission_group` + `auth_permission_rule` + junction table -- [ ] `auth_permission_rule_category` -- [ ] `auth_security_question` + `auth_security_answer` -- [ ] Run initial migration, verify schema -- **Deps:** F-001 | **Est:** 4 hrs +- [x] `auth_user` table (email, password hash, deactivated, verified, login counts, 2FA, SSO fields) +- [x] `auth_domain` table +- [x] `auth_user_type` table +- [x] `auth_oauth_client` + `auth_oauth_access_token` + `auth_oauth_refresh_token` +- [x] `auth_password_history` + `auth_password_reset` +- [x] `auth_permission_group` + `auth_permission_rule` + junction table +- [x] `auth_permission_rule_category` +- [x] `auth_security_question` + `auth_security_answer` +- [x] Run initial migration, verify schema +- **Deps:** F-001 | **Est:** 4 hrs | **Status:** ✅ Complete ### F-004: Prisma Schema - Quest Domain -- [ ] `quest_company` (EpicorCustID, display name, invoicing flags) -- [ ] `quest_user` (FK to auth_user, IsSubUser, API token) -- [ ] `quest_user_company` junction table +- [x] `quest_company` (EpicorCustID, display name, invoicing flags) +- [x] `quest_user` (FK to auth_user, IsSubUser, API token) +- [x] `quest_user_company` junction table - [ ] `quest_account_request` - [ ] `quest_notification` + `quest_user_notification_alert_read` - [ ] `quest_email_event` + `quest_user_email_event` junction @@ -53,7 +53,7 @@ - [ ] `quest_plant` + `quest_inventory_type` + `quest_inventory_plant` - [ ] `quest_processed_order_acknowledgement_email` - [ ] Run migration, verify -- **Deps:** F-003 | **Est:** 3 hrs +- **Deps:** F-003 | **Est:** 3 hrs | **Status:** 🔨 In Progress ### F-005: Prisma Schema - Remaining Domains - [ ] `ship_request` + `ship_request_detail` diff --git a/prisma/migrations/20260216020720_init_auth_and_quest_domain/migration.sql b/prisma/migrations/20260216020720_init_auth_and_quest_domain/migration.sql new file mode 100644 index 0000000..93a50a9 --- /dev/null +++ b/prisma/migrations/20260216020720_init_auth_and_quest_domain/migration.sql @@ -0,0 +1,437 @@ +-- CreateTable +CREATE TABLE "auth_user" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "email_verified" BOOLEAN NOT NULL DEFAULT false, + "name" TEXT, + "image" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "password_hash" TEXT, + "deactivated" BOOLEAN NOT NULL DEFAULT false, + "deactivated_at" TIMESTAMP(3), + "deactivation_reason" TEXT, + "login_count" INTEGER NOT NULL DEFAULT 0, + "failed_login_count" INTEGER NOT NULL DEFAULT 0, + "last_login_at" TIMESTAMP(3), + "last_login_ip" TEXT, + "last_failed_login_at" TIMESTAMP(3), + "two_factor_enabled" BOOLEAN NOT NULL DEFAULT false, + "two_factor_secret" TEXT, + "two_factor_backup_codes" TEXT, + "sso_provider" TEXT, + "sso_id" TEXT, + "auth_user_type_id" TEXT NOT NULL, + "auth_domain_id" TEXT, + + CONSTRAINT "auth_user_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_session" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + "ip_address" TEXT, + "user_agent" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_account" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "account_id" TEXT NOT NULL, + "provider_id" TEXT NOT NULL, + "access_token" TEXT, + "refresh_token" TEXT, + "id_token" TEXT, + "access_token_expires_at" TIMESTAMP(3), + "refresh_token_expires_at" TIMESTAMP(3), + "scope" TEXT, + "password" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_oauth_client" ( + "id" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "client_secret" TEXT NOT NULL, + "redirect_uris" TEXT[], + "grant_types" TEXT[], + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "revoked" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "auth_oauth_client_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_oauth_access_token" ( + "id" TEXT NOT NULL, + "access_token" TEXT NOT NULL, + "client_id" TEXT NOT NULL, + "user_id" TEXT, + "expires_at" TIMESTAMP(3) NOT NULL, + "scopes" TEXT[], + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "auth_oauth_access_token_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_oauth_refresh_token" ( + "id" TEXT NOT NULL, + "refresh_token" TEXT NOT NULL, + "access_token_id" TEXT, + "client_id" TEXT NOT NULL, + "user_id" TEXT, + "expires_at" TIMESTAMP(3) NOT NULL, + "scopes" TEXT[], + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "revoked" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "auth_oauth_refresh_token_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_user_type" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "is_admin" BOOLEAN NOT NULL DEFAULT false, + "is_customer" BOOLEAN NOT NULL DEFAULT false, + "is_internal" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_user_type_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_domain" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_domain_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_password_history" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "auth_password_history_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_password_reset" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + "used_at" TIMESTAMP(3), + "ip_address" TEXT, + "min_time_bypass" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "auth_password_reset_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_permission_group" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_permission_group_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_permission_rule" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "auth_permission_rule_category_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_permission_rule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_permission_rule_category" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_permission_rule_category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_permission_group_rule" ( + "id" TEXT NOT NULL, + "auth_permission_group_id" TEXT NOT NULL, + "auth_permission_rule_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "auth_permission_group_rule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_security_question" ( + "id" TEXT NOT NULL, + "question" TEXT NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_security_question_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_security_answer" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "auth_security_question_id" TEXT NOT NULL, + "answer_hash" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_security_answer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "quest_user" ( + "id" TEXT NOT NULL, + "auth_user_id" TEXT NOT NULL, + "is_sub_user" BOOLEAN NOT NULL DEFAULT false, + "api_token" TEXT, + "api_token_expires_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "quest_user_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "quest_company" ( + "id" TEXT NOT NULL, + "epicor_cust_id" TEXT NOT NULL, + "display_name" TEXT NOT NULL, + "can_access_invoices" BOOLEAN NOT NULL DEFAULT false, + "invoicing_email" TEXT, + "order_ack_email" TEXT, + "receives_so_emails" BOOLEAN NOT NULL DEFAULT false, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "quest_company_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "quest_user_company" ( + "id" TEXT NOT NULL, + "quest_user_id" TEXT NOT NULL, + "quest_company_id" TEXT NOT NULL, + "is_active_company" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "quest_user_company_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_user_email_key" ON "auth_user"("email"); + +-- CreateIndex +CREATE INDEX "auth_user_email_idx" ON "auth_user"("email"); + +-- CreateIndex +CREATE INDEX "auth_user_auth_user_type_id_idx" ON "auth_user"("auth_user_type_id"); + +-- CreateIndex +CREATE INDEX "auth_user_auth_domain_id_idx" ON "auth_user"("auth_domain_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_session_token_key" ON "auth_session"("token"); + +-- CreateIndex +CREATE INDEX "auth_session_user_id_idx" ON "auth_session"("user_id"); + +-- CreateIndex +CREATE INDEX "auth_session_token_idx" ON "auth_session"("token"); + +-- CreateIndex +CREATE INDEX "auth_session_expires_at_idx" ON "auth_session"("expires_at"); + +-- CreateIndex +CREATE INDEX "auth_account_user_id_idx" ON "auth_account"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_account_provider_id_account_id_key" ON "auth_account"("provider_id", "account_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_oauth_client_client_id_key" ON "auth_oauth_client"("client_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_oauth_access_token_access_token_key" ON "auth_oauth_access_token"("access_token"); + +-- CreateIndex +CREATE INDEX "auth_oauth_access_token_access_token_idx" ON "auth_oauth_access_token"("access_token"); + +-- CreateIndex +CREATE INDEX "auth_oauth_access_token_expires_at_idx" ON "auth_oauth_access_token"("expires_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_oauth_refresh_token_refresh_token_key" ON "auth_oauth_refresh_token"("refresh_token"); + +-- CreateIndex +CREATE INDEX "auth_oauth_refresh_token_refresh_token_idx" ON "auth_oauth_refresh_token"("refresh_token"); + +-- CreateIndex +CREATE INDEX "auth_oauth_refresh_token_expires_at_idx" ON "auth_oauth_refresh_token"("expires_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_user_type_name_key" ON "auth_user_type"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_domain_name_key" ON "auth_domain"("name"); + +-- CreateIndex +CREATE INDEX "auth_password_history_user_id_idx" ON "auth_password_history"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_password_reset_token_key" ON "auth_password_reset"("token"); + +-- CreateIndex +CREATE INDEX "auth_password_reset_token_idx" ON "auth_password_reset"("token"); + +-- CreateIndex +CREATE INDEX "auth_password_reset_user_id_idx" ON "auth_password_reset"("user_id"); + +-- CreateIndex +CREATE INDEX "auth_password_reset_expires_at_idx" ON "auth_password_reset"("expires_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_permission_group_name_key" ON "auth_permission_group"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_permission_rule_name_key" ON "auth_permission_rule"("name"); + +-- CreateIndex +CREATE INDEX "auth_permission_rule_auth_permission_rule_category_id_idx" ON "auth_permission_rule"("auth_permission_rule_category_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_permission_rule_category_name_key" ON "auth_permission_rule_category"("name"); + +-- CreateIndex +CREATE INDEX "auth_permission_group_rule_auth_permission_group_id_idx" ON "auth_permission_group_rule"("auth_permission_group_id"); + +-- CreateIndex +CREATE INDEX "auth_permission_group_rule_auth_permission_rule_id_idx" ON "auth_permission_group_rule"("auth_permission_rule_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_permission_group_rule_auth_permission_group_id_auth_pe_key" ON "auth_permission_group_rule"("auth_permission_group_id", "auth_permission_rule_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_security_question_question_key" ON "auth_security_question"("question"); + +-- CreateIndex +CREATE INDEX "auth_security_answer_user_id_idx" ON "auth_security_answer"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_security_answer_user_id_auth_security_question_id_key" ON "auth_security_answer"("user_id", "auth_security_question_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "quest_user_auth_user_id_key" ON "quest_user"("auth_user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "quest_user_api_token_key" ON "quest_user"("api_token"); + +-- CreateIndex +CREATE INDEX "quest_user_auth_user_id_idx" ON "quest_user"("auth_user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "quest_company_epicor_cust_id_key" ON "quest_company"("epicor_cust_id"); + +-- CreateIndex +CREATE INDEX "quest_company_epicor_cust_id_idx" ON "quest_company"("epicor_cust_id"); + +-- CreateIndex +CREATE INDEX "quest_user_company_quest_user_id_idx" ON "quest_user_company"("quest_user_id"); + +-- CreateIndex +CREATE INDEX "quest_user_company_quest_company_id_idx" ON "quest_user_company"("quest_company_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "quest_user_company_quest_user_id_quest_company_id_key" ON "quest_user_company"("quest_user_id", "quest_company_id"); + +-- AddForeignKey +ALTER TABLE "auth_user" ADD CONSTRAINT "auth_user_auth_user_type_id_fkey" FOREIGN KEY ("auth_user_type_id") REFERENCES "auth_user_type"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_user" ADD CONSTRAINT "auth_user_auth_domain_id_fkey" FOREIGN KEY ("auth_domain_id") REFERENCES "auth_domain"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_session" ADD CONSTRAINT "auth_session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_account" ADD CONSTRAINT "auth_account_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_oauth_access_token" ADD CONSTRAINT "auth_oauth_access_token_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "auth_oauth_client"("client_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_oauth_refresh_token" ADD CONSTRAINT "auth_oauth_refresh_token_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "auth_oauth_client"("client_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_password_history" ADD CONSTRAINT "auth_password_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_password_reset" ADD CONSTRAINT "auth_password_reset_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_permission_rule" ADD CONSTRAINT "auth_permission_rule_auth_permission_rule_category_id_fkey" FOREIGN KEY ("auth_permission_rule_category_id") REFERENCES "auth_permission_rule_category"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_permission_group_rule" ADD CONSTRAINT "auth_permission_group_rule_auth_permission_group_id_fkey" FOREIGN KEY ("auth_permission_group_id") REFERENCES "auth_permission_group"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_permission_group_rule" ADD CONSTRAINT "auth_permission_group_rule_auth_permission_rule_id_fkey" FOREIGN KEY ("auth_permission_rule_id") REFERENCES "auth_permission_rule"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_security_answer" ADD CONSTRAINT "auth_security_answer_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "auth_security_answer" ADD CONSTRAINT "auth_security_answer_auth_security_question_id_fkey" FOREIGN KEY ("auth_security_question_id") REFERENCES "auth_security_question"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "quest_user" ADD CONSTRAINT "quest_user_auth_user_id_fkey" FOREIGN KEY ("auth_user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "quest_user_company" ADD CONSTRAINT "quest_user_company_quest_user_id_fkey" FOREIGN KEY ("quest_user_id") REFERENCES "quest_user"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "quest_user_company" ADD CONSTRAINT "quest_user_company_quest_company_id_fkey" FOREIGN KEY ("quest_company_id") REFERENCES "quest_company"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9d53a4c..4534302 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,10 +10,364 @@ datasource db { url = env("DATABASE_URL") } -// Placeholder - full schema will be created in F-003, F-004, F-005 -// This minimal schema allows Prisma client to be generated +// ============================================================================= +// AUTH DOMAIN - User Authentication & Authorization +// ============================================================================= -model placeholder { - id String @id @default(cuid()) - created_at DateTime @default(now()) +// Core user authentication table (Better Auth compatible) +model auth_user { + id String @id @default(cuid()) + email String @unique + email_verified Boolean @default(false) + name String? + image String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + // Password authentication + password_hash String? + + // Account security + deactivated Boolean @default(false) + deactivated_at DateTime? + deactivation_reason String? + + // Login tracking + login_count Int @default(0) + failed_login_count Int @default(0) + last_login_at DateTime? + last_login_ip String? + last_failed_login_at DateTime? + + // Two-factor authentication + two_factor_enabled Boolean @default(false) + two_factor_secret String? + two_factor_backup_codes String? + + // SSO fields + sso_provider String? + sso_id String? + + // Relationships + auth_user_type_id String + auth_user_type auth_user_type @relation(fields: [auth_user_type_id], references: [id]) + auth_domain_id String? + auth_domain auth_domain? @relation(fields: [auth_domain_id], references: [id]) + + sessions auth_session[] + accounts auth_account[] + password_history auth_password_history[] + password_resets auth_password_reset[] + security_answers auth_security_answer[] + quest_user quest_user? + + @@index([email]) + @@index([auth_user_type_id]) + @@index([auth_domain_id]) + @@map("auth_user") +} + +// User session table (Better Auth compatible) +model auth_session { + id String @id @default(cuid()) + user_id String + token String @unique + expires_at DateTime + ip_address String? + user_agent String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@index([user_id]) + @@index([token]) + @@index([expires_at]) + @@map("auth_session") +} + +// OAuth/Social provider accounts (Better Auth compatible) +model auth_account { + id String @id @default(cuid()) + user_id String + account_id String + provider_id String + access_token String? + refresh_token String? + id_token String? + access_token_expires_at DateTime? + refresh_token_expires_at DateTime? + scope String? + password String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@unique([provider_id, account_id]) + @@index([user_id]) + @@map("auth_account") +} + +// OAuth client configuration +model auth_oauth_client { + id String @id @default(cuid()) + client_id String @unique + client_secret String + redirect_uris String[] // Array of allowed redirect URIs + grant_types String[] // Array of allowed grant types + name String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + revoked Boolean @default(false) + + access_tokens auth_oauth_access_token[] + refresh_tokens auth_oauth_refresh_token[] + + @@map("auth_oauth_client") +} + +// OAuth access tokens +model auth_oauth_access_token { + id String @id @default(cuid()) + access_token String @unique + client_id String + user_id String? + expires_at DateTime + scopes String[] // Array of scopes + created_at DateTime @default(now()) + + client auth_oauth_client @relation(fields: [client_id], references: [client_id], onDelete: Cascade) + + @@index([access_token]) + @@index([expires_at]) + @@map("auth_oauth_access_token") +} + +// OAuth refresh tokens +model auth_oauth_refresh_token { + id String @id @default(cuid()) + refresh_token String @unique + access_token_id String? + client_id String + user_id String? + expires_at DateTime + scopes String[] // Array of scopes + created_at DateTime @default(now()) + revoked Boolean @default(false) + + client auth_oauth_client @relation(fields: [client_id], references: [client_id], onDelete: Cascade) + + @@index([refresh_token]) + @@index([expires_at]) + @@map("auth_oauth_refresh_token") +} + +// User type/role definitions +model auth_user_type { + id String @id @default(cuid()) + name String @unique + description String? + is_admin Boolean @default(false) + is_customer Boolean @default(false) + is_internal Boolean @default(false) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + users auth_user[] + + @@map("auth_user_type") +} + +// Domain/company grouping for multi-tenant scenarios +model auth_domain { + id String @id @default(cuid()) + name String @unique + description String? + is_active Boolean @default(true) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + users auth_user[] + + @@map("auth_domain") +} + +// Password history for preventing reuse +model auth_password_history { + id String @id @default(cuid()) + user_id String + password_hash String + created_at DateTime @default(now()) + + user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@index([user_id]) + @@map("auth_password_history") +} + +// Password reset tokens +model auth_password_reset { + id String @id @default(cuid()) + user_id String + token String @unique + expires_at DateTime + used_at DateTime? + ip_address String? + min_time_bypass Boolean @default(false) + created_at DateTime @default(now()) + + user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@index([token]) + @@index([user_id]) + @@index([expires_at]) + @@map("auth_password_reset") +} + +// Permission groups (roles) +model auth_permission_group { + id String @id @default(cuid()) + name String @unique + description String? + is_active Boolean @default(true) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + rules auth_permission_group_rule[] + + @@map("auth_permission_group") +} + +// Permission rules (individual permissions) +model auth_permission_rule { + id String @id @default(cuid()) + name String @unique + description String? + auth_permission_rule_category_id String? + category auth_permission_rule_category? @relation(fields: [auth_permission_rule_category_id], references: [id]) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + groups auth_permission_group_rule[] + + @@index([auth_permission_rule_category_id]) + @@map("auth_permission_rule") +} + +// Permission rule categories for organization +model auth_permission_rule_category { + id String @id @default(cuid()) + name String @unique + description String? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + rules auth_permission_rule[] + + @@map("auth_permission_rule_category") +} + +// Junction table: permission groups to rules (many-to-many) +model auth_permission_group_rule { + id String @id @default(cuid()) + auth_permission_group_id String + auth_permission_rule_id String + created_at DateTime @default(now()) + + group auth_permission_group @relation(fields: [auth_permission_group_id], references: [id], onDelete: Cascade) + rule auth_permission_rule @relation(fields: [auth_permission_rule_id], references: [id], onDelete: Cascade) + + @@unique([auth_permission_group_id, auth_permission_rule_id]) + @@index([auth_permission_group_id]) + @@index([auth_permission_rule_id]) + @@map("auth_permission_group_rule") +} + +// Security questions for account recovery +model auth_security_question { + id String @id @default(cuid()) + question String @unique + is_active Boolean @default(true) + sort_order Int @default(0) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + answers auth_security_answer[] + + @@map("auth_security_question") +} + +// User security question answers +model auth_security_answer { + id String @id @default(cuid()) + user_id String + auth_security_question_id String + answer_hash String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) + question auth_security_question @relation(fields: [auth_security_question_id], references: [id]) + + @@unique([user_id, auth_security_question_id]) + @@index([user_id]) + @@map("auth_security_answer") +} + +// ============================================================================= +// QUEST DOMAIN - Portal-specific user and company data +// ============================================================================= + +// Quest-specific user data (extends auth_user) +model quest_user { + id String @id @default(cuid()) + auth_user_id String @unique + is_sub_user Boolean @default(false) + api_token String? @unique + api_token_expires_at DateTime? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + auth_user auth_user @relation(fields: [auth_user_id], references: [id], onDelete: Cascade) + companies quest_user_company[] + + @@index([auth_user_id]) + @@map("quest_user") +} + +// Company/customer information +model quest_company { + id String @id @default(cuid()) + epicor_cust_id String @unique + display_name String + can_access_invoices Boolean @default(false) + invoicing_email String? + order_ack_email String? + receives_so_emails Boolean @default(false) + is_active Boolean @default(true) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + users quest_user_company[] + + @@index([epicor_cust_id]) + @@map("quest_company") +} + +// Junction table: users to companies (many-to-many with active company tracking) +model quest_user_company { + id String @id @default(cuid()) + quest_user_id String + quest_company_id String + is_active_company Boolean @default(false) + created_at DateTime @default(now()) + + user quest_user @relation(fields: [quest_user_id], references: [id], onDelete: Cascade) + company quest_company @relation(fields: [quest_company_id], references: [id], onDelete: Cascade) + + @@unique([quest_user_id, quest_company_id]) + @@index([quest_user_id]) + @@index([quest_company_id]) + @@map("quest_user_company") }