Skip to content

Commit

Permalink
feat: delete stale user sessions (#8738)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Nov 13, 2024
1 parent d2daae5 commit c6519cd
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 3 deletions.
21 changes: 21 additions & 0 deletions src/lib/services/session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
25 changes: 23 additions & 2 deletions src/lib/services/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -90,6 +90,8 @@ class UserService {

private settingService: SettingService;

private flagResolver: IFlagResolver;

private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};

private baseUriPath: string;
Expand All @@ -103,9 +105,14 @@ class UserService {
getLogger,
authentication,
eventBus,
flagResolver,
}: Pick<
IUnleashConfig,
'getLogger' | 'authentication' | 'server' | 'eventBus'
| 'getLogger'
| 'authentication'
| 'server'
| 'eventBus'
| 'flagResolver'
>,
services: {
accessService: AccessService;
Expand All @@ -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));

Expand Down Expand Up @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/types/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export type IFlagKey =
| 'productivityReportEmail'
| 'enterprise-payg'
| 'simplifyProjectOverview'
| 'flagOverviewRedesign';
| 'flagOverviewRedesign'
| 'deleteStaleUserSessions';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

Expand Down
15 changes: 15 additions & 0 deletions src/test/e2e/services/session-service.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

0 comments on commit c6519cd

Please sign in to comment.