/** * @zh 房间认证 Mixin * @en Room authentication mixin */ import type { Room, Player } from '../../room/index.js'; import type { IAuthContext, AuthRoomConfig } from '../types.js'; import { getAuthContext } from './withAuth.js'; import { createGuestContext } from '../context.js'; /** * @zh 带认证的玩家 * @en Player with authentication */ export interface AuthPlayer> extends Player { /** * @zh 认证上下文 * @en Authentication context */ readonly auth: IAuthContext; /** * @zh 用户信息(快捷访问) * @en User info (shortcut) */ readonly user: TUser | null; } /** * @zh 带认证的房间接口 * @en Room with authentication interface */ export interface IAuthRoom { /** * @zh 认证钩子(在 onJoin 之前调用) * @en Auth hook (called before onJoin) */ onAuth?(player: AuthPlayer): boolean | Promise; /** * @zh 获取带认证信息的玩家 * @en Get player with auth info */ getAuthPlayer(id: string): AuthPlayer | undefined; /** * @zh 获取所有带认证信息的玩家 * @en Get all players with auth info */ getAuthPlayers(): AuthPlayer[]; /** * @zh 按角色获取玩家 * @en Get players by role */ getPlayersByRole(role: string): AuthPlayer[]; /** * @zh 按用户 ID 获取玩家 * @en Get player by user ID */ getPlayerByUserId(userId: string): AuthPlayer | undefined; } /** * @zh 认证房间构造器类型 * @en Auth room constructor type */ export type AuthRoomClass = new (...args: any[]) => Room & IAuthRoom; /** * @zh 玩家认证上下文存储 * @en Player auth context storage */ const playerAuthContexts = new WeakMap>(); /** * @zh 包装玩家对象添加认证信息 * @en Wrap player object with auth info */ function wrapPlayerWithAuth(player: Player, authContext: IAuthContext): AuthPlayer { playerAuthContexts.set(player, authContext); Object.defineProperty(player, 'auth', { get: () => playerAuthContexts.get(player) ?? createGuestContext(), enumerable: true, configurable: false }); Object.defineProperty(player, 'user', { get: () => (playerAuthContexts.get(player) as IAuthContext | undefined)?.user ?? null, enumerable: true, configurable: false }); return player as AuthPlayer; } /** * @zh 包装房间类添加认证功能 * @en Wrap room class with authentication functionality * * @zh 使用 mixin 模式为房间添加认证检查,在玩家加入前验证认证状态 * @en Uses mixin pattern to add auth checks to room, validates auth before player joins * * @example * ```typescript * import { Room, onMessage } from '@esengine/server'; * import { withRoomAuth, type AuthPlayer } from '@esengine/server/auth'; * * interface User { * id: string; * name: string; * roles: string[]; * } * * class GameRoom extends withRoomAuth(Room, { * requireAuth: true, * allowedRoles: ['player', 'premium'], * }) { * onJoin(player: AuthPlayer) { * console.log(`${player.user?.name} joined the game`); * this.broadcast('PlayerJoined', { * id: player.id, * name: player.user?.name * }); * } * * // Optional: custom auth validation * async onAuth(player: AuthPlayer): Promise { * // Additional validation logic * if (player.auth.hasRole('banned')) { * return false; * } * return true; * } * * @onMessage('Chat') * handleChat(data: { text: string }, player: AuthPlayer) { * this.broadcast('Chat', { * from: player.user?.name ?? 'Guest', * text: data.text * }); * } * } * ``` */ export function withRoomAuth Room = new (...args: any[]) => Room>( Base: TBase, config: AuthRoomConfig = {} ): TBase & (new (...args: any[]) => IAuthRoom) { const { requireAuth = true, allowedRoles = [], roleCheckMode = 'any' } = config; abstract class AuthRoom extends (Base as new (...args: any[]) => Room) implements IAuthRoom { private _originalOnJoin: ((player: Player) => void | Promise) | undefined; constructor(...args: any[]) { super(...args); this._originalOnJoin = this.onJoin?.bind(this); this.onJoin = this._authOnJoin.bind(this); } /** * @zh 认证钩子(可覆盖) * @en Auth hook (can be overridden) */ onAuth?(player: AuthPlayer): boolean | Promise; /** * @zh 包装的 onJoin 方法 * @en Wrapped onJoin method */ private async _authOnJoin(player: Player): Promise { const conn = (player as any).connection ?? (player as any)._conn; const authContext = conn ? (getAuthContext(conn) ?? createGuestContext()) : createGuestContext(); if (requireAuth && !authContext.isAuthenticated) { console.warn(`[AuthRoom] Rejected unauthenticated player: ${player.id}`); this.kick(player as any, 'Authentication required'); return; } if (allowedRoles.length > 0) { const hasRole = roleCheckMode === 'any' ? authContext.hasAnyRole(allowedRoles) : authContext.hasAllRoles(allowedRoles); if (!hasRole) { console.warn(`[AuthRoom] Rejected player ${player.id}: insufficient roles`); this.kick(player as any, 'Insufficient permissions'); return; } } const authPlayer = wrapPlayerWithAuth(player, authContext); if (typeof this.onAuth === 'function') { try { const allowed = await this.onAuth(authPlayer); if (!allowed) { console.warn(`[AuthRoom] Rejected player ${player.id}: onAuth returned false`); this.kick(player as any, 'Authentication rejected'); return; } } catch (error) { console.error(`[AuthRoom] Error in onAuth for player ${player.id}:`, error); this.kick(player as any, 'Authentication error'); return; } } if (this._originalOnJoin) { await this._originalOnJoin(authPlayer as unknown as Player); } } /** * @zh 获取带认证信息的玩家 * @en Get player with auth info */ getAuthPlayer(id: string): AuthPlayer | undefined { const player = this.getPlayer(id); return player as AuthPlayer | undefined; } /** * @zh 获取所有带认证信息的玩家 * @en Get all players with auth info */ getAuthPlayers(): AuthPlayer[] { return this.players as AuthPlayer[]; } /** * @zh 按角色获取玩家 * @en Get players by role */ getPlayersByRole(role: string): AuthPlayer[] { return this.getAuthPlayers().filter(p => p.auth?.hasRole(role)); } /** * @zh 按用户 ID 获取玩家 * @en Get player by user ID */ getPlayerByUserId(userId: string): AuthPlayer | undefined { return this.getAuthPlayers().find(p => p.auth?.userId === userId); } } return AuthRoom as unknown as TBase & (new (...args: any[]) => IAuthRoom); } /** * @zh 抽象认证房间基类 * @en Abstract auth room base class * * @zh 如果不想使用 mixin,可以直接继承此类 * @en If you don't want to use mixin, you can extend this class directly * * @example * ```typescript * import { AuthRoomBase } from '@esengine/server/auth'; * * class GameRoom extends AuthRoomBase { * protected readonly authConfig = { * requireAuth: true, * allowedRoles: ['player'] * }; * * onJoin(player: AuthPlayer) { * // player has .auth and .user properties * } * } * ``` */ export abstract class AuthRoomBase> implements IAuthRoom { /** * @zh 认证配置(子类可覆盖) * @en Auth config (can be overridden by subclass) */ protected readonly authConfig: AuthRoomConfig = { requireAuth: true, allowedRoles: [], roleCheckMode: 'any' }; /** * @zh 认证钩子 * @en Auth hook */ onAuth?(player: AuthPlayer): boolean | Promise; getAuthPlayer(id: string): AuthPlayer | undefined { return undefined; } getAuthPlayers(): AuthPlayer[] { return []; } getPlayersByRole(role: string): AuthPlayer[] { return []; } getPlayerByUserId(userId: string): AuthPlayer | undefined { return undefined; } }