import { pgTable, text, boolean, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core'; import { ports } from './ports'; import { clients } from './clients'; /** * Portal users - one per client account that's been invited to the client * portal. Separate from the CRM `users` table (managed by better-auth) so the * authentication realms stay isolated. * * Created by an admin from the client detail page; the admin's invite mails * an activation token that lets the client set their own password. */ export const portalUsers = pgTable( 'portal_users', { id: text('id') .primaryKey() .$defaultFn(() => crypto.randomUUID()), portId: text('port_id') .notNull() .references(() => ports.id), clientId: text('client_id') .notNull() .references(() => clients.id, { onDelete: 'cascade' }), email: text('email').notNull(), /** * scrypt-hashed password. Format: `salt:keyHex` (both base64url). Null * until the user activates their account. */ passwordHash: text('password_hash'), name: text('name'), isActive: boolean('is_active').notNull().default(true), lastLoginAt: timestamp('last_login_at', { withTimezone: true }), createdBy: text('created_by').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ uniqueIndex('idx_portal_users_email_unique').on(table.email), index('idx_portal_users_client').on(table.clientId), index('idx_portal_users_port').on(table.portId), ], ); /** * Single-use tokens for portal-account activation and password reset. * * `tokenHash` is a SHA-256 hash of the raw token sent in the email. Lookups * happen by hash so a DB compromise never leaks active tokens. */ export const portalAuthTokens = pgTable( 'portal_auth_tokens', { id: text('id') .primaryKey() .$defaultFn(() => crypto.randomUUID()), portalUserId: text('portal_user_id') .notNull() .references(() => portalUsers.id, { onDelete: 'cascade' }), tokenHash: text('token_hash').notNull(), type: text('type').notNull(), // 'activation' | 'reset' expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), usedAt: timestamp('used_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ uniqueIndex('idx_portal_tokens_hash_unique').on(table.tokenHash), index('idx_portal_tokens_user').on(table.portalUserId), ], ); export type PortalUser = typeof portalUsers.$inferSelect; export type NewPortalUser = typeof portalUsers.$inferInsert; export type PortalAuthToken = typeof portalAuthTokens.$inferSelect; export type NewPortalAuthToken = typeof portalAuthTokens.$inferInsert;