mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: active sessions are now destroyed if auth/reset and auth/validate endpoints are used (#806)
This commit is contained in:
		
							parent
							
								
									0de4c98a58
								
							
						
					
					
						commit
						578078e03f
					
				| @ -2,29 +2,29 @@ | ||||
| 
 | ||||
| // eslint-disable-next-line
 | ||||
| import EventEmitter from "events"; | ||||
| import { Knex } from 'knex'; | ||||
| import { AccessStore } from './access-store'; | ||||
| import { ResetTokenStore } from './reset-token-store'; | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| import { IUnleashStores } from '../types/stores'; | ||||
| 
 | ||||
| const { createDb } = require('./db-pool'); | ||||
| const EventStore = require('./event-store'); | ||||
| const FeatureToggleStore = require('./feature-toggle-store'); | ||||
| const FeatureTypeStore = require('./feature-type-store'); | ||||
| const StrategyStore = require('./strategy-store'); | ||||
| const ClientInstanceStore = require('./client-instance-store'); | ||||
| const ClientMetricsDb = require('./client-metrics-db'); | ||||
| const ClientMetricsStore = require('./client-metrics-store'); | ||||
| const ClientApplicationsStore = require('./client-applications-store'); | ||||
| const ContextFieldStore = require('./context-field-store'); | ||||
| const SettingStore = require('./setting-store'); | ||||
| const UserStore = require('./user-store'); | ||||
| const ProjectStore = require('./project-store'); | ||||
| const TagStore = require('./tag-store'); | ||||
| const TagTypeStore = require('./tag-type-store'); | ||||
| const AddonStore = require('./addon-store'); | ||||
| const { ApiTokenStore } = require('./api-token-store'); | ||||
| import { createDb } from './db-pool'; | ||||
| import EventStore from './event-store'; | ||||
| import FeatureToggleStore from './feature-toggle-store'; | ||||
| import FeatureTypeStore from './feature-type-store'; | ||||
| import StrategyStore from './strategy-store'; | ||||
| import ClientInstanceStore from './client-instance-store'; | ||||
| import ClientMetricsDb from './client-metrics-db'; | ||||
| import ClientMetricsStore from './client-metrics-store'; | ||||
| import ClientApplicationsStore from './client-applications-store'; | ||||
| import ContextFieldStore from './context-field-store'; | ||||
| import SettingStore from './setting-store'; | ||||
| import UserStore from './user-store'; | ||||
| import ProjectStore from './project-store'; | ||||
| import TagStore from './tag-store'; | ||||
| import TagTypeStore from './tag-type-store'; | ||||
| import AddonStore from './addon-store'; | ||||
| import { ApiTokenStore } from './api-token-store'; | ||||
| import SessionStore from './session-store'; | ||||
| import { AccessStore } from './access-store'; | ||||
| import { ResetTokenStore } from './reset-token-store'; | ||||
| 
 | ||||
| export const createStores = ( | ||||
|     config: IUnleashConfig, | ||||
| @ -41,11 +41,7 @@ export const createStores = ( | ||||
|         featureToggleStore: new FeatureToggleStore(db, eventBus, getLogger), | ||||
|         featureTypeStore: new FeatureTypeStore(db, getLogger), | ||||
|         strategyStore: new StrategyStore(db, getLogger), | ||||
|         clientApplicationsStore: new ClientApplicationsStore( | ||||
|             db, | ||||
|             eventBus, | ||||
|             getLogger, | ||||
|         ), | ||||
|         clientApplicationsStore: new ClientApplicationsStore(db, eventBus), | ||||
|         clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger), | ||||
|         clientMetricsStore: new ClientMetricsStore( | ||||
|             clientMetricsDb, | ||||
| @ -62,6 +58,7 @@ export const createStores = ( | ||||
|         accessStore: new AccessStore(db, eventBus, getLogger), | ||||
|         apiTokenStore: new ApiTokenStore(db, eventBus, getLogger), | ||||
|         resetTokenStore: new ResetTokenStore(db, eventBus, getLogger), | ||||
|         sessionStore: new SessionStore(db, eventBus, getLogger), | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										105
									
								
								src/lib/db/session-store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/lib/db/session-store.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| import EventEmitter from 'events'; | ||||
| import { Knex } from 'knex'; | ||||
| import { Logger, LogProvider } from '../logger'; | ||||
| import NotFoundError from '../error/notfound-error'; | ||||
| 
 | ||||
| const TABLE = 'unleash_session'; | ||||
| 
 | ||||
| interface ISessionRow { | ||||
|     sid: string; | ||||
|     sess: string; | ||||
|     created_at: Date; | ||||
|     expired?: Date; | ||||
| } | ||||
| 
 | ||||
| export interface ISession { | ||||
|     sid: string; | ||||
|     sess: any; | ||||
|     createdAt: Date; | ||||
|     expired?: Date; | ||||
| } | ||||
| 
 | ||||
| export default class SessionStore { | ||||
|     private logger: Logger; | ||||
| 
 | ||||
|     private eventBus: EventEmitter; | ||||
| 
 | ||||
|     private db: Knex; | ||||
| 
 | ||||
|     constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { | ||||
|         this.db = db; | ||||
|         this.eventBus = eventBus; | ||||
|         this.logger = getLogger('lib/db/session-store.ts'); | ||||
|     } | ||||
| 
 | ||||
|     async getActiveSessions(): Promise<ISession[]> { | ||||
|         const rows = await this.db<ISessionRow>(TABLE) | ||||
|             .whereNull('expired') | ||||
|             .orWhere('expired', '>', new Date()) | ||||
|             .orderBy('created_at', 'desc'); | ||||
|         return rows.map(this.rowToSession); | ||||
|     } | ||||
| 
 | ||||
|     async getSessionsForUser(userId: number): Promise<ISession[]> { | ||||
|         const rows = await this.db<ISessionRow>( | ||||
|             TABLE, | ||||
|         ).whereRaw(`(sess -> 'user' ->> 'id')::int = ?`, [userId]); | ||||
|         if (rows && rows.length > 0) { | ||||
|             return rows.map(this.rowToSession); | ||||
|         } | ||||
|         throw new NotFoundError( | ||||
|             `Could not find sessions for user with id ${userId}`, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     async getSession(sid: string): Promise<ISession> { | ||||
|         const row = await this.db<ISessionRow>(TABLE) | ||||
|             .where('sid', '=', sid) | ||||
|             .first(); | ||||
|         if (row) { | ||||
|             return this.rowToSession(row); | ||||
|         } | ||||
|         throw new NotFoundError(`Could not find session with sid ${sid}`); | ||||
|     } | ||||
| 
 | ||||
|     async deleteSessionsForUser(userId: number): Promise<void> { | ||||
|         await this.db<ISessionRow>(TABLE) | ||||
|             .whereRaw(`(sess -> 'user' ->> 'id')::int = ?`, [userId]) | ||||
|             .del(); | ||||
|     } | ||||
| 
 | ||||
|     async deleteSession(sid: string): Promise<void> { | ||||
|         await this.db<ISessionRow>(TABLE) | ||||
|             .where('sid', '=', sid) | ||||
|             .del(); | ||||
|     } | ||||
| 
 | ||||
|     async insertSession(data: Omit<ISession, 'createdAt'>): Promise<ISession> { | ||||
|         const row = await this.db<ISessionRow>(TABLE) | ||||
|             .insert({ | ||||
|                 sid: data.sid, | ||||
|                 sess: JSON.stringify(data.sess), | ||||
|                 expired: data.expired || new Date(Date.now() + 86400000), | ||||
|             }) | ||||
|             .returning<ISessionRow>(['sid', 'sess', 'created_at', 'expired']); | ||||
|         if (row) { | ||||
|             return this.rowToSession(row); | ||||
|         } | ||||
|         throw new Error('Could not insert session'); | ||||
|     } | ||||
| 
 | ||||
|     async deleteAll(): Promise<void> { | ||||
|         await this.db(TABLE).del(); | ||||
|     } | ||||
| 
 | ||||
|     private rowToSession(row: ISessionRow): ISession { | ||||
|         return { | ||||
|             sid: row.sid, | ||||
|             sess: row.sess, | ||||
|             createdAt: row.created_at, | ||||
|             expired: row.expired, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = SessionStore; | ||||
| @ -1,3 +1,4 @@ | ||||
| import { Request, Response } from 'express'; | ||||
| import Controller from '../controller'; | ||||
| import { ADMIN } from '../../permissions'; | ||||
| import UserService from '../../services/user-service'; | ||||
| @ -8,6 +9,7 @@ import { IUnleashConfig } from '../../types/option'; | ||||
| import { EmailService, MAIL_ACCEPTED } from '../../services/email-service'; | ||||
| import ResetTokenService from '../../services/reset-token-service'; | ||||
| import { IUnleashServices } from '../../types/services'; | ||||
| import SessionService from '../../services/session-service'; | ||||
| 
 | ||||
| const getCreatorUsernameOrPassword = req => req.user.username || req.user.email; | ||||
| 
 | ||||
| @ -22,6 +24,8 @@ export default class UserAdminController extends Controller { | ||||
| 
 | ||||
|     private resetTokenService: ResetTokenService; | ||||
| 
 | ||||
|     private sessionService: SessionService; | ||||
| 
 | ||||
|     constructor( | ||||
|         config: IUnleashConfig, | ||||
|         { | ||||
| @ -29,12 +33,14 @@ export default class UserAdminController extends Controller { | ||||
|             accessService, | ||||
|             emailService, | ||||
|             resetTokenService, | ||||
|             sessionService, | ||||
|         }: Pick< | ||||
|         IUnleashServices, | ||||
|         | 'userService' | ||||
|         | 'accessService' | ||||
|         | 'emailService' | ||||
|         | 'resetTokenService' | ||||
|         | 'sessionService' | ||||
|         >, | ||||
|     ) { | ||||
|         super(config); | ||||
| @ -43,6 +49,7 @@ export default class UserAdminController extends Controller { | ||||
|         this.logger = config.getLogger('routes/user-controller.ts'); | ||||
|         this.emailService = emailService; | ||||
|         this.resetTokenService = resetTokenService; | ||||
|         this.sessionService = sessionService; | ||||
| 
 | ||||
|         this.get('/', this.getUsers, ADMIN); | ||||
|         this.get('/search', this.search); | ||||
| @ -52,6 +59,7 @@ export default class UserAdminController extends Controller { | ||||
|         this.post('/:id/change-password', this.changePassword, ADMIN); | ||||
|         this.delete('/:id', this.deleteUser, ADMIN); | ||||
|         this.post('/reset-password', this.resetPassword); | ||||
|         this.get('/active-sessions', this.getActiveSessions, ADMIN); | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||
| @ -88,6 +96,15 @@ export default class UserAdminController extends Controller { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async getActiveSessions(req: Request, res: Response): Promise<void> { | ||||
|         try { | ||||
|             const sessions = await this.sessionService.getActiveSessions(); | ||||
|             res.json(sessions); | ||||
|         } catch (error) { | ||||
|             handleErrors(res, this.logger, error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||
|     async search(req, res): Promise<void> { | ||||
|         const { q } = req.query; | ||||
|  | ||||
| @ -10,6 +10,7 @@ import UserService from '../../services/user-service'; | ||||
| import User from '../../types/user'; | ||||
| import { Logger } from '../../logger'; | ||||
| import { handleErrors } from './util'; | ||||
| import SessionService from '../../services/session-service'; | ||||
| 
 | ||||
| interface IChangeUserRequest { | ||||
|     password: string; | ||||
| @ -26,6 +27,8 @@ class UserController extends Controller { | ||||
| 
 | ||||
|     private userService: UserService; | ||||
| 
 | ||||
|     private sessionService: SessionService; | ||||
| 
 | ||||
|     private logger: Logger; | ||||
| 
 | ||||
|     constructor( | ||||
| @ -33,15 +36,21 @@ class UserController extends Controller { | ||||
|         { | ||||
|             accessService, | ||||
|             userService, | ||||
|         }: Pick<IUnleashServices, 'accessService' | 'userService'>, | ||||
|             sessionService, | ||||
|         }: Pick< | ||||
|         IUnleashServices, | ||||
|         'accessService' | 'userService' | 'sessionService' | ||||
|         >, | ||||
|     ) { | ||||
|         super(config); | ||||
|         this.accessService = accessService; | ||||
|         this.userService = userService; | ||||
|         this.sessionService = sessionService; | ||||
|         this.logger = config.getLogger('lib/routes/admin-api/user.ts'); | ||||
| 
 | ||||
|         this.get('/', this.getUser); | ||||
|         this.post('/change-password', this.updateUserPass); | ||||
|         this.get('/my-sessions', this.mySessions); | ||||
|     } | ||||
| 
 | ||||
|     async getUser(req: IAuthRequest, res: Response): Promise<void> { | ||||
| @ -81,6 +90,25 @@ class UserController extends Controller { | ||||
|             res.status(401).end(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async mySessions( | ||||
|         req: UserRequest<any, any, any, any>, | ||||
|         res: Response, | ||||
|     ): Promise<void> { | ||||
|         const { user } = req; | ||||
|         if (user) { | ||||
|             try { | ||||
|                 const sessions = await this.sessionService.getSessionsForUser( | ||||
|                     user.id, | ||||
|                 ); | ||||
|                 res.json(sessions); | ||||
|             } catch (e) { | ||||
|                 handleErrors(res, this.logger, e); | ||||
|             } | ||||
|         } else { | ||||
|             res.status(401).end(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = UserController; | ||||
|  | ||||
| @ -4,10 +4,7 @@ import UserService from '../../services/user-service'; | ||||
| import { Logger } from '../../logger'; | ||||
| import { handleErrors } from '../admin-api/util'; | ||||
| import { IUnleashConfig } from '../../types/option'; | ||||
| 
 | ||||
| interface IServices { | ||||
|     userService: UserService; | ||||
| } | ||||
| import { IUnleashServices } from '../../types/services'; | ||||
| 
 | ||||
| interface IValidateQuery { | ||||
|     token: string; | ||||
| @ -18,13 +15,19 @@ interface IChangePasswordBody { | ||||
|     password: string; | ||||
| } | ||||
| 
 | ||||
| interface SessionRequest<PARAMS, QUERY, BODY, K> | ||||
|     extends Request<PARAMS, QUERY, BODY, K> { | ||||
|     session?; | ||||
|     user?; | ||||
| } | ||||
| 
 | ||||
| const UNLEASH = 'Unleash'; | ||||
| class ResetPasswordController extends Controller { | ||||
|     userService: UserService; | ||||
|     private userService: UserService; | ||||
| 
 | ||||
|     logger: Logger; | ||||
|     private logger: Logger; | ||||
| 
 | ||||
|     constructor(config: IUnleashConfig, { userService }: IServices) { | ||||
|     constructor(config: IUnleashConfig, { userService }: IUnleashServices) { | ||||
|         super(config); | ||||
|         this.logger = config.getLogger( | ||||
|             'lib/routes/auth/reset-password-controller.ts', | ||||
| @ -65,6 +68,7 @@ class ResetPasswordController extends Controller { | ||||
|         const { token } = req.query; | ||||
|         try { | ||||
|             const user = await this.userService.getUserForToken(token); | ||||
|             await this.logout(req); | ||||
|             res.status(200).json(user); | ||||
|         } catch (e) { | ||||
|             handleErrors(res, this.logger, e); | ||||
| @ -75,6 +79,7 @@ class ResetPasswordController extends Controller { | ||||
|         req: Request<unknown, unknown, IChangePasswordBody, unknown>, | ||||
|         res: Response, | ||||
|     ): Promise<void> { | ||||
|         await this.logout(req); | ||||
|         const { token, password } = req.body; | ||||
|         try { | ||||
|             await this.userService.resetPassword(token, password); | ||||
| @ -83,6 +88,12 @@ class ResetPasswordController extends Controller { | ||||
|             handleErrors(res, this.logger, e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async logout(req: SessionRequest<any, any, any, any>) { | ||||
|         if (req.session) { | ||||
|             req.session.destroy(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default ResetPasswordController; | ||||
|  | ||||
| @ -16,19 +16,19 @@ const MASKED_VALUE = '*****'; | ||||
| class AddonService { | ||||
|     constructor( | ||||
|         { addonStore, eventStore, featureToggleStore }, | ||||
|         { getLogger, unleashUrl }, | ||||
|         config, | ||||
|         tagTypeService, | ||||
|     ) { | ||||
|         this.eventStore = eventStore; | ||||
|         this.addonStore = addonStore; | ||||
|         this.featureToggleStore = featureToggleStore; | ||||
|         this.getLogger = getLogger; | ||||
|         this.logger = getLogger('services/addon-service.js'); | ||||
|         this.getLogger = config.getLogger; | ||||
|         this.logger = config.getLogger('services/addon-service.js'); | ||||
|         this.tagTypeService = tagTypeService; | ||||
| 
 | ||||
|         this.addonProviders = this.loadProviders({ | ||||
|             getLogger, | ||||
|             unleashUrl, | ||||
|             getLogger: config.getLogger, | ||||
|             unleashUrl: config.server.unleashUrl, | ||||
|         }); | ||||
|         this.sensitiveParams = this.loadSensitiveParams(this.addonProviders); | ||||
|         if (addonStore) { | ||||
|  | ||||
| @ -88,7 +88,11 @@ function getSetup() { | ||||
|     const stores = store.createStores(); | ||||
|     const tagTypeService = new TagTypeService(stores, { getLogger }); | ||||
|     return { | ||||
|         addonService: new AddonService(stores, { getLogger }, tagTypeService), | ||||
|         addonService: new AddonService( | ||||
|             stores, | ||||
|             { getLogger, server: { unleashUrl: 'http://test' } }, | ||||
|             tagTypeService, | ||||
|         ), | ||||
|         stores, | ||||
|         tagTypeService, | ||||
|     }; | ||||
|  | ||||
| @ -5,22 +5,23 @@ import FeatureTypeService from './feature-type-service'; | ||||
| import EventService from './event-service'; | ||||
| import HealthService from './health-service'; | ||||
| 
 | ||||
| const FeatureToggleService = require('./feature-toggle-service'); | ||||
| const ProjectService = require('./project-service'); | ||||
| const StateService = require('./state-service'); | ||||
| const ClientMetricsService = require('./client-metrics'); | ||||
| const TagTypeService = require('./tag-type-service'); | ||||
| const TagService = require('./tag-service'); | ||||
| const StrategyService = require('./strategy-service'); | ||||
| const AddonService = require('./addon-service'); | ||||
| const ContextService = require('./context-service'); | ||||
| const VersionService = require('./version-service'); | ||||
| const { EmailService } = require('./email-service'); | ||||
| const { AccessService } = require('./access-service'); | ||||
| const { ApiTokenService } = require('./api-token-service'); | ||||
| const UserService = require('./user-service'); | ||||
| const ResetTokenService = require('./reset-token-service'); | ||||
| const SettingService = require('./setting-service'); | ||||
| import FeatureToggleService from './feature-toggle-service'; | ||||
| import ProjectService from './project-service'; | ||||
| import StateService from './state-service'; | ||||
| import ClientMetricsService from './client-metrics'; | ||||
| import TagTypeService from './tag-type-service'; | ||||
| import TagService from './tag-service'; | ||||
| import StrategyService from './strategy-service'; | ||||
| import AddonService from './addon-service'; | ||||
| import ContextService from './context-service'; | ||||
| import VersionService from './version-service'; | ||||
| import { EmailService } from './email-service'; | ||||
| import { AccessService } from './access-service'; | ||||
| import { ApiTokenService } from './api-token-service'; | ||||
| import UserService from './user-service'; | ||||
| import ResetTokenService from './reset-token-service'; | ||||
| import SettingService from './setting-service'; | ||||
| import SessionService from './session-service'; | ||||
| 
 | ||||
| export const createServices = ( | ||||
|     stores: IUnleashStores, | ||||
| @ -32,11 +33,7 @@ export const createServices = ( | ||||
|     const contextService = new ContextService(stores, config); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const eventService = new EventService(stores, config); | ||||
|     const featureToggleService = new FeatureToggleService( | ||||
|         stores, | ||||
|         config, | ||||
|         accessService, | ||||
|     ); | ||||
|     const featureToggleService = new FeatureToggleService(stores, config); | ||||
|     const featureTypeService = new FeatureTypeService(stores, config); | ||||
|     const projectService = new ProjectService(stores, config, accessService); | ||||
|     const resetTokenService = new ResetTokenService(stores, config); | ||||
| @ -45,10 +42,12 @@ export const createServices = ( | ||||
|     const tagService = new TagService(stores, config); | ||||
|     const tagTypeService = new TagTypeService(stores, config); | ||||
|     const addonService = new AddonService(stores, config, tagTypeService); | ||||
|     const sessionService = new SessionService(stores, config); | ||||
|     const userService = new UserService(stores, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
|     const versionService = new VersionService(stores, config); | ||||
|     const healthService = new HealthService(stores, config); | ||||
| @ -74,6 +73,7 @@ export const createServices = ( | ||||
|         resetTokenService, | ||||
|         eventService, | ||||
|         settingService, | ||||
|         sessionService, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										47
									
								
								src/lib/services/session-service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/lib/services/session-service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| import { IUnleashStores } from '../types/stores'; | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| import { Logger } from '../logger'; | ||||
| import SessionStore, { ISession } from '../db/session-store'; | ||||
| 
 | ||||
| export default class SessionService { | ||||
|     private logger: Logger; | ||||
| 
 | ||||
|     private sessionStore: SessionStore; | ||||
| 
 | ||||
|     constructor( | ||||
|         { sessionStore }: Pick<IUnleashStores, 'sessionStore'>, | ||||
|         { getLogger }: Pick<IUnleashConfig, 'getLogger'>, | ||||
|     ) { | ||||
|         this.logger = getLogger('lib/services/session-service.ts'); | ||||
|         this.sessionStore = sessionStore; | ||||
|     } | ||||
| 
 | ||||
|     async getActiveSessions(): Promise<ISession[]> { | ||||
|         return this.sessionStore.getActiveSessions(); | ||||
|     } | ||||
| 
 | ||||
|     async getSessionsForUser(userId: number): Promise<ISession[]> { | ||||
|         return this.sessionStore.getSessionsForUser(userId); | ||||
|     } | ||||
| 
 | ||||
|     async getSession(sid: string): Promise<ISession> { | ||||
|         return this.sessionStore.getSession(sid); | ||||
|     } | ||||
| 
 | ||||
|     async deleteSessionsForUser(userId: number): Promise<void> { | ||||
|         return this.sessionStore.deleteSessionsForUser(userId); | ||||
|     } | ||||
| 
 | ||||
|     async deleteSession(sid: string): Promise<void> { | ||||
|         return this.sessionStore.deleteSession(sid); | ||||
|     } | ||||
| 
 | ||||
|     async insertSession({ | ||||
|         sid, | ||||
|         sess, | ||||
|     }: Pick<ISession, 'sid' | 'sess'>): Promise<ISession> { | ||||
|         return this.sessionStore.insertSession({ sid, sess }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = SessionService; | ||||
| @ -8,6 +8,8 @@ import { EmailService } from './email-service'; | ||||
| import OwaspValidationError from '../error/owasp-validation-error'; | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| import { createTestConfig } from '../../test/config/test-config'; | ||||
| import SessionService from './session-service'; | ||||
| import FakeSessionStore from '../../test/fixtures/fake-session-store'; | ||||
| 
 | ||||
| const config: IUnleashConfig = createTestConfig(); | ||||
| 
 | ||||
| @ -19,12 +21,15 @@ test('Should create new user', async t => { | ||||
|         { userStore, resetTokenStore }, | ||||
|         config, | ||||
|     ); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
|     const user = await service.createUser({ | ||||
|         username: 'test', | ||||
| @ -48,11 +53,14 @@ test('Should create default user', async t => { | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     await service.initAdminUser(); | ||||
| @ -71,11 +79,14 @@ test('Should be a valid password', async t => { | ||||
|     ); | ||||
| 
 | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     const valid = service.validatePassword('this is a strong password!'); | ||||
| @ -92,11 +103,14 @@ test('Password must be at least 10 chars', async t => { | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     t.throws(() => service.validatePassword('admin'), { | ||||
| @ -114,11 +128,14 @@ test('The password must contain at least one uppercase letter.', async t => { | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     t.throws(() => service.validatePassword('qwertyabcde'), { | ||||
| @ -137,10 +154,14 @@ test('The password must contain at least one number', async t => { | ||||
|     ); | ||||
| 
 | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     t.throws(() => service.validatePassword('qwertyabcdE'), { | ||||
| @ -158,11 +179,14 @@ test('The password must contain at least one special character', async t => { | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     t.throws(() => service.validatePassword('qwertyabcdE2'), { | ||||
| @ -180,11 +204,14 @@ test('Should be a valid password with special chars', async t => { | ||||
|         config, | ||||
|     ); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
|     const sessionStore = new FakeSessionStore(); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
| 
 | ||||
|     const service = new UserService({ userStore }, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     const valid = service.validatePassword('this is a strong password!'); | ||||
|  | ||||
| @ -15,6 +15,9 @@ import NotFoundError from '../error/notfound-error'; | ||||
| import OwaspValidationError from '../error/owasp-validation-error'; | ||||
| import { EmailService } from './email-service'; | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| import SessionService from './session-service'; | ||||
| import { IUnleashServices } from '../types/services'; | ||||
| import { IUnleashStores } from '../types/stores'; | ||||
| 
 | ||||
| export interface ICreateUser { | ||||
|     name?: string; | ||||
| @ -45,16 +48,6 @@ interface ITokenUser extends IUpdateUser { | ||||
|     role: IRoleDescription; | ||||
| } | ||||
| 
 | ||||
| interface IStores { | ||||
|     userStore: UserStore; | ||||
| } | ||||
| 
 | ||||
| interface IServices { | ||||
|     accessService: AccessService; | ||||
|     resetTokenService: ResetTokenService; | ||||
|     emailService: EmailService; | ||||
| } | ||||
| 
 | ||||
| const saltRounds = 10; | ||||
| 
 | ||||
| class UserService { | ||||
| @ -66,21 +59,35 @@ class UserService { | ||||
| 
 | ||||
|     private resetTokenService: ResetTokenService; | ||||
| 
 | ||||
|     private sessionService: SessionService; | ||||
| 
 | ||||
|     private emailService: EmailService; | ||||
| 
 | ||||
|     constructor( | ||||
|         stores: IStores, | ||||
|         stores: Pick<IUnleashStores, 'userStore'>, | ||||
|         { | ||||
|             getLogger, | ||||
|             authentication, | ||||
|         }: Pick<IUnleashConfig, 'getLogger' | 'authentication'>, | ||||
|         { accessService, resetTokenService, emailService }: IServices, | ||||
|         { | ||||
|             accessService, | ||||
|             resetTokenService, | ||||
|             emailService, | ||||
|             sessionService, | ||||
|         }: Pick< | ||||
|         IUnleashServices, | ||||
|             | 'accessService' | ||||
|             | 'resetTokenService' | ||||
|             | 'emailService' | ||||
|             | 'sessionService' | ||||
|         >, | ||||
|     ) { | ||||
|         this.logger = getLogger('service/user-service.js'); | ||||
|         this.store = stores.userStore; | ||||
|         this.accessService = accessService; | ||||
|         this.resetTokenService = resetTokenService; | ||||
|         this.emailService = emailService; | ||||
|         this.sessionService = sessionService; | ||||
|         if (authentication && authentication.createAdminUser) { | ||||
|             process.nextTick(() => this.initAdminUser()); | ||||
|         } | ||||
| @ -288,6 +295,11 @@ class UserService { | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * If the password is a strong password will update password and delete all sessions for the user we're changing the password for | ||||
|      * @param token - the token authenticating this request | ||||
|      * @param password - new password | ||||
|      */ | ||||
|     async resetPassword(token: string, password: string): Promise<void> { | ||||
|         this.validatePassword(password); | ||||
|         const user = await this.getUserForToken(token); | ||||
| @ -297,6 +309,7 @@ class UserService { | ||||
|         }); | ||||
|         if (allowed) { | ||||
|             await this.changePassword(user.id, password); | ||||
|             await this.sessionService.deleteSessionsForUser(user.id); | ||||
|         } else { | ||||
|             throw new InvalidTokenError(); | ||||
|         } | ||||
|  | ||||
| @ -17,6 +17,7 @@ import FeatureTypeService from '../services/feature-type-service'; | ||||
| import EventService from '../services/event-service'; | ||||
| import HealthService from '../services/health-service'; | ||||
| import SettingService from '../services/setting-service'; | ||||
| import SessionService from '../services/session-service'; | ||||
| 
 | ||||
| export interface IUnleashServices { | ||||
|     accessService: AccessService; | ||||
| @ -31,6 +32,7 @@ export interface IUnleashServices { | ||||
|     healthService: HealthService; | ||||
|     projectService: ProjectService; | ||||
|     resetTokenService: ResetTokenService; | ||||
|     sessionService: SessionService; | ||||
|     settingService: SettingService; | ||||
|     stateService: StateService; | ||||
|     strategyService: StrategyService; | ||||
|  | ||||
| @ -16,6 +16,7 @@ import AddonStore from '../db/addon-store'; | ||||
| import { AccessStore } from '../db/access-store'; | ||||
| import { ApiTokenStore } from '../db/api-token-store'; | ||||
| import { ResetTokenStore } from '../db/reset-token-store'; | ||||
| import SessionStore from '../db/session-store'; | ||||
| 
 | ||||
| export interface IUnleashStores { | ||||
|     projectStore: ProjectStore; | ||||
| @ -28,6 +29,7 @@ export interface IUnleashStores { | ||||
|     featureToggleStore: FeatureToggleStore; | ||||
|     contextFieldStore: ContextFieldStore; | ||||
|     settingStore: SettingStore; | ||||
|     sessionStore: SessionStore; | ||||
|     userStore: UserStore; | ||||
|     tagStore: TagStore; | ||||
|     tagTypeStore: TagTypeStore; | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| import test from 'ava'; | ||||
| import sinon from 'sinon'; | ||||
| import { URL } from 'url'; | ||||
| import EventEmitter from 'events'; | ||||
| import dbInit from '../../helpers/database-init'; | ||||
| import getLogger from '../../../fixtures/no-logger'; | ||||
| 
 | ||||
| @ -9,11 +11,13 @@ import { | ||||
| } from '../../../../lib/services/access-service'; | ||||
| import ResetTokenService from '../../../../lib/services/reset-token-service'; | ||||
| import UserService from '../../../../lib/services/user-service'; | ||||
| import { setupApp } from '../../helpers/test-helper'; | ||||
| import { setupApp, setupAppWithAuth } from '../../helpers/test-helper'; | ||||
| import { EmailService } from '../../../../lib/services/email-service'; | ||||
| import User from '../../../../lib/types/user'; | ||||
| import { IUnleashConfig } from '../../../../lib/types/option'; | ||||
| import { createTestConfig } from '../../../config/test-config'; | ||||
| import SessionStore from '../../../../lib/db/session-store'; | ||||
| import SessionService from '../../../../lib/services/session-service'; | ||||
| 
 | ||||
| let stores; | ||||
| let db; | ||||
| @ -46,11 +50,17 @@ test.before(async () => { | ||||
|     stores = db.stores; | ||||
|     accessService = new AccessService(stores, config); | ||||
|     const emailService = new EmailService(config.email, config.getLogger); | ||||
| 
 | ||||
|     const sessionStore = new SessionStore( | ||||
|         db, | ||||
|         new EventEmitter(), | ||||
|         config.getLogger, | ||||
|     ); | ||||
|     const sessionService = new SessionService({ sessionStore }, config); | ||||
|     userService = new UserService(stores, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
|     resetTokenService = new ResetTokenService(stores, config); | ||||
|     const adminRole = await accessService.getRootRole(RoleName.ADMIN); | ||||
| @ -99,7 +109,7 @@ test.serial('Can use token to reset password', async t => { | ||||
|     ); | ||||
|     const relative = getBackendResetUrl(url); | ||||
|     // Can't login before reset
 | ||||
|     t.throwsAsync<Error>( | ||||
|     await t.throwsAsync<Error>( | ||||
|         async () => userService.loginUser(user.email, password), | ||||
|         { | ||||
|             instanceOf: Error, | ||||
| @ -171,6 +181,69 @@ test.serial('Invalid token should yield 401', async t => { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test.serial( | ||||
|     'Calling validate endpoint with already existing session should destroy session', | ||||
|     async t => { | ||||
|         t.plan(0); | ||||
|         const request = await setupAppWithAuth(stores); | ||||
|         await request | ||||
|             .post('/api/admin/login') | ||||
|             .send({ | ||||
|                 email: 'user@mail.com', | ||||
|             }) | ||||
|             .expect(200); | ||||
|         await request.get('/api/admin/features').expect(200); | ||||
|         const url = await resetTokenService.createResetPasswordUrl( | ||||
|             user.id, | ||||
|             adminUser.username, | ||||
|         ); | ||||
|         const relative = getBackendResetUrl(url); | ||||
| 
 | ||||
|         await request | ||||
|             .get(relative) | ||||
|             .expect(200) | ||||
|             .expect('Content-Type', /json/); | ||||
|         await request.get('/api/admin/features').expect(401); // we no longer should have a valid session
 | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| test.serial( | ||||
|     'Calling reset endpoint with already existing session should logout/destroy existing session', | ||||
|     async t => { | ||||
|         t.plan(0); | ||||
|         const request = await setupAppWithAuth(stores); | ||||
|         const url = await resetTokenService.createResetPasswordUrl( | ||||
|             user.id, | ||||
|             adminUser.username, | ||||
|         ); | ||||
|         const relative = getBackendResetUrl(url); | ||||
|         let token; | ||||
|         await request | ||||
|             .get(relative) | ||||
|             .expect(200) | ||||
|             .expect('Content-Type', /json/) | ||||
|             .expect(res => { | ||||
|                 token = res.body.token; | ||||
|             }); | ||||
|         await request | ||||
|             .post('/api/admin/login') | ||||
|             .send({ | ||||
|                 email: 'user@mail.com', | ||||
|             }) | ||||
|             .expect(200); | ||||
|         await request.get('/api/admin/features').expect(200); // If we login we can access features endpoint
 | ||||
|         await request | ||||
|             .post('/auth/reset/password') | ||||
|             .send({ | ||||
|                 email: user.email, | ||||
|                 token, | ||||
|                 password, | ||||
|             }) | ||||
|             .expect(200); | ||||
|         await request.get('/api/admin/features').expect(401); // we no longer have a valid session after using the reset password endpoint
 | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| test.serial( | ||||
|     'Trying to change password with an invalid token should yield 401', | ||||
|     async t => { | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { EmailService } from '../../../lib/services/email-service'; | ||||
| import User from '../../../lib/types/user'; | ||||
| import { IUnleashConfig } from '../../../lib/types/option'; | ||||
| import { createTestConfig } from '../../config/test-config'; | ||||
| import SessionService from '../../../lib/services/session-service'; | ||||
| 
 | ||||
| const config: IUnleashConfig = createTestConfig(); | ||||
| 
 | ||||
| @ -20,18 +21,20 @@ let userIdToCreateResetFor: number; | ||||
| let accessService: AccessService; | ||||
| let userService: UserService; | ||||
| let resetTokenService: ResetTokenService; | ||||
| let sessionService: SessionService; | ||||
| test.before(async () => { | ||||
|     db = await dbInit('reset_token_service_serial', getLogger); | ||||
|     stores = db.stores; | ||||
|     accessService = new AccessService(stores, config); | ||||
|     resetTokenService = new ResetTokenService(stores, config); | ||||
| 
 | ||||
|     sessionService = new SessionService(stores, config); | ||||
|     const emailService = new EmailService(undefined, config.getLogger); | ||||
| 
 | ||||
|     userService = new UserService(stores, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
| 
 | ||||
|     adminUser = await userService.createUser({ | ||||
|  | ||||
							
								
								
									
										113
									
								
								src/test/e2e/services/session-service.e2e.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/test/e2e/services/session-service.e2e.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | ||||
| import test, { after, before, beforeEach } from 'ava'; | ||||
| import noLoggerProvider from '../../fixtures/no-logger'; | ||||
| import dbInit from '../helpers/database-init'; | ||||
| import SessionService from '../../../lib/services/session-service'; | ||||
| import NotFoundError from '../../../lib/error/notfound-error'; | ||||
| 
 | ||||
| let stores; | ||||
| let db; | ||||
| let sessionService; | ||||
| const newSession = { | ||||
|     sid: 'abc123', | ||||
|     sess: { | ||||
|         cookie: { | ||||
|             originalMaxAge: 2880000, | ||||
|             expires: new Date(Date.now() + 86_400_000).toDateString(), | ||||
|             secure: false, | ||||
|             httpOnly: true, | ||||
|             path: '/', | ||||
|         }, | ||||
|         user: { | ||||
|             id: 1, | ||||
|             username: 'admin', | ||||
|             imageUrl: | ||||
|                 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro', | ||||
|             seenAt: '2021-04-26T10:59:18.782Z', | ||||
|             loginAttempts: 0, | ||||
|             createdAt: '2021-04-22T05:12:54.368Z', | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| const otherSession = { | ||||
|     sid: 'xyz321', | ||||
|     sess: { | ||||
|         cookie: { | ||||
|             originalMaxAge: 2880000, | ||||
|             expires: new Date(Date.now() + 86400000).toDateString(), | ||||
|             secure: false, | ||||
|             httpOnly: true, | ||||
|             path: '/', | ||||
|         }, | ||||
|         user: { | ||||
|             id: 2, | ||||
|             username: 'editor', | ||||
|             imageUrl: | ||||
|                 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro', | ||||
|             seenAt: '2021-04-26T10:59:18.782Z', | ||||
|             loginAttempts: 0, | ||||
|             createdAt: '2021-04-22T05:12:54.368Z', | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| before(async () => { | ||||
|     db = await dbInit('session_service_serial', noLoggerProvider); | ||||
|     stores = db.stores; | ||||
|     sessionService = new SessionService(stores, { | ||||
|         getLogger: noLoggerProvider, | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| beforeEach(async () => { | ||||
|     await db.stores.sessionStore.deleteAll(); | ||||
| }); | ||||
| 
 | ||||
| after.always(async () => { | ||||
|     await db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| test.serial('should list active sessions', async t => { | ||||
|     await sessionService.insertSession(newSession); | ||||
|     await sessionService.insertSession(otherSession); | ||||
| 
 | ||||
|     const sessions = await sessionService.getActiveSessions(); | ||||
|     t.is(sessions.length, 2); | ||||
|     t.deepEqual(sessions[0].sess, otherSession.sess); // Ordered newest first
 | ||||
|     t.deepEqual(sessions[1].sess, newSession.sess); | ||||
| }); | ||||
| 
 | ||||
| test.serial('Should list active sessions for user', async t => { | ||||
|     await sessionService.insertSession(newSession); | ||||
|     await sessionService.insertSession(otherSession); | ||||
| 
 | ||||
|     const sessions = await sessionService.getSessionsForUser(2); // editor session
 | ||||
|     t.is(sessions.length, 1); | ||||
|     t.deepEqual(sessions[0].sess, otherSession.sess); | ||||
| }); | ||||
| 
 | ||||
| test.serial('Can delete sessions by user', async t => { | ||||
|     await sessionService.insertSession(newSession); | ||||
|     await sessionService.insertSession(otherSession); | ||||
| 
 | ||||
|     const sessions = await sessionService.getActiveSessions(); | ||||
|     t.is(sessions.length, 2); | ||||
|     await sessionService.deleteSessionsForUser(2); | ||||
|     await t.throwsAsync( | ||||
|         async () => { | ||||
|             await sessionService.getSessionsForUser(2); | ||||
|         }, | ||||
|         { instanceOf: NotFoundError }, | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test.serial('Can delete session by sid', async t => { | ||||
|     await sessionService.insertSession(newSession); | ||||
|     await sessionService.insertSession(otherSession); | ||||
| 
 | ||||
|     const sessions = await sessionService.getActiveSessions(); | ||||
|     t.is(sessions.length, 2); | ||||
| 
 | ||||
|     await sessionService.deleteSession('abc123'); | ||||
|     await t.throwsAsync(async () => sessionService.getSession('abc123'), { | ||||
|         instanceOf: NotFoundError, | ||||
|     }); | ||||
| }); | ||||
| @ -9,6 +9,7 @@ import { IRole } from '../../../lib/db/access-store'; | ||||
| import ResetTokenService from '../../../lib/services/reset-token-service'; | ||||
| import { EmailService } from '../../../lib/services/email-service'; | ||||
| import { createTestConfig } from '../../config/test-config'; | ||||
| import SessionService from '../../../lib/services/session-service'; | ||||
| 
 | ||||
| let db; | ||||
| let stores; | ||||
| @ -23,11 +24,13 @@ test.before(async () => { | ||||
|     const accessService = new AccessService(stores, config); | ||||
|     const resetTokenService = new ResetTokenService(stores, config); | ||||
|     const emailService = new EmailService(undefined, config.getLogger); | ||||
|     const sessionService = new SessionService(stores, config); | ||||
| 
 | ||||
|     userService = new UserService(stores, config, { | ||||
|         accessService, | ||||
|         resetTokenService, | ||||
|         emailService, | ||||
|         sessionService, | ||||
|     }); | ||||
|     userStore = stores.userStore; | ||||
|     const rootRoles = await accessService.getRootRoles(); | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/test/fixtures/fake-session-store.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/test/fixtures/fake-session-store.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| import SessionStore, { ISession } from '../../lib/db/session-store'; | ||||
| import noLoggerProvider from './no-logger'; | ||||
| 
 | ||||
| export default class FakeSessionStore extends SessionStore { | ||||
|     private sessions: ISession[] = []; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(undefined, undefined, noLoggerProvider); | ||||
|     } | ||||
| 
 | ||||
|     async getActiveSessions(): Promise<ISession[]> { | ||||
|         return this.sessions.filter(session => session.expired != null); | ||||
|     } | ||||
| 
 | ||||
|     async getSessionsForUser(userId: number): Promise<ISession[]> { | ||||
|         return this.sessions.filter(session => session.sess.user.id === userId); | ||||
|     } | ||||
| 
 | ||||
|     async deleteSessionsForUser(userId: number): Promise<void> { | ||||
|         this.sessions = this.sessions.filter( | ||||
|             session => session.sess.user.id !== userId, | ||||
|         ); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user