// Reference schema for @urbicon-ui/auth
// Copy the relevant models into your own schema.prisma.
// Consumer-specific fields (e.g. 'apartment') belong in a separate
// UserProfile model — NOT here.
//
// Ids: `String @id` maps to `text`, which holds any id scheme. Mapping them to
// a native type instead is supported and needs no interface change — add
// `@db.Uuid` throughout. The package treats ids as opaque strings either way.
// What a native type does change: an id that does not parse raises an error
// instead of matching nothing, so an adapter over such a column must report
// that as a miss (the shipped Prisma adapter does). A numeric key needs an
// adapter that converts at the boundary. See "Ids: opaque strings" in
// docs/AUTH.md.
//
// Eight models below hang off User via `onDelete: Cascade` — seven through
// `userId`, plus Invitation through `invitedById`.

model User {
  id                     String    @id @default(uuid())
  email                  String    @unique
  name                   String
  passwordHash           String
  role                   String    @default("USER")
  emailVerified          Boolean   @default(false)
  tokenVersion           Int       @default(0)

  // Lockout
  failedLoginAttempts    Int       @default(0)
  lockedUntil            DateTime?
  lastFailedLogin        DateTime?

  // Verification
  verificationToken         String?   @unique
  verificationTokenExpires  DateTime?

  // Password-Reset
  passwordResetToken         String?  @unique
  passwordResetTokenExpires  DateTime?

  // Email-Change (verified to the NEW address; one pending change per user)
  pendingEmail               String?
  emailChangeToken           String?   @unique
  emailChangeTokenExpires    DateTime?

  // Two-Factor (TOTP). The secret is stored ENCRYPTED at rest (AES-256-GCM,
  // see server/totp.ts). totpEnabled is the only one surfaced publicly
  // (sanitizeUser); the secret never leaves the server.
  totpSecret                 String?
  totpEnabled                Boolean   @default(false)
  totpConfirmedAt            DateTime?

  // Relations (auth-intern)
  pushSubscriptions         PushSubscription[]
  notifications             Notification[]
  notificationPreferences   NotificationPreference[]
  passkeys                  Passkey[]
  refreshTokens             RefreshToken[]
  twoFactorBackupCodes      TwoFactorBackupCode[]
  invitationsSent           Invitation[]    @relation("InvitedBy")
  federatedAccounts         FederatedAccount[]

  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

model Invitation {
  id          String    @id @default(uuid())
  email       String    @unique
  role        String
  // onDelete: Cascade so deleting the inviter also removes the invitations they
  // sent. The shipped adapter's `user.delete` ALSO removes them in a transaction
  // (portable for schemas without this cascade), so the two are belt-and-braces.
  invitedBy   User      @relation("InvitedBy", fields: [invitedById], references: [id], onDelete: Cascade)
  invitedById String
  usedAt      DateTime?
  createdAt   DateTime  @default(now())
  // SHA-256 of the invitation token. @unique because it is the lookup key on
  // the registration path; the raw token is returned once, at creation, and
  // never stored — a dump of this table cannot redeem anything.
  tokenHash   String    @unique
  // Registration rejects an invitation past this instant even while usedAt is
  // still null, so the window between minting and use is bounded.
  expiresAt   DateTime
  // When the invitation was delivered BY EMAIL, null when it was only handed
  // out as a copied link. Only the first is a mailbox-possession proof, and
  // only it lets `autoVerifyInvited` skip verification.
  emailedAt   DateTime?
}

model PushSubscription {
  id        String   @id @default(uuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  endpoint  String   @unique
  keys      Json
  createdAt DateTime @default(now())
}

model Notification {
  id        String    @id @default(uuid())
  userId    String
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  typeKey   String
  title     String
  body      String?
  url       String?
  icon      String?
  readAt    DateTime?
  createdAt DateTime  @default(now())
}

model NotificationType {
  key         String  @id
  label       String
  description String?
}

model NotificationPreference {
  id      String  @id @default(uuid())
  userId  String
  user    User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  typeKey String
  sse     Boolean @default(true)
  push    Boolean @default(true)
  email   Boolean @default(true)

  @@unique([userId, typeKey])
}

model Passkey {
  id            String    @id @default(uuid())
  userId        String
  user          User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  credentialId  String    @unique
  publicKey     Bytes
  publicKeyAlg  Int
  counter       Int       @default(0)
  transports    String[]  @default([])
  aaguid        String
  name          String    @default("Passkey")
  createdAt     DateTime  @default(now())
  lastUsedAt    DateTime?
}

// Refresh tokens are stored as SHA-256 hashes (never raw). Each login starts a
// new `family` — an opaque grouping label the package fills with a random UUID,
// but a plain `String` column, NOT an id: do not give it `@db.Uuid`, or a
// consumer-supplied family value stops being storable. Every successful
// rotation replaces the token inside the same family. If a *revoked* token in a
// family is ever presented again, the full family is invalidated as a
// reuse-detection countermeasure (stolen-token-scenario). Opt-in via
// `AuthConfig.refreshToken`.
model RefreshToken {
  id            String    @id @default(uuid())
  userId        String
  user          User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  tokenHash     String    @unique
  family        String
  expiresAt     DateTime
  revokedAt     DateTime?
  replacedById  String?
  createdAt     DateTime  @default(now())

  // Session-listing metadata (optional). userAgent drives device recognition;
  // ip is only written when the consumer sets config.sessions.storeIp.
  userAgent     String?
  ip            String?

  @@index([userId])
  @@index([family])
  @@index([expiresAt])
}

// Two-factor backup (recovery) codes, stored as SHA-256 hashes (never raw).
// 8–10 are issued once at 2FA enable; each is single-use (usedAt flips null→now
// on redemption). Cleared and re-issued on enable, removed on disable; the
// cascade also clears them when the user is deleted. Opt-in via
// `AuthConfig.twoFactor`.
model TwoFactorBackupCode {
  id        String    @id @default(uuid())
  userId    String
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  codeHash  String
  usedAt    DateTime?
  createdAt DateTime  @default(now())

  @@index([userId])
}

// Federated identity links (SSO, CONSUMER side): maps an IdP identity —
// (issuer, subject) — to a LOCAL user row, for `createFederatedAuthHandle`'s
// `resolveUser`. This model belongs in the *consumer* app's schema (point
// `userId` at that app's own user model if it doesn't use this User model);
// an IdP-only deployment doesn't need it. `issuer` is the consumer's stable
// label for the IdP (canonically its origin, e.g. "https://auth.example.com")
// — the IdP token carries no `iss` claim, the trust anchor is the one JWKS
// URL you configured. Unique on (issuer, subject): one federated identity
// links to exactly one local user. Optional — see
// `createPrismaFederatedAccountRepository` (throws without this model).
model FederatedAccount {
  id        String   @id @default(uuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  issuer    String
  subject   String
  createdAt DateTime @default(now())

  @@unique([issuer, subject])
  @@index([userId])
}
