import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { verifyToken } from "@tria-plc/api-common/utils/token"; import { ESessionStatus } from "@tria-plc/api-common/utils/enums/user.enum"; import { Session } from "@tria-plc/iamapi-common/entities/iam/user/session.entity"; /** * Authenticates a WebSocket handshake by mirroring the HTTP JwtGuard: the access * token payload is only a *session* pointer (`{ id: }`), not the * user — so we verify the signature (`verifyToken`), then load the IAM session * and require it to be ACTIVE and unexpired, and read the real user id out of * `session.userInfo`. There is no context-free verifier in the auth package, so * this lookup is unavoidable; using the typed `Session` entity (rather than raw * SQL) keeps it column-rename-safe and consistent with the package's own model. * * Returns the IAM user id, or null for any invalid/expired/revoked/malformed token. */ @Injectable() export class WsAuthService { private readonly logger = new Logger(WsAuthService.name); constructor( @InjectRepository(Session) private readonly sessions: Repository, ) {} async resolveUserId(token?: string): Promise { if (!token) return null; try { const payload = verifyToken(token) as { id?: string }; const sessionId = payload?.id; if (!sessionId) return null; const session = await this.sessions.findOne({ where: { id: sessionId }, }); if (!session) return null; if (session.status !== ESessionStatus.ACTIVE) return null; if (!session.expiryTime || new Date(session.expiryTime) <= new Date()) { return null; } return session.userInfo?.id ?? null; } catch (err) { this.logger.debug(`WS auth rejected: ${(err as Error).message}`); return null; } } }