feat: internationalize API error responses by locale
Add shared error codes with zh/en/ms messages, coded app exceptions, and locale-aware global filter. Frontends send X-Locale so error text matches the active UI language. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { appForbidden, appUnauthorized } from '../../shared/common/app-error';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
@@ -31,7 +32,7 @@ export class AuthService {
|
||||
select: { userType: true },
|
||||
});
|
||||
if (!user || (user.userType !== 'ADMIN' && user.userType !== 'AGENT')) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
throw appUnauthorized('INVALID_CREDENTIALS');
|
||||
}
|
||||
const portal = user.userType === 'ADMIN' ? 'admin' : 'agent';
|
||||
return this.login(username, password, portal);
|
||||
@@ -44,20 +45,20 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!user || !user.auth) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
throw appUnauthorized('INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
const expectedType = portal === 'admin' ? 'ADMIN' : portal === 'agent' ? 'AGENT' : 'PLAYER';
|
||||
if (user.userType !== expectedType) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
throw appUnauthorized('INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
if (user.status === 'DISABLED') {
|
||||
throw new ForbiddenException('Account disabled');
|
||||
throw appForbidden('ACCOUNT_DISABLED');
|
||||
}
|
||||
|
||||
if (portal === 'agent' && user.status === 'SUSPENDED') {
|
||||
throw new ForbiddenException('Agent account suspended');
|
||||
throw appForbidden('AGENT_ACCOUNT_SUSPENDED');
|
||||
}
|
||||
|
||||
if (portal === 'player' && user.parentId) {
|
||||
@@ -68,13 +69,13 @@ export class AuthService {
|
||||
select: { userType: true, status: true },
|
||||
});
|
||||
if (parentAgent?.userType === 'AGENT' && parentAgent.status !== 'ACTIVE') {
|
||||
throw new ForbiddenException('上级代理已停用,暂无法登录');
|
||||
throw appForbidden('PARENT_AGENT_SUSPENDED');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (user.auth.lockedUntil && user.auth.lockedUntil > new Date()) {
|
||||
throw new ForbiddenException('Account locked, try again later');
|
||||
throw appForbidden('ACCOUNT_LOCKED');
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.auth.passwordHash);
|
||||
@@ -86,7 +87,7 @@ export class AuthService {
|
||||
where: { userId: user.id },
|
||||
data: { loginFailCount: failCount, lockedUntil },
|
||||
});
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
throw appUnauthorized('INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
await this.prisma.userAuth.update({
|
||||
@@ -125,15 +126,15 @@ export class AuthService {
|
||||
|
||||
async changePassword(userId: bigint, oldPassword: string, newPassword: string) {
|
||||
const auth = await this.prisma.userAuth.findUnique({ where: { userId } });
|
||||
if (!auth) throw new UnauthorizedException('User not found');
|
||||
if (!auth) throw appUnauthorized('USER_NOT_FOUND');
|
||||
|
||||
const settings = await this.systemConfig.getPlayerAccountSettings();
|
||||
if (!settings.allowPasswordChange) {
|
||||
throw new ForbiddenException('当前平台未开放玩家自行修改密码');
|
||||
throw appForbidden('PASSWORD_CHANGE_DISABLED');
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(oldPassword, auth.passwordHash);
|
||||
if (!valid) throw new UnauthorizedException('Invalid old password');
|
||||
if (!valid) throw appUnauthorized('INVALID_OLD_PASSWORD');
|
||||
|
||||
const hash = await bcrypt.hash(newPassword, 10);
|
||||
await this.prisma.userAuth.update({
|
||||
|
||||
Reference in New Issue
Block a user