@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
22import { v4 as uuidv4 } from 'uuid' ;
33import * as crypto from 'crypto' ;
44import { RedisService } from '../redis/redis.service' ;
5+ import { Role } from './role.enum' ;
56
67@Injectable ( )
78export class SessionService {
@@ -24,7 +25,8 @@ export class SessionService {
2425 username : string ,
2526 avatar ?: string ,
2627 ip ?: string ,
27- userAgent ?: string
28+ userAgent ?: string ,
29+ role : Role = Role . PARTICIPANT
2830 ) {
2931 const sessionId = uuidv4 ( ) ;
3032 const sessionToken = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
@@ -40,19 +42,20 @@ export class SessionService {
4042 createdAt : new Date ( ) . toISOString ( ) ,
4143 ip : ip || null ,
4244 userAgent : userAgent || null ,
45+ role,
4346 } ;
4447
4548 const client = this . redisService . getClient ( ) ;
4649 const ttl = Number ( process . env . SESSION_TTL_SECONDS || 604800 ) ;
4750
48- // CRITICAL FIX: Before creating new session, check for and destroy old sessions with same username
51+ // Before creating new session, check for and destroy old sessions with same username
4952 await this . destroyOldSessionsForUsername ( roomId , username ) ;
5053
5154 // Store session data
5255 await client . set ( `${ this . tokenPrefix } ${ sessionToken } ` , sessionId , 'EX' , ttl ) ;
5356 await client . set ( key , JSON . stringify ( payload ) , 'EX' , ttl ) ;
5457
55- // CRITICAL FIX: Map username to sessionId for quick lookup
58+ // Map username to sessionId for quick lookup
5659 const usernameSessionKey = `${ this . usernameSessionPrefix } ${ roomId } :${ username } ` ;
5760 await client . set ( usernameSessionKey , sessionId , 'EX' , ttl ) ;
5861
@@ -129,6 +132,76 @@ export class SessionService {
129132 await client . del ( `${ this . tokenPrefix } ${ sessionToken } ` ) ;
130133 }
131134
135+ /**
136+ * Destroy a session by its sessionId (not token).
137+ * Removes session payload, any associated token keys, username mapping and presence entry.
138+ */
139+ async destroySessionById ( sessionId : string ) {
140+ const client = this . redisService . getClient ( ) ;
141+ const raw = await client . get ( this . getSessionKey ( sessionId ) ) ;
142+ if ( ! raw ) return ;
143+ const sessionData = JSON . parse ( raw ) ;
144+
145+ // Remove session key
146+ await client . del ( this . getSessionKey ( sessionId ) ) ;
147+
148+ // Remove any token that maps to this sessionId
149+ const tokenKeys = await client . keys ( `${ this . tokenPrefix } *` ) ;
150+ for ( const tokenKey of tokenKeys ) {
151+ const sid = await client . get ( tokenKey ) ;
152+ if ( sid === sessionId ) {
153+ await client . del ( tokenKey ) ;
154+ }
155+ }
156+
157+ // Remove username -> session mapping
158+ if ( sessionData && sessionData . roomId && sessionData . username ) {
159+ const usernameSessionKey = `${ this . usernameSessionPrefix } ${ sessionData . roomId } :${ sessionData . username } ` ;
160+ await client . del ( usernameSessionKey ) ;
161+
162+ // Remove presence entry
163+ await client . hdel ( `presence:${ sessionData . roomId } ` , sessionId ) ;
164+ }
165+ }
166+
167+ /**
168+ * Destroy all sessions for a given room.
169+ * This will remove session records, tokens, username mappings and presence entries.
170+ */
171+ async destroyAllSessionsForRoom ( roomId : string ) {
172+ const client = this . redisService . getClient ( ) ;
173+
174+ const sessionIds = new Set < string > ( ) ;
175+
176+ // 1) Collect sessionIds from presence hash (active connections)
177+ try {
178+ const pres = await client . hkeys ( `presence:${ roomId } ` ) ;
179+ pres . forEach ( ( s ) => sessionIds . add ( s ) ) ;
180+ } catch ( e ) {
181+ // ignore
182+ }
183+
184+ // 2) Also scan session keys for any sessions belonging to the room (covers disconnected but valid sessions)
185+ const sessKeys = await client . keys ( `${ this . sessionPrefix } *` ) ;
186+ for ( const k of sessKeys ) {
187+ try {
188+ const raw = await client . get ( k ) ;
189+ if ( ! raw ) continue ;
190+ const parsed = JSON . parse ( raw ) ;
191+ if ( parsed . roomId === roomId && parsed . sessionId ) {
192+ sessionIds . add ( parsed . sessionId ) ;
193+ }
194+ } catch ( e ) {
195+ // ignore parse errors
196+ }
197+ }
198+
199+ for ( const sid of sessionIds ) {
200+ await this . destroySessionById ( sid ) ;
201+ this . logger . log ( `Destroyed session ${ sid } for room ${ roomId } ` ) ;
202+ }
203+ }
204+
132205 /**
133206 * NEW: Get session by username and room
134207 */
0 commit comments