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:
parent
d2daae5857
commit
c6519cd95d
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -58,7 +58,8 @@ export type IFlagKey =
|
||||
| 'productivityReportEmail'
|
||||
| 'enterprise-payg'
|
||||
| 'simplifyProjectOverview'
|
||||
| 'flagOverviewRedesign';
|
||||
| 'flagOverviewRedesign'
|
||||
| 'deleteStaleUserSessions';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user