diff --git a/src/modules/auth/entities/api-key.entity.ts b/src/modules/auth/entities/api-key.entity.ts new file mode 100644 index 0000000..fe825a9 --- /dev/null +++ b/src/modules/auth/entities/api-key.entity.ts @@ -0,0 +1,84 @@ +/** + * ApiKey Entity + * Gestión de API Keys para integraciones headless/CI/CD + * Compatible con erp-core api-key.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../core/entities/user.entity'; +import { Tenant } from '../../core/entities/tenant.entity'; + +@Entity({ schema: 'auth', name: 'api_keys' }) +@Index('idx_api_keys_lookup', ['keyIndex', 'isActive'], { + where: 'is_active = TRUE', +}) +@Index('idx_api_keys_expiration', ['expirationDate'], { + where: 'expiration_date IS NOT NULL', +}) +@Index('idx_api_keys_user', ['userId']) +@Index('idx_api_keys_tenant', ['tenantId']) +export class ApiKey { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 16, nullable: false, name: 'key_index' }) + keyIndex: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'key_hash' }) + keyHash: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + scope: string | null; + + @Column({ type: 'inet', array: true, nullable: true, name: 'allowed_ips' }) + allowedIps: string[] | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'expiration_date' }) + expirationDate: Date | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'last_used_at' }) + lastUsedAt: Date | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'revoked_by' }) + revokedByUser: User | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'revoked_at' }) + revokedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'revoked_by' }) + revokedBy: string | null; +} diff --git a/src/modules/auth/entities/company.entity.ts b/src/modules/auth/entities/company.entity.ts new file mode 100644 index 0000000..22b919c --- /dev/null +++ b/src/modules/auth/entities/company.entity.ts @@ -0,0 +1,79 @@ +/** + * Company Entity + * Soporte multi-empresa dentro de un tenant + * Compatible con erp-core company.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Tenant } from '../../core/entities/tenant.entity'; + +@Entity({ schema: 'auth', name: 'companies' }) +@Index('idx_companies_tenant_id', ['tenantId']) +@Index('idx_companies_parent_company_id', ['parentCompanyId']) +@Index('idx_companies_active', ['tenantId'], { where: 'deleted_at IS NULL' }) +@Index('idx_companies_tax_id', ['taxId']) +export class Company { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 255, nullable: true, name: 'legal_name' }) + legalName: string | null; + + @Column({ type: 'varchar', length: 50, nullable: true, name: 'tax_id' }) + taxId: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'currency_id' }) + currencyId: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'parent_company_id' }) + parentCompanyId: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'partner_id' }) + partnerId: string | null; + + @Column({ type: 'jsonb', default: {} }) + settings: Record; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @ManyToOne(() => Company, { nullable: true }) + @JoinColumn({ name: 'parent_company_id' }) + parentCompany: Company | null; + + @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/group.entity.ts b/src/modules/auth/entities/group.entity.ts new file mode 100644 index 0000000..e2b6b3c --- /dev/null +++ b/src/modules/auth/entities/group.entity.ts @@ -0,0 +1,75 @@ +/** + * Group Entity + * Agrupación de usuarios para gestión de permisos + * Compatible con erp-core group.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Tenant } from '../../core/entities/tenant.entity'; + +@Entity({ schema: 'auth', name: 'groups' }) +@Index('idx_groups_tenant_id', ['tenantId']) +@Index('idx_groups_code', ['code']) +@Index('idx_groups_category', ['category']) +@Index('idx_groups_is_system', ['isSystem']) +export class Group { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'boolean', default: false, nullable: false, name: 'is_system' }) + isSystem: boolean; + + @Column({ type: 'varchar', length: 100, nullable: true }) + category: string | null; + + @Column({ type: 'varchar', length: 20, nullable: true }) + color: string | null; + + @Column({ type: 'integer', default: 30, nullable: true, name: 'api_key_max_duration_days' }) + apiKeyMaxDurationDays: number | null; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @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/index.ts b/src/modules/auth/entities/index.ts new file mode 100644 index 0000000..ebcc69e --- /dev/null +++ b/src/modules/auth/entities/index.ts @@ -0,0 +1,15 @@ +/** + * Auth Entities - Export + */ + +export { RefreshToken } from './refresh-token.entity'; +export { User } from './user.entity'; +export { Workshop } from './workshop.entity'; +export { Role } from './role.entity'; +export { Permission } from './permission.entity'; +export { UserRole } from './user-role.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'; diff --git a/src/modules/auth/entities/password-reset.entity.ts b/src/modules/auth/entities/password-reset.entity.ts new file mode 100644 index 0000000..b8d5ac5 --- /dev/null +++ b/src/modules/auth/entities/password-reset.entity.ts @@ -0,0 +1,49 @@ +/** + * PasswordReset Entity + * Flujo seguro de recuperación de contraseña con token temporal + * Compatible con erp-core password-reset.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../core/entities/user.entity'; + +@Entity({ schema: 'auth', name: 'password_resets' }) +@Index('idx_password_resets_user_id', ['userId']) +@Index('idx_password_resets_token', ['token']) +@Index('idx_password_resets_expires_at', ['expiresAt']) +export class PasswordReset { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'varchar', length: 500, unique: true, nullable: false }) + token: string; + + @Column({ type: 'timestamp', nullable: false, name: 'expires_at' }) + expiresAt: Date; + + @Column({ type: 'timestamp', nullable: true, name: 'used_at' }) + usedAt: Date | null; + + @Column({ type: 'inet', nullable: true, name: 'ip_address' }) + ipAddress: string | null; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; +} diff --git a/src/modules/auth/entities/permission.entity.ts b/src/modules/auth/entities/permission.entity.ts new file mode 100644 index 0000000..8599af9 --- /dev/null +++ b/src/modules/auth/entities/permission.entity.ts @@ -0,0 +1,34 @@ +/** + * Permission Entity + * Permisos granulares del sistema + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +@Entity({ schema: 'auth', name: 'permissions' }) +export class Permission { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 100, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 200 }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @Column({ type: 'varchar', length: 50 }) + module: string; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/auth/entities/role.entity.ts b/src/modules/auth/entities/role.entity.ts new file mode 100644 index 0000000..08f5065 --- /dev/null +++ b/src/modules/auth/entities/role.entity.ts @@ -0,0 +1,90 @@ +/** + * Role Entity + * Roles del sistema para RBAC + * Compatible con erp-core role.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + ManyToMany, + ManyToOne, + JoinColumn, + JoinTable, + Index, +} from 'typeorm'; +import { Permission } from './permission.entity'; +import { UserRole } from './user-role.entity'; +import { Tenant } from '../../core/entities/tenant.entity'; + +@Entity({ schema: 'auth', name: 'roles' }) +@Index('idx_roles_tenant_id', ['tenantId']) +@Index('idx_roles_code', ['code']) +@Index('idx_roles_is_system', ['isSystem']) +export class Role { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: true, name: 'tenant_id' }) + tenantId: string | null; + + @Column({ type: 'varchar', length: 50, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100 }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ name: 'is_system', type: 'boolean', default: false }) + isSystem: boolean; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive: boolean; + + @Column({ type: 'varchar', length: 20, nullable: true }) + color: string | null; + + // Relations + @ManyToOne(() => Tenant, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant | null; + + @ManyToMany(() => Permission) + @JoinTable({ + name: 'role_permissions', + schema: 'auth', + joinColumn: { name: 'role_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' }, + }) + permissions: Permission[]; + + @OneToMany(() => UserRole, (userRole) => userRole.role) + userRoles: UserRole[]; + + // Audit trail + @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; + + @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) + deletedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'deleted_by' }) + deletedBy: string | null; +} diff --git a/src/modules/auth/entities/session.entity.ts b/src/modules/auth/entities/session.entity.ts new file mode 100644 index 0000000..346ffdd --- /dev/null +++ b/src/modules/auth/entities/session.entity.ts @@ -0,0 +1,85 @@ +/** + * Session Entity + * Gestión de sesiones de usuario (reemplaza refresh tokens simples) + * Compatible con erp-core session.entity + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../../core/entities/user.entity'; + +export enum SessionStatus { + ACTIVE = 'active', + EXPIRED = 'expired', + REVOKED = 'revoked', +} + +@Entity({ schema: 'auth', name: 'sessions' }) +@Index('idx_sessions_user_id', ['userId']) +@Index('idx_sessions_token', ['token']) +@Index('idx_sessions_status', ['status']) +@Index('idx_sessions_expires_at', ['expiresAt']) +export class Session { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'varchar', length: 500, unique: true, nullable: false }) + token: string; + + @Column({ + type: 'varchar', + length: 500, + unique: true, + nullable: true, + name: 'refresh_token', + }) + refreshToken: string | null; + + @Column({ + type: 'enum', + enum: SessionStatus, + default: SessionStatus.ACTIVE, + nullable: false, + }) + status: SessionStatus; + + @Column({ type: 'timestamp', nullable: false, name: 'expires_at' }) + expiresAt: Date; + + @Column({ type: 'timestamp', nullable: true, name: 'refresh_expires_at' }) + refreshExpiresAt: Date | 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: 'jsonb', nullable: true, name: 'device_info' }) + deviceInfo: Record | null; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + @Column({ type: 'timestamp', nullable: true, name: 'revoked_at' }) + revokedAt: Date | null; + + @Column({ type: 'varchar', length: 100, nullable: true, name: 'revoked_reason' }) + revokedReason: string | null; +} diff --git a/src/modules/auth/entities/user-role.entity.ts b/src/modules/auth/entities/user-role.entity.ts new file mode 100644 index 0000000..9a6ea4b --- /dev/null +++ b/src/modules/auth/entities/user-role.entity.ts @@ -0,0 +1,54 @@ +/** + * UserRole Entity + * Relación usuarios-roles con soporte multi-tenant + * + * @module Auth + */ + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + Index, +} from 'typeorm'; +import { User } from '../../core/entities/user.entity'; +import { Role } from './role.entity'; +import { Tenant } from '../../core/entities/tenant.entity'; + +@Entity({ schema: 'auth', name: 'user_roles' }) +@Index(['userId', 'roleId', 'tenantId'], { unique: true }) +export class UserRole { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @Column({ name: 'role_id', type: 'uuid' }) + roleId: string; + + @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) + tenantId: string; + + @Column({ name: 'assigned_by', type: 'uuid', nullable: true }) + assignedBy: string; + + @CreateDateColumn({ name: 'assigned_at', type: 'timestamptz' }) + assignedAt: Date; + + // Relations + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Role, (role) => role.userRoles, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'role_id' }) + role: Role; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; +}