1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

feat: delete stale user sessions (#8738)

This commit is contained in:
Mateusz Kwasniewski 2024-11-13 15:41:07 +01:00 committed by GitHub
parent d2daae5857
commit c6519cd95d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 3 deletions

View File

@ -2,6 +2,7 @@ import type { IUnleashStores } from '../types/stores';
import type { IUnleashConfig } from '../types/option';
import type { Logger } from '../logger';
import type { ISession, ISessionStore } from '../types/stores/session-store';
import { compareDesc } from 'date-fns';
export default class SessionService {
private logger: Logger;
@ -32,6 +33,26 @@ export default class SessionService {
return this.sessionStore.deleteSessionsForUser(userId);
}
async deleteStaleSessionsForUser(
userId: number,
maxSessions: number,
): Promise<void> {
let userSessions: ISession[] = [];
try {
// this method may throw errors when no session
userSessions = await this.sessionStore.getSessionsForUser(userId);
} catch (e) {}
const newestFirst = userSessions.sort((a, b) =>
compareDesc(a.createdAt, b.createdAt),
);
const sessionsToDelete = newestFirst.slice(maxSessions);
await Promise.all(
sessionsToDelete.map((session) =>
this.sessionStore.delete(session.sid),
),
);
}
async deleteSession(sid: string): Promise<void> {
return this.sessionStore.delete(sid);
}

View File

@ -40,7 +40,7 @@ import type { TokenUserSchema } from '../openapi/spec/token-user-schema';
import PasswordMismatch from '../error/password-mismatch';
import type EventService from '../features/events/event-service';
import { SYSTEM_USER, SYSTEM_USER_AUDIT } from '../types';
import { type IFlagResolver, SYSTEM_USER, SYSTEM_USER_AUDIT } from '../types';
import { PasswordPreviouslyUsedError } from '../error/password-previously-used';
import { RateLimitError } from '../error/rate-limit-error';
import type EventEmitter from 'events';
@ -90,6 +90,8 @@ class UserService {
private settingService: SettingService;
private flagResolver: IFlagResolver;
private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};
private baseUriPath: string;
@ -103,9 +105,14 @@ class UserService {
getLogger,
authentication,
eventBus,
flagResolver,
}: Pick<
IUnleashConfig,
'getLogger' | 'authentication' | 'server' | 'eventBus'
| 'getLogger'
| 'authentication'
| 'server'
| 'eventBus'
| 'flagResolver'
>,
services: {
accessService: AccessService;
@ -125,6 +132,7 @@ class UserService {
this.emailService = services.emailService;
this.sessionService = services.sessionService;
this.settingService = services.settingService;
this.flagResolver = flagResolver;
process.nextTick(() => this.initAdminUser(authentication));
@ -400,6 +408,19 @@ class UserService {
const match = await bcrypt.compare(password, passwordHash);
if (match) {
const loginOrder = await this.store.successfullyLogin(user);
const deleteStaleUserSessions = this.flagResolver.getVariant(
'deleteStaleUserSessions',
);
if (deleteStaleUserSessions.feature_enabled) {
const allowedSessions = Number(
deleteStaleUserSessions.payload?.value || 30,
);
// subtract current user session that will be created
await this.sessionService.deleteStaleSessionsForUser(
user.id,
Math.max(allowedSessions - 1, 0),
);
}
this.eventBus.emit(USER_LOGIN, { loginOrder });
return user;
}

View File

@ -58,7 +58,8 @@ export type IFlagKey =
| 'productivityReportEmail'
| 'enterprise-payg'
| 'simplifyProjectOverview'
| 'flagOverviewRedesign';
| 'flagOverviewRedesign'
| 'deleteStaleUserSessions';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

View File

@ -112,3 +112,18 @@ test('Can delete session by sid', async () => {
sessionService.getSession('abc123'),
).rejects.toThrow(NotFoundError);
});
test('Can delete stale sessions', async () => {
await sessionService.insertSession(newSession);
await sessionService.insertSession({ ...newSession, sid: 'new' });
const sessionsToKeep = 1;
await sessionService.deleteStaleSessionsForUser(
newSession.sess.user.id,
sessionsToKeep,
);
const sessions = await sessionService.getSessionsForUser(1);
expect(sessions.length).toBe(1);
expect(sessions[0].sid).toBe('new');
});