diff --git a/src/modules/auth/entities/company.entity.ts b/src/modules/auth/entities/company.entity.ts index 22b919c..38e7eca 100644 --- a/src/modules/auth/entities/company.entity.ts +++ b/src/modules/auth/entities/company.entity.ts @@ -14,9 +14,11 @@ import { UpdateDateColumn, Index, ManyToOne, + ManyToMany, JoinColumn, } from 'typeorm'; import { Tenant } from '../../core/entities/tenant.entity'; +import { User } from './user.entity'; @Entity({ schema: 'auth', name: 'companies' }) @Index('idx_companies_tenant_id', ['tenantId']) @@ -59,6 +61,9 @@ export class Company { @JoinColumn({ name: 'parent_company_id' }) parentCompany: Company | null; + @ManyToMany(() => User, (user) => user.companies) + users: User[]; + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) createdAt: Date; diff --git a/src/modules/auth/entities/index.ts b/src/modules/auth/entities/index.ts index 1dd8bd8..0c4b204 100644 --- a/src/modules/auth/entities/index.ts +++ b/src/modules/auth/entities/index.ts @@ -6,6 +6,7 @@ export { RefreshToken } from './refresh-token.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'; diff --git a/src/modules/auth/entities/password-reset.entity.ts b/src/modules/auth/entities/password-reset.entity.ts index b8d5ac5..4b8bcc3 100644 --- a/src/modules/auth/entities/password-reset.entity.ts +++ b/src/modules/auth/entities/password-reset.entity.ts @@ -15,7 +15,7 @@ import { ManyToOne, JoinColumn, } from 'typeorm'; -import { User } from '../../core/entities/user.entity'; +import { User } from './user.entity'; @Entity({ schema: 'auth', name: 'password_resets' }) @Index('idx_password_resets_user_id', ['userId']) @@ -40,7 +40,7 @@ export class PasswordReset { @Column({ type: 'inet', nullable: true, name: 'ip_address' }) ipAddress: string | null; - @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @ManyToOne(() => User, (user) => user.passwordResets, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) user: User; diff --git a/src/modules/auth/entities/role.entity.ts b/src/modules/auth/entities/role.entity.ts index 08f5065..2612081 100644 --- a/src/modules/auth/entities/role.entity.ts +++ b/src/modules/auth/entities/role.entity.ts @@ -22,6 +22,7 @@ import { import { Permission } from './permission.entity'; import { UserRole } from './user-role.entity'; import { Tenant } from '../../core/entities/tenant.entity'; +import { User } from './user.entity'; @Entity({ schema: 'auth', name: 'roles' }) @Index('idx_roles_tenant_id', ['tenantId']) @@ -69,6 +70,9 @@ export class Role { @OneToMany(() => UserRole, (userRole) => userRole.role) userRoles: UserRole[]; + @ManyToMany(() => User, (user) => user.roles) + users: User[]; + // Audit trail @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) createdAt: Date; diff --git a/src/modules/auth/entities/session.entity.ts b/src/modules/auth/entities/session.entity.ts index 346ffdd..ddf5904 100644 --- a/src/modules/auth/entities/session.entity.ts +++ b/src/modules/auth/entities/session.entity.ts @@ -15,7 +15,7 @@ import { ManyToOne, JoinColumn, } from 'typeorm'; -import { User } from '../../core/entities/user.entity'; +import { User } from './user.entity'; export enum SessionStatus { ACTIVE = 'active', @@ -70,7 +70,7 @@ export class Session { @Column({ type: 'jsonb', nullable: true, name: 'device_info' }) deviceInfo: Record | null; - @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @ManyToOne(() => User, (user) => user.sessions, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) user: User; diff --git a/src/modules/auth/entities/user.entity.ts b/src/modules/auth/entities/user.entity.ts new file mode 100644 index 0000000..6d5b6bf --- /dev/null +++ b/src/modules/auth/entities/user.entity.ts @@ -0,0 +1,167 @@ +/** + * User Entity + * Usuarios del sistema con autenticacion y permisos + * Compatible con erp-core user.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, + OneToMany, +} from 'typeorm'; +import { Tenant } from '../../core/entities/tenant.entity'; +import { Role } from './role.entity'; +import { Company } from './company.entity'; +import { Session } from './session.entity'; +import { PasswordReset } from './password-reset.entity'; + +export enum UserStatus { + ACTIVE = 'active', + INACTIVE = 'inactive', + SUSPENDED = 'suspended', + PENDING_VERIFICATION = 'pending_verification', +} + +@Entity({ schema: 'auth', name: 'users' }) +@Index('idx_users_tenant_id', ['tenantId']) +@Index('idx_users_email', ['email']) +@Index('idx_users_status', ['status'], { where: 'deleted_at IS NULL' }) +@Index('idx_users_email_tenant', ['tenantId', 'email']) +@Index('idx_users_created_at', ['createdAt']) +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + email: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'password_hash' }) + passwordHash: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'full_name' }) + fullName: string; + + @Column({ type: 'varchar', length: 500, nullable: true, name: 'avatar_url' }) + avatarUrl: string | null; + + @Column({ + type: 'enum', + enum: UserStatus, + default: UserStatus.ACTIVE, + nullable: false, + }) + status: UserStatus; + + @Column({ type: 'boolean', default: false, nullable: false, name: 'is_superuser' }) + isSuperuser: boolean; + + @Column({ name: 'is_superadmin', default: false }) + isSuperadmin: boolean; + + @Column({ name: 'mfa_enabled', default: false }) + mfaEnabled: boolean; + + @Column({ name: 'mfa_secret_encrypted', type: 'text', nullable: true }) + mfaSecretEncrypted: string; + + @Column({ name: 'mfa_backup_codes', type: 'text', array: true, nullable: true }) + mfaBackupCodes: string[]; + + @Column({ name: 'oauth_provider', length: 50, nullable: true }) + oauthProvider: string; + + @Column({ name: 'oauth_provider_id', length: 255, nullable: true }) + oauthProviderId: string; + + @Column({ + type: 'timestamp', + nullable: true, + name: 'email_verified_at', + }) + emailVerifiedAt: Date | null; + + @Column({ type: 'timestamp', nullable: true, name: 'last_login_at' }) + lastLoginAt: Date | null; + + @Column({ type: 'inet', nullable: true, name: 'last_login_ip' }) + lastLoginIp: string | null; + + @Column({ type: 'integer', default: 0, name: 'login_count' }) + loginCount: number; + + @Column({ type: 'varchar', length: 10, default: 'es' }) + language: string; + + @Column({ type: 'varchar', length: 50, default: 'America/Mexico_City' }) + timezone: string; + + @Column({ type: 'jsonb', default: {} }) + settings: Record; + + // Relaciones + @ManyToOne(() => Tenant, (tenant) => tenant.users, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @ManyToMany(() => Role, (role) => role.users) + @JoinTable({ + name: 'user_roles', + schema: 'auth', + joinColumn: { name: 'user_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' }, + }) + roles: Role[]; + + @ManyToMany(() => Company, (company) => company.users) + @JoinTable({ + name: 'user_companies', + schema: 'auth', + joinColumn: { name: 'user_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'company_id', referencedColumnName: 'id' }, + }) + companies: Company[]; + + @OneToMany(() => Session, (session) => session.user) + sessions: Session[]; + + @OneToMany(() => PasswordReset, (passwordReset) => passwordReset.user) + passwordResets: PasswordReset[]; + + // Auditoria + @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; +}