前台: - 未登录可浏览首页/赛事/赔率,下注等操作弹出登录引导(去登录/继续浏览) - 顶部新增客服入口与 iframe 弹窗 - 登录页支持暂不登录返回浏览 API: - 首页/赛事/冠军盘接口改为公开访问,支持 X-Locale 头 - JWT 守卫支持可选认证 返水: - 注单新增 is_cashbacked 字段,发放时自动标记 - 预览展示玩家余额,明确平台直发不从代理扣款 - 后台注单列表与玩家历史展示回水状态 其他: - 串关禁止同场重复选号(SAME_MATCH) - 补充结算资金流分析文档 Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { AuthGuard } from '@nestjs/passport';
|
|
import { IS_PUBLIC_KEY, PERMISSIONS_KEY } from '../../shared/common/decorators';
|
|
import { appForbidden, appUnauthorized } from '../../shared/common/app-error';
|
|
|
|
@Injectable()
|
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
constructor(private reflector: Reflector) {
|
|
super();
|
|
}
|
|
|
|
canActivate(context: ExecutionContext) {
|
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
if (isPublic) return true;
|
|
return super.canActivate(context);
|
|
}
|
|
|
|
handleRequest<TUser = unknown>(err: Error | null, user: TUser): TUser {
|
|
if (err || !user) throw err || appUnauthorized('INVALID_CREDENTIALS');
|
|
return user;
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class PermissionsGuard implements CanActivate {
|
|
constructor(private reflector: Reflector) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const required = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
|
|
const { user } = context.switchToHttp().getRequest();
|
|
if (!user || user.userType !== 'ADMIN') {
|
|
throw appForbidden('ADMIN_ACCESS_REQUIRED');
|
|
}
|
|
if (user.role === 'SUPER_ADMIN') return true;
|
|
|
|
if (!required?.length) {
|
|
throw appForbidden('INSUFFICIENT_PERMISSIONS');
|
|
}
|
|
|
|
const userPerms: string[] = user.permissions ?? [];
|
|
const hasAccess = required.some((p) => userPerms.includes(p));
|
|
if (!hasAccess) throw appForbidden('INSUFFICIENT_PERMISSIONS');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function UserTypeGuard(...types: string[]) {
|
|
@Injectable()
|
|
class MixedUserTypeGuard implements CanActivate {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const { user } = context.switchToHttp().getRequest();
|
|
if (!types.includes(user?.userType)) {
|
|
throw appForbidden('ACCESS_DENIED_PORTAL');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return MixedUserTypeGuard;
|
|
}
|
|
|
|
@Injectable()
|
|
export class PlayerGuard implements CanActivate {
|
|
constructor(private reflector: Reflector) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
if (isPublic) return true;
|
|
const { user } = context.switchToHttp().getRequest();
|
|
if (user?.userType !== 'PLAYER') throw appForbidden('PLAYER_ACCESS_ONLY');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class AdminGuard implements CanActivate {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const { user } = context.switchToHttp().getRequest();
|
|
if (user?.userType !== 'ADMIN') throw appForbidden('ADMIN_ACCESS_ONLY');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Injectable()
|
|
export class AgentGuard implements CanActivate {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const { user } = context.switchToHttp().getRequest();
|
|
if (user?.userType !== 'AGENT') throw appForbidden('AGENT_ACCESS_ONLY');
|
|
return true;
|
|
}
|
|
}
|