feat(player): 接入创蓝短信手机注册与登录页优化
新增 SMS 验证码注册、8 国手机号选择与 Redis 频控;优化登录/注册 UI 及图形验证码样式。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import { assertPlayerUsername } from '@thebet365/shared';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { InvitesService } from './invites.service';
|
||||
import { SmsService } from './sms/sms.service';
|
||||
import { normalizePhone, resolvePlayerLoginUsername } from './sms/phone.util';
|
||||
|
||||
const MAX_LOGIN_FAILS = 5;
|
||||
const LOCK_DURATION_MS = 15 * 60 * 1000;
|
||||
@@ -27,6 +29,7 @@ export class AuthService {
|
||||
private config: ConfigService,
|
||||
private systemConfig: SystemConfigService,
|
||||
private invites: InvitesService,
|
||||
private sms: SmsService,
|
||||
) {}
|
||||
|
||||
/** 平台管理员 / 代理统一登录(按 userType 签发对应 JWT) */
|
||||
@@ -42,9 +45,19 @@ export class AuthService {
|
||||
return this.login(username, password, portal);
|
||||
}
|
||||
|
||||
async login(username: string, password: string, portal: 'player' | 'admin' | 'agent') {
|
||||
async login(
|
||||
username: string,
|
||||
password: string,
|
||||
portal: 'player' | 'admin' | 'agent',
|
||||
countryCode?: string,
|
||||
) {
|
||||
const lookupUsername =
|
||||
portal === 'player'
|
||||
? resolvePlayerLoginUsername(username, countryCode)
|
||||
: username.trim();
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { username },
|
||||
where: { username: lookupUsername },
|
||||
include: { auth: true, adminRole: { include: { role: true } } },
|
||||
});
|
||||
|
||||
@@ -143,23 +156,35 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async registerPlayer(data: {
|
||||
username: string;
|
||||
phone: string;
|
||||
countryCode: string;
|
||||
password: string;
|
||||
smsCode: string;
|
||||
sessionId: string;
|
||||
inviteCode?: string;
|
||||
locale?: string;
|
||||
}) {
|
||||
const username = data.username.trim();
|
||||
if (!username) {
|
||||
throw appBadRequest('USERNAME_REQUIRED');
|
||||
}
|
||||
const phone = normalizePhone(data.countryCode, data.phone);
|
||||
const username = phone;
|
||||
|
||||
try {
|
||||
assertPlayerUsername(username);
|
||||
} catch {
|
||||
throw appBadRequest('USERNAME_FORMAT_INVALID');
|
||||
throw appBadRequest('PHONE_INVALID');
|
||||
}
|
||||
if (!data.password || data.password.length < 8) {
|
||||
throw appBadRequest('PASSWORD_MIN_LENGTH');
|
||||
}
|
||||
if (!data.smsCode?.trim() || !data.sessionId?.trim()) {
|
||||
throw appBadRequest('SMS_CODE_REQUIRED');
|
||||
}
|
||||
|
||||
await this.sms.verifyCode({
|
||||
phone: data.phone,
|
||||
countryCode: data.countryCode,
|
||||
code: data.smsCode.trim(),
|
||||
sessionId: data.sessionId.trim(),
|
||||
});
|
||||
|
||||
const { parentId, sponsorId, inviteId } = await this.resolveInviteSponsor(data.inviteCode);
|
||||
const inviteSponsorId = parentId == null && sponsorId != null ? sponsorId : null;
|
||||
@@ -169,7 +194,7 @@ export class AuthService {
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw appBadRequest('USERNAME_TAKEN');
|
||||
throw appBadRequest('PHONE_TAKEN');
|
||||
}
|
||||
|
||||
const hash = await this.hashPassword(data.password);
|
||||
@@ -195,7 +220,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
await tx.userPreference.create({
|
||||
data: { userId: created.id, locale },
|
||||
data: { userId: created.id, locale, phone },
|
||||
});
|
||||
|
||||
if (inviteId) {
|
||||
|
||||
Reference in New Issue
Block a user