From 728f8ae7fd0fbd8f2497c60e69fb56d96fff1c7e Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 3 Feb 2026 07:48:30 -0600 Subject: [PATCH] [ST-P3-008] feat: Add missing auth entities from erp-core - tenant.entity.ts: Multi-tenant support - device.entity.ts: User devices tracking - verification-code.entity.ts: MFA verification codes - trusted-device.entity.ts: Trusted device management - mfa-audit-log.entity.ts: MFA audit logging - oauth-provider.entity.ts: OAuth provider configuration - oauth-state.entity.ts: OAuth state management - oauth-user-link.entity.ts: OAuth user linking - user-profile.entity.ts: User profile definitions - profile-tool.entity.ts: Profile tool permissions - profile-module.entity.ts: Profile module access - user-profile-assignment.entity.ts: Profile assignments Total: 12 new entities synchronized from erp-core. Co-Authored-By: Claude Opus 4.5 --- src/modules/auth/entities/device.entity.ts | 64 +++++++ src/modules/auth/entities/index.ts | 32 +++- .../auth/entities/mfa-audit-log.entity.ts | 80 ++++++++ .../auth/entities/oauth-provider.entity.ts | 181 ++++++++++++++++++ .../auth/entities/oauth-state.entity.ts | 60 ++++++ .../auth/entities/oauth-user-link.entity.ts | 68 +++++++ .../auth/entities/profile-module.entity.ts | 27 +++ .../auth/entities/profile-tool.entity.ts | 36 ++++ src/modules/auth/entities/tenant.entity.ts | 91 +++++++++ .../auth/entities/trusted-device.entity.ts | 107 +++++++++++ .../user-profile-assignment.entity.ts | 36 ++++ .../auth/entities/user-profile.entity.ts | 52 +++++ .../auth/entities/verification-code.entity.ts | 82 ++++++++ 13 files changed, 912 insertions(+), 4 deletions(-) create mode 100644 src/modules/auth/entities/device.entity.ts create mode 100644 src/modules/auth/entities/mfa-audit-log.entity.ts create mode 100644 src/modules/auth/entities/oauth-provider.entity.ts create mode 100644 src/modules/auth/entities/oauth-state.entity.ts create mode 100644 src/modules/auth/entities/oauth-user-link.entity.ts create mode 100644 src/modules/auth/entities/profile-module.entity.ts create mode 100644 src/modules/auth/entities/profile-tool.entity.ts create mode 100644 src/modules/auth/entities/tenant.entity.ts create mode 100644 src/modules/auth/entities/trusted-device.entity.ts create mode 100644 src/modules/auth/entities/user-profile-assignment.entity.ts create mode 100644 src/modules/auth/entities/user-profile.entity.ts create mode 100644 src/modules/auth/entities/verification-code.entity.ts diff --git a/src/modules/auth/entities/device.entity.ts b/src/modules/auth/entities/device.entity.ts new file mode 100644 index 0000000..f6b4f47 --- /dev/null +++ b/src/modules/auth/entities/device.entity.ts @@ -0,0 +1,64 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, + Index, +} from 'typeorm'; +import { Tenant } from './tenant.entity'; +import { User } from './user.entity'; + +@Entity({ schema: 'auth', name: 'devices' }) +@Index('idx_devices_tenant_id', ['tenantId']) +@Index('idx_devices_user_id', ['userId']) +@Index('idx_devices_device_id', ['deviceId']) +export class Device { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'device_id' }) + deviceId: string; + + @Column({ type: 'varchar', length: 255, nullable: true, name: 'device_name' }) + deviceName: string; + + @Column({ type: 'varchar', length: 50, nullable: false, name: 'device_type' }) + deviceType: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + platform: string; + + @Column({ type: 'varchar', length: 50, nullable: true, name: 'os_version' }) + osVersion: string; + + @Column({ type: 'varchar', length: 20, nullable: true, name: 'app_version' }) + appVersion: string; + + @Column({ type: 'text', nullable: true, name: 'push_token' }) + pushToken: string; + + @Column({ name: 'is_trusted', default: false }) + isTrusted: boolean; + + @Column({ type: 'timestamptz', nullable: true, name: 'last_active_at' }) + lastActiveAt: Date; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; +} diff --git a/src/modules/auth/entities/index.ts b/src/modules/auth/entities/index.ts index 0c4b204..6977b34 100644 --- a/src/modules/auth/entities/index.ts +++ b/src/modules/auth/entities/index.ts @@ -1,14 +1,38 @@ /** * Auth Entities - Export + * Updated: 2026-02-03 (ST-P3-008) */ -export { RefreshToken } from './refresh-token.entity'; +// Core entities +export { Tenant, TenantStatus } from './tenant.entity'; +export { User, UserStatus } from './user.entity'; export { Role } from './role.entity'; export { Permission } from './permission.entity'; export { UserRole } from './user-role.entity'; -export { User, UserStatus } from './user.entity'; export { Session, SessionStatus } from './session.entity'; -export { ApiKey } from './api-key.entity'; -export { PasswordReset } from './password-reset.entity'; export { Company } from './company.entity'; export { Group } from './group.entity'; + +// Authentication +export { RefreshToken } from './refresh-token.entity'; +export { ApiKey } from './api-key.entity'; +export { PasswordReset } from './password-reset.entity'; +export { VerificationCode, CodeType } from './verification-code.entity'; + +// Devices +export { Device } from './device.entity'; +export { TrustedDevice, TrustLevel } from './trusted-device.entity'; + +// MFA +export { MfaAuditLog, MfaEventType } from './mfa-audit-log.entity'; + +// OAuth +export { OAuthProvider } from './oauth-provider.entity'; +export { OAuthState } from './oauth-state.entity'; +export { OAuthUserLink } from './oauth-user-link.entity'; + +// User Profiles +export { UserProfile } from './user-profile.entity'; +export { ProfileTool } from './profile-tool.entity'; +export { ProfileModule } from './profile-module.entity'; +export { UserProfileAssignment } from './user-profile-assignment.entity'; diff --git a/src/modules/auth/entities/mfa-audit-log.entity.ts b/src/modules/auth/entities/mfa-audit-log.entity.ts new file mode 100644 index 0000000..9442a29 --- /dev/null +++ b/src/modules/auth/entities/mfa-audit-log.entity.ts @@ -0,0 +1,80 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +export enum MfaEventType { + MFA_SETUP_INITIATED = 'mfa_setup_initiated', + MFA_SETUP_COMPLETED = 'mfa_setup_completed', + MFA_DISABLED = 'mfa_disabled', + TOTP_VERIFIED = 'totp_verified', + TOTP_FAILED = 'totp_failed', + BACKUP_CODE_USED = 'backup_code_used', + BACKUP_CODES_REGENERATED = 'backup_codes_regenerated', + DEVICE_TRUSTED = 'device_trusted', + DEVICE_REVOKED = 'device_revoked', + ANOMALY_DETECTED = 'anomaly_detected', + ACCOUNT_LOCKED = 'account_locked', + ACCOUNT_UNLOCKED = 'account_unlocked', +} + +@Entity({ schema: 'auth', name: 'mfa_audit_log' }) +@Index('idx_mfa_audit_user', ['userId', 'createdAt']) +@Index('idx_mfa_audit_event', ['eventType', 'createdAt']) +@Index('idx_mfa_audit_failures', ['userId', 'createdAt'], { + where: 'success = FALSE', +}) +export class MfaAuditLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ + type: 'enum', + enum: MfaEventType, + nullable: false, + name: 'event_type', + }) + eventType: MfaEventType; + + @Column({ type: 'boolean', nullable: false }) + success: boolean; + + @Column({ type: 'varchar', length: 128, nullable: true, name: 'failure_reason' }) + failureReason: string | null; + + @Column({ type: 'inet', nullable: true, name: 'ip_address' }) + ipAddress: string | null; + + @Column({ type: 'text', nullable: true, name: 'user_agent' }) + userAgent: string | null; + + @Column({ + type: 'varchar', + length: 128, + nullable: true, + name: 'device_fingerprint', + }) + deviceFingerprint: string | null; + + @Column({ type: 'jsonb', nullable: true }) + location: Record | null; + + @Column({ type: 'jsonb', default: {}, nullable: true }) + metadata: Record; + + @ManyToOne(() => User) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/auth/entities/oauth-provider.entity.ts b/src/modules/auth/entities/oauth-provider.entity.ts new file mode 100644 index 0000000..f22e299 --- /dev/null +++ b/src/modules/auth/entities/oauth-provider.entity.ts @@ -0,0 +1,181 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Tenant } from './tenant.entity'; +import { User } from './user.entity'; +import { Role } from './role.entity'; + +@Entity({ schema: 'auth', name: 'oauth_providers' }) +@Index('idx_oauth_providers_enabled', ['isEnabled']) +@Index('idx_oauth_providers_tenant', ['tenantId']) +@Index('idx_oauth_providers_code', ['code']) +export class OAuthProvider { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: true, name: 'tenant_id' }) + tenantId: string | null; + + @Column({ type: 'varchar', length: 50, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'client_id' }) + clientId: string; + + @Column({ type: 'varchar', length: 500, nullable: true, name: 'client_secret' }) + clientSecret: string | null; + + @Column({ + type: 'varchar', + length: 500, + nullable: false, + name: 'authorization_endpoint', + }) + authorizationEndpoint: string; + + @Column({ + type: 'varchar', + length: 500, + nullable: false, + name: 'token_endpoint', + }) + tokenEndpoint: string; + + @Column({ + type: 'varchar', + length: 500, + nullable: false, + name: 'userinfo_endpoint', + }) + userinfoEndpoint: string; + + @Column({ type: 'varchar', length: 500, nullable: true, name: 'jwks_uri' }) + jwksUri: string | null; + + @Column({ + type: 'varchar', + length: 500, + default: 'openid profile email', + nullable: false, + }) + scope: string; + + @Column({ + type: 'varchar', + length: 50, + default: 'code', + nullable: false, + name: 'response_type', + }) + responseType: string; + + @Column({ + type: 'boolean', + default: true, + nullable: false, + name: 'pkce_enabled', + }) + pkceEnabled: boolean; + + @Column({ + type: 'varchar', + length: 10, + default: 'S256', + nullable: true, + name: 'code_challenge_method', + }) + codeChallengeMethod: string | null; + + @Column({ + type: 'jsonb', + nullable: false, + name: 'claim_mapping', + default: { + sub: 'oauth_uid', + email: 'email', + name: 'name', + picture: 'avatar_url', + }, + }) + claimMapping: Record; + + @Column({ type: 'varchar', length: 100, nullable: true, name: 'icon_class' }) + iconClass: string | null; + + @Column({ type: 'varchar', length: 100, nullable: true, name: 'button_text' }) + buttonText: string | null; + + @Column({ type: 'varchar', length: 20, nullable: true, name: 'button_color' }) + buttonColor: string | null; + + @Column({ + type: 'integer', + default: 10, + nullable: false, + name: 'display_order', + }) + displayOrder: number; + + @Column({ type: 'boolean', default: false, nullable: false, name: 'is_enabled' }) + isEnabled: boolean; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_visible' }) + isVisible: boolean; + + @Column({ + type: 'text', + array: true, + nullable: true, + name: 'allowed_domains', + }) + allowedDomains: string[] | null; + + @Column({ + type: 'boolean', + default: false, + nullable: false, + name: 'auto_create_users', + }) + autoCreateUsers: boolean; + + @Column({ type: 'uuid', nullable: true, name: 'default_role_id' }) + defaultRoleId: string | null; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant | null; + + @ManyToOne(() => Role, { nullable: true }) + @JoinColumn({ name: 'default_role_id' }) + defaultRole: Role | null; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'created_by' }) + createdByUser: User | null; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'updated_by' }) + updatedByUser: User | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'created_by' }) + createdBy: string | null; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'updated_by' }) + updatedBy: string | null; +} diff --git a/src/modules/auth/entities/oauth-state.entity.ts b/src/modules/auth/entities/oauth-state.entity.ts new file mode 100644 index 0000000..8973a16 --- /dev/null +++ b/src/modules/auth/entities/oauth-state.entity.ts @@ -0,0 +1,60 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { OAuthProvider } from './oauth-provider.entity'; +import { User } from './user.entity'; + +@Entity({ schema: 'auth', name: 'oauth_states' }) +@Index('idx_oauth_states_state', ['state']) +@Index('idx_oauth_states_expires', ['expiresAt']) +export class OAuthState { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 64, nullable: false, unique: true }) + state: string; + + @Column({ type: 'varchar', length: 128, nullable: true, name: 'code_verifier' }) + codeVerifier: string | null; + + @Column({ type: 'uuid', nullable: false, name: 'provider_id' }) + providerId: string; + + @Column({ type: 'varchar', length: 500, nullable: false, name: 'redirect_uri' }) + redirectUri: string; + + @Column({ type: 'varchar', length: 500, nullable: true, name: 'return_url' }) + returnUrl: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'link_user_id' }) + linkUserId: string | null; + + @Column({ type: 'inet', nullable: true, name: 'ip_address' }) + ipAddress: string | null; + + @Column({ type: 'text', nullable: true, name: 'user_agent' }) + userAgent: string | null; + + @ManyToOne(() => OAuthProvider) + @JoinColumn({ name: 'provider_id' }) + provider: OAuthProvider; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'link_user_id' }) + linkUser: User | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: false, name: 'expires_at' }) + expiresAt: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'used_at' }) + usedAt: Date | null; +} diff --git a/src/modules/auth/entities/oauth-user-link.entity.ts b/src/modules/auth/entities/oauth-user-link.entity.ts new file mode 100644 index 0000000..9eff012 --- /dev/null +++ b/src/modules/auth/entities/oauth-user-link.entity.ts @@ -0,0 +1,68 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; +import { OAuthProvider } from './oauth-provider.entity'; + +@Entity({ schema: 'auth', name: 'oauth_user_links' }) +@Index('idx_oauth_links_user', ['userId']) +@Index('idx_oauth_links_provider', ['providerId']) +@Index('idx_oauth_links_oauth_uid', ['oauthUid']) +export class OAuthUserLink { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'uuid', nullable: false, name: 'provider_id' }) + providerId: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'oauth_uid' }) + oauthUid: string; + + @Column({ type: 'varchar', length: 255, nullable: true, name: 'oauth_email' }) + oauthEmail: string | null; + + @Column({ type: 'text', nullable: true, name: 'access_token' }) + accessToken: string | null; + + @Column({ type: 'text', nullable: true, name: 'refresh_token' }) + refreshToken: string | null; + + @Column({ type: 'text', nullable: true, name: 'id_token' }) + idToken: string | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'token_expires_at' }) + tokenExpiresAt: Date | null; + + @Column({ type: 'jsonb', nullable: true, name: 'raw_userinfo' }) + rawUserinfo: Record | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'last_login_at' }) + lastLoginAt: Date | null; + + @Column({ type: 'integer', default: 0, nullable: false, name: 'login_count' }) + loginCount: number; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => OAuthProvider, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'provider_id' }) + provider: OAuthProvider; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/auth/entities/profile-module.entity.ts b/src/modules/auth/entities/profile-module.entity.ts new file mode 100644 index 0000000..79bd23a --- /dev/null +++ b/src/modules/auth/entities/profile-module.entity.ts @@ -0,0 +1,27 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { UserProfile } from './user-profile.entity'; + +@Entity({ schema: 'auth', name: 'profile_modules' }) +export class ProfileModule { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'profile_id' }) + profileId: string; + + @Column({ type: 'varchar', length: 50, nullable: false, name: 'module_code' }) + moduleCode: string; + + @Column({ name: 'is_enabled', default: true }) + isEnabled: boolean; + + @ManyToOne(() => UserProfile, (p) => p.modules, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'profile_id' }) + profile: UserProfile; +} diff --git a/src/modules/auth/entities/profile-tool.entity.ts b/src/modules/auth/entities/profile-tool.entity.ts new file mode 100644 index 0000000..c74bf88 --- /dev/null +++ b/src/modules/auth/entities/profile-tool.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { UserProfile } from './user-profile.entity'; + +@Entity({ schema: 'auth', name: 'profile_tools' }) +export class ProfileTool { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'profile_id' }) + profileId: string; + + @Column({ type: 'varchar', length: 50, nullable: false, name: 'tool_code' }) + toolCode: string; + + @Column({ name: 'can_view', default: false }) + canView: boolean; + + @Column({ name: 'can_create', default: false }) + canCreate: boolean; + + @Column({ name: 'can_edit', default: false }) + canEdit: boolean; + + @Column({ name: 'can_delete', default: false }) + canDelete: boolean; + + @ManyToOne(() => UserProfile, (p) => p.tools, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'profile_id' }) + profile: UserProfile; +} diff --git a/src/modules/auth/entities/tenant.entity.ts b/src/modules/auth/entities/tenant.entity.ts new file mode 100644 index 0000000..9841183 --- /dev/null +++ b/src/modules/auth/entities/tenant.entity.ts @@ -0,0 +1,91 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + OneToMany, +} from 'typeorm'; +import { Company } from './company.entity'; +import { User } from './user.entity'; +import { Role } from './role.entity'; + +export enum TenantStatus { + ACTIVE = 'active', + SUSPENDED = 'suspended', + TRIAL = 'trial', + CANCELLED = 'cancelled', +} + +@Entity({ schema: 'auth', name: 'tenants' }) +@Index('idx_tenants_subdomain', ['subdomain']) +@Index('idx_tenants_status', ['status'], { where: 'deleted_at IS NULL' }) +@Index('idx_tenants_created_at', ['createdAt']) +export class Tenant { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 100, unique: true, nullable: false }) + subdomain: string; + + @Column({ + type: 'varchar', + length: 100, + unique: true, + nullable: false, + name: 'schema_name', + }) + schemaName: string; + + @Column({ + type: 'enum', + enum: TenantStatus, + default: TenantStatus.ACTIVE, + nullable: false, + }) + status: TenantStatus; + + @Column({ type: 'jsonb', default: {} }) + settings: Record; + + @Column({ type: 'varchar', length: 50, default: 'basic', nullable: true }) + plan: string; + + @Column({ type: 'integer', default: 10, name: 'max_users' }) + maxUsers: number; + + @OneToMany(() => Company, (company) => company.tenant) + companies: Company[]; + + @OneToMany(() => User, (user) => user.tenant) + users: User[]; + + @OneToMany(() => Role, (role) => role.tenant) + roles: Role[]; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @Column({ type: 'uuid', nullable: true, name: 'created_by' }) + createdBy: string | null; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + }) + updatedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'updated_by' }) + updatedBy: string | null; + + @Column({ type: 'timestamp', nullable: true, name: 'deleted_at' }) + deletedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'deleted_by' }) + deletedBy: string | null; +} diff --git a/src/modules/auth/entities/trusted-device.entity.ts b/src/modules/auth/entities/trusted-device.entity.ts new file mode 100644 index 0000000..fb08f37 --- /dev/null +++ b/src/modules/auth/entities/trusted-device.entity.ts @@ -0,0 +1,107 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +export enum TrustLevel { + STANDARD = 'standard', + HIGH = 'high', + TEMPORARY = 'temporary', +} + +@Entity({ schema: 'auth', name: 'trusted_devices' }) +@Index('idx_trusted_devices_user', ['userId'], { where: 'is_active' }) +@Index('idx_trusted_devices_fingerprint', ['deviceFingerprint']) +@Index('idx_trusted_devices_expires', ['trustExpiresAt'], { + where: 'trust_expires_at IS NOT NULL AND is_active', +}) +export class TrustedDevice { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ + type: 'varchar', + length: 128, + nullable: false, + name: 'device_fingerprint', + }) + deviceFingerprint: string; + + @Column({ type: 'varchar', length: 128, nullable: true, name: 'device_name' }) + deviceName: string | null; + + @Column({ type: 'varchar', length: 32, nullable: true, name: 'device_type' }) + deviceType: string | null; + + @Column({ type: 'text', nullable: true, name: 'user_agent' }) + userAgent: string | null; + + @Column({ type: 'varchar', length: 64, nullable: true, name: 'browser_name' }) + browserName: string | null; + + @Column({ + type: 'varchar', + length: 32, + nullable: true, + name: 'browser_version', + }) + browserVersion: string | null; + + @Column({ type: 'varchar', length: 64, nullable: true, name: 'os_name' }) + osName: string | null; + + @Column({ type: 'varchar', length: 32, nullable: true, name: 'os_version' }) + osVersion: string | null; + + @Column({ type: 'inet', nullable: false, name: 'registered_ip' }) + registeredIp: string; + + @Column({ type: 'jsonb', nullable: true, name: 'registered_location' }) + registeredLocation: Record | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @Column({ + type: 'enum', + enum: TrustLevel, + default: TrustLevel.STANDARD, + nullable: false, + name: 'trust_level', + }) + trustLevel: TrustLevel; + + @Column({ type: 'timestamptz', nullable: true, name: 'trust_expires_at' }) + trustExpiresAt: Date | null; + + @Column({ type: 'timestamptz', nullable: false, name: 'last_used_at' }) + lastUsedAt: Date; + + @Column({ type: 'inet', nullable: true, name: 'last_used_ip' }) + lastUsedIp: string | null; + + @Column({ type: 'integer', default: 1, nullable: false, name: 'use_count' }) + useCount: number; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'revoked_at' }) + revokedAt: Date | null; + + @Column({ type: 'varchar', length: 128, nullable: true, name: 'revoked_reason' }) + revokedReason: string | null; +} diff --git a/src/modules/auth/entities/user-profile-assignment.entity.ts b/src/modules/auth/entities/user-profile-assignment.entity.ts new file mode 100644 index 0000000..b9c7dec --- /dev/null +++ b/src/modules/auth/entities/user-profile-assignment.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { User } from './user.entity'; +import { UserProfile } from './user-profile.entity'; + +@Entity({ schema: 'auth', name: 'user_profile_assignments' }) +export class UserProfileAssignment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'uuid', nullable: false, name: 'profile_id' }) + profileId: string; + + @Column({ name: 'is_default', default: false }) + isDefault: boolean; + + @CreateDateColumn({ name: 'assigned_at', type: 'timestamp' }) + assignedAt: Date; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => UserProfile, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'profile_id' }) + profile: UserProfile; +} diff --git a/src/modules/auth/entities/user-profile.entity.ts b/src/modules/auth/entities/user-profile.entity.ts new file mode 100644 index 0000000..3090a07 --- /dev/null +++ b/src/modules/auth/entities/user-profile.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + JoinColumn, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; +import { Tenant } from './tenant.entity'; +import { ProfileTool } from './profile-tool.entity'; +import { ProfileModule } from './profile-module.entity'; + +@Entity({ schema: 'auth', name: 'user_profiles' }) +@Index('idx_user_profiles_tenant_id', ['tenantId']) +export class UserProfile { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 10, nullable: false }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ name: 'is_active', default: true }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', nullable: true }) + updatedAt: Date; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @OneToMany(() => ProfileTool, (pt) => pt.profile) + tools: ProfileTool[]; + + @OneToMany(() => ProfileModule, (pm) => pm.profile) + modules: ProfileModule[]; +} diff --git a/src/modules/auth/entities/verification-code.entity.ts b/src/modules/auth/entities/verification-code.entity.ts new file mode 100644 index 0000000..c828735 --- /dev/null +++ b/src/modules/auth/entities/verification-code.entity.ts @@ -0,0 +1,82 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from './user.entity'; +import { Session } from './session.entity'; + +export enum CodeType { + TOTP_SETUP = 'totp_setup', + SMS = 'sms', + EMAIL = 'email', + BACKUP = 'backup', +} + +@Entity({ schema: 'auth', name: 'verification_codes' }) +@Index('idx_verification_codes_user', ['userId', 'codeType'], { + where: 'used_at IS NULL', +}) +@Index('idx_verification_codes_expires', ['expiresAt'], { + where: 'used_at IS NULL', +}) +export class VerificationCode { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'uuid', nullable: true, name: 'session_id' }) + sessionId: string | null; + + @Column({ + type: 'enum', + enum: CodeType, + nullable: false, + name: 'code_type', + }) + codeType: CodeType; + + @Column({ type: 'varchar', length: 64, nullable: false, name: 'code_hash' }) + codeHash: string; + + @Column({ type: 'integer', default: 6, nullable: false, name: 'code_length' }) + codeLength: number; + + @Column({ type: 'varchar', length: 256, nullable: true }) + destination: string | null; + + @Column({ type: 'integer', default: 0, nullable: false }) + attempts: number; + + @Column({ type: 'integer', default: 5, nullable: false, name: 'max_attempts' }) + maxAttempts: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: false, name: 'expires_at' }) + expiresAt: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'used_at' }) + usedAt: Date | null; + + @Column({ type: 'inet', nullable: true, name: 'ip_address' }) + ipAddress: string | null; + + @Column({ type: 'text', nullable: true, name: 'user_agent' }) + userAgent: string | null; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Session, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'session_id' }) + session: Session | null; +}