import { Inject , Injectable } from '@nestjs/common' ; import { validateDto } from '@fc/common' ; import { ConfigService , validationOptions } from '@fc/config' ; import { CryptographyService } from '@fc/cryptography' ; import { LoggerLevelNames , LoggerService } from '@fc/logger' ; import { Redis , REDIS_CONNECTION_TOKEN } from '@fc/redis' ; import { SessionConfig } from '../dto' ; import { SessionBadAliasException , SessionBadFormatException , SessionBadStringifyException , SessionStorageException , } from '../exceptions' ; import { ISessionBoundContext , ISessionOptions , ISessionRequest , ISessionResponse , } from '../interfaces' ; import { SESSION_TOKEN_OPTIONS } from '../tokens' ; import { ExcludedRoutes } from '../types' ; export type RedisQueryResult = [ Error | null , any ] ; @ Injectable ( ) export class SessionService { constructor ( @ Inject ( SESSION_TOKEN_OPTIONS ) private readonly sessionOptions , private readonly config , private readonly logger , @ Inject ( REDIS_CONNECTION_TOKEN ) private readonly redis , private readonly cryptography ) { this . logger . setContext ( this . constructor . name ) ; } param ctx param key return async get ( ctx , key ? ) { const session = await this . getFullSession ( ctx ) ; if ( key ) { return this . getByKey ( ctx , session , key ) ; } return this . getModule ( ctx , session ) ; } param ctx param keyOrData param data return async set ( ctx , keyOrData , data ? ) { this . logger . debug ( 'store session in redis' ) ; const session = await this . getFullSession ( ctx ) ; if ( typeof keyOrData === 'string' ) { this . setByKey ( ctx , session , keyOrData , data ) ; } else if ( typeof keyOrData === 'object' ) { this . setModule ( ctx , session , keyOrData ) ; } return this . save ( ctx , session ) ; } param ctx return async refresh ( req , res ) { const { lifetime } = this . config . get < SessionConfig > ( 'Session' ) ; const sessionId = this . getSessionIdFromCookie ( req ) ; const sessionKey = this . getSessionKey ( sessionId ) ; await this . redis . expire ( sessionKey , lifetime ) ; this . setCookies ( res , sessionId ) ; this . bindToRequest ( req , sessionId ) ; return sessionId ; } param ctx return private async getFullSession ( ctx ) { const sessionKey = this . getSessionKey ( ctx . sessionId ) ; let dataCipher ; try { dataCipher = await this . redis . get ( sessionKey ) ; } catch ( error ) { throw new SessionStorageException ( ) ; } /**
         * If the cipher is invalid, we set an empty session.
         */ if ( ! dataCipher ) { return { } ; } const data = this . unserialize ( dataCipher ) ; await this . validate ( data ) ; this . logger . trace ( { ctx , data } ) ; return data ; } param ctx param session param key return private getByKey ( ctx , session , key ) { return session [ ctx . moduleName ] ?. [ key ] ; } param ctx param session return private getModule ( ctx , session ) { return session [ ctx . moduleName ] ; } param ctx param session param key param data private setByKey ( ctx , session , key , data ) { if ( session [ ctx . moduleName ] ) { session [ ctx . moduleName ] [ key ] = data ; } else { session [ ctx . moduleName ] = { [ key ] : data , } ; } } param ctx param session param data private setModule ( ctx , session , data ) { session [ ctx . moduleName ] = { ... session [ ctx . moduleName ] , ... data , } ; } param ctx param data return private async save ( ctx , data ) { const { lifetime } = this . config . get < SessionConfig > ( 'Session' ) ; const key = this . getSessionKey ( ctx . sessionId ) ; const serialized = this . serialize ( data ) ; const multi = this . redis . multi ( ) ; multi . set ( key , serialized ) ; multi . expire ( key , lifetime ) ; const status = await multi . exec ( ) ; return Boolean ( status ) ; } init ( req , res ) { const { sessionIdLength } = this . config . get < SessionConfig > ( 'Session' ) ; const sessionId = this . cryptography . genRandomString ( sessionIdLength ) ; this . setCookies ( res , sessionId ) ; this . bindToRequest ( req , sessionId ) ; return sessionId ; } async reset ( req , res ) { const sessionId = this . getSessionIdFromCookie ( req ) ; const sessionKey = this . getSessionKey ( sessionId ) ; res . locals . session = { } ; await this . redis . del ( sessionKey ) ; return this . init ( req , res ) ; } getSessionIdFromCookie ( req ) { const { sessionCookieName } = this . config . get < SessionConfig > ( 'Session' ) ; return req . signedCookies [ sessionCookieName ] ; } param { ISessionRequest } req param { string } sessionId private bindToRequest ( req , sessionId ) { req . sessionService = this ; req . sessionId = sessionId ; } private setCookies ( res , sessionId ) { const { cookieOptions , sessionCookieName } = this . config . get < SessionConfig > ( 'Session' ) ; res . cookie ( sessionCookieName , sessionId , cookieOptions ) ; see . res . cookie ( 'duplicate-cookie-name' , 'duplicate-cookie-value' , cookieOptions ) ; } param data returns private serialize ( data ) { const { encryptionKey } = this . config . get < SessionConfig > ( 'Session' ) ; todo see https let dataString ; try { dataString = JSON . stringify ( data ) ; } catch ( error ) { this . logger . trace ( error , LoggerLevelNames . ERROR ) ; throw new SessionBadStringifyException ( ) ; } const dataCipher = this . cryptography . encryptSymetric ( encryptionKey , dataString ) ; return dataCipher ; } param data returns private unserialize ( data ) { const session = this . config . get < SessionConfig > ( 'Session' ) ; const { encryptionKey } = session ; const dataString = this . cryptography . decryptSymetric ( encryptionKey , data ) ; try { return JSON . parse ( dataString ) ; } catch ( error ) { throw new SessionBadFormatException ( ) ; } } param session todo private async validate ( session ) { todo see https await validateDto ( session , this . sessionOptions . schema , validationOptions ) ; } param ctx return private getSessionKey ( sessionId ) { const { prefix } = this . config . get < SessionConfig > ( 'Session' ) ; return `${ prefix }::${ sessionId }` ; } param { string } key param { string } value param { number } lifetime returns { RedisQueryResult [ ] async setAlias ( key , value ) { const { lifetime } = this . config . get < SessionConfig > ( 'Session' ) ; const multi = this . redis . multi ( ) ; multi . set ( key , value ) ; multi . expire ( key , lifetime ) ; const result = await multi . exec ( ) ; return result ; } param { string } key returns { Promise < string > } async getAlias ( key ) { if ( ! key ) { throw new SessionBadAliasException ( ) ; } const multi = this . redis . multi ( ) ; multi . get ( key ) ; const results = await multi . exec ( ) ; const value = results [ 0 ] [ 1 ] ; return value ; } shouldHandleSession ( route , excludedRoutes ) { const shouldExclude = excludedRoutes . find ( ( excludedRoute ) => { if ( excludedRoute instanceof RegExp ) { return excludedRoute . test ( route ) ; } return excludedRoute === route ; } ) ; return ! shouldExclude ; } }