gcx/backend/services/auth-service/src/domain/entities/user.entity.ts

150 lines
3.7 KiB
TypeScript

import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
VersionColumn,
Index,
} from 'typeorm';
export enum UserRole {
USER = 'user',
ISSUER = 'issuer',
MARKET_MAKER = 'market_maker',
ADMIN = 'admin',
}
export enum UserStatus {
ACTIVE = 'active',
FROZEN = 'frozen',
DELETED = 'deleted',
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index('idx_users_phone')
@Column({ type: 'varchar', length: 20, unique: true, nullable: true })
phone: string | null;
@Index('idx_users_email')
@Column({ type: 'varchar', length: 100, unique: true, nullable: true })
email: string | null;
@Column({ name: 'password_hash', type: 'varchar', length: 255 })
passwordHash: string;
@Column({ type: 'varchar', length: 50, nullable: true })
nickname: string | null;
@Column({ name: 'avatar_url', type: 'varchar', length: 500, nullable: true })
avatarUrl: string | null;
@Column({ name: 'kyc_level', type: 'smallint', default: 0 })
kycLevel: number;
@Column({
name: 'wallet_mode',
type: 'varchar',
length: 10,
default: 'standard',
})
walletMode: 'standard' | 'external' | 'pro';
@Index('idx_users_role')
@Column({
type: 'varchar',
length: 20,
default: UserRole.USER,
})
role: UserRole;
@Index('idx_users_status')
@Column({
type: 'varchar',
length: 20,
default: UserStatus.ACTIVE,
})
status: UserStatus;
@Column({ name: 'residence_state', type: 'varchar', length: 5, nullable: true })
residenceState: string | null;
@Column({ type: 'varchar', length: 5, nullable: true })
nationality: string | null;
@Column({ name: 'login_fail_count', type: 'int', default: 0 })
loginFailCount: number;
@Column({ name: 'locked_until', type: 'timestamptz', nullable: true })
lockedUntil: Date | null;
@Column({ name: 'last_login_at', type: 'timestamptz', nullable: true })
lastLoginAt: Date | null;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@VersionColumn({ default: 1 })
version: number;
/* ── Domain Methods: Account Lockout ── */
/** 账号是否处于锁定状态 */
get isLocked(): boolean {
return this.lockedUntil !== null && new Date() < this.lockedUntil;
}
/** 是否可以登录 (状态正常且未锁定) */
get canLogin(): boolean {
return this.status === UserStatus.ACTIVE && !this.isLocked;
}
/** 剩余锁定秒数 (0 = 未锁定) */
get lockRemainingSeconds(): number {
if (!this.lockedUntil) return 0;
const diff = this.lockedUntil.getTime() - Date.now();
return diff > 0 ? Math.ceil(diff / 1000) : 0;
}
/**
* 记录登录成功 — 清除失败计数
*/
recordLoginSuccess(ip?: string): void {
this.loginFailCount = 0;
this.lockedUntil = null;
this.lastLoginAt = new Date();
}
/**
* 记录登录失败 — 指数退避锁定
* @param maxAttempts 触发锁定的失败次数阈值 (默认 6)
* @returns 剩余尝试次数 & 锁定信息
*/
recordLoginFailure(maxAttempts = 6): {
remainingAttempts: number;
lockMinutes?: number;
} {
this.loginFailCount += 1;
if (this.loginFailCount < maxAttempts) {
return { remainingAttempts: maxAttempts - this.loginFailCount };
}
// 指数退避: 2^(failCount - maxAttempts) 分钟,最大 1440 分钟 (24h)
const lockMinutes = Math.min(
Math.pow(2, this.loginFailCount - maxAttempts),
1440,
);
this.lockedUntil = new Date(Date.now() + lockMinutes * 60 * 1000);
return { remainingAttempts: 0, lockMinutes };
}
}