mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
Count active browser sessions per user (#8736)
Show info on how many devices a user is logged in to an admin.
This commit is contained in:
parent
bcbbd5c3e6
commit
60fb647489
@ -108,6 +108,18 @@ export default class SessionStore implements ISessionStore {
|
||||
expired: row.expired,
|
||||
};
|
||||
}
|
||||
|
||||
async getSessionsCount(): Promise<{ userId: number; count: number }[]> {
|
||||
const rows = await this.db(TABLE)
|
||||
.select(this.db.raw("sess->'user'->>'id' AS user_id"))
|
||||
.count('* as count')
|
||||
.groupBy('user_id');
|
||||
|
||||
return rows.map((row) => ({
|
||||
userId: Number(row.user_id),
|
||||
count: Number(row.count),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SessionStore;
|
||||
|
@ -99,6 +99,12 @@ export const userSchema = {
|
||||
nullable: true,
|
||||
example: '01HTMEXAMPLESCIMID7SWWGHN6',
|
||||
},
|
||||
activeSessions: {
|
||||
description: 'Count of active browser sessions for this user',
|
||||
type: 'integer',
|
||||
nullable: true,
|
||||
example: 2,
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
@ -60,6 +60,14 @@ export default class SessionService {
|
||||
}: Pick<ISession, 'sid' | 'sess'>): Promise<ISession> {
|
||||
return this.sessionStore.insertSession({ sid, sess });
|
||||
}
|
||||
|
||||
async getSessionsCount() {
|
||||
return Object.fromEntries(
|
||||
(await this.sessionStore.getSessionsCount()).map(
|
||||
({ userId, count }) => [userId, count],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SessionService;
|
||||
|
@ -209,6 +209,15 @@ class UserService {
|
||||
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
||||
return { ...u, rootRole: roleId };
|
||||
});
|
||||
if (this.flagResolver.isEnabled('showUserDeviceCount')) {
|
||||
const sessionCounts = await this.sessionService.getSessionsCount();
|
||||
const usersWithSessionCounts = usersWithRootRole.map((u) => ({
|
||||
...u,
|
||||
activeSessions: sessionCounts[u.id] || 0,
|
||||
}));
|
||||
return usersWithSessionCounts;
|
||||
}
|
||||
|
||||
return usersWithRootRole;
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,7 @@ export type IFlagKey =
|
||||
| 'enterprise-payg'
|
||||
| 'simplifyProjectOverview'
|
||||
| 'flagOverviewRedesign'
|
||||
| 'showUserDeviceCount'
|
||||
| 'deleteStaleUserSessions';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
@ -283,6 +284,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_FLAG_OVERVIEW_REDESIGN,
|
||||
false,
|
||||
),
|
||||
showUserDeviceCount: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_SHOW_USER_DEVICE_COUNT,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -12,4 +12,5 @@ export interface ISessionStore extends Store<ISession, string> {
|
||||
getSessionsForUser(userId: number): Promise<ISession[]>;
|
||||
deleteSessionsForUser(userId: number): Promise<void>;
|
||||
insertSession(data: Omit<ISession, 'createdAt'>): Promise<ISession>;
|
||||
getSessionsCount(): Promise<{ userId: number; count: number }[]>;
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ process.nextTick(async () => {
|
||||
webhookDomainLogging: true,
|
||||
releasePlans: false,
|
||||
simplifyProjectOverview: true,
|
||||
showUserDeviceCount: true,
|
||||
flagOverviewRedesign: true,
|
||||
},
|
||||
},
|
||||
|
@ -38,6 +38,7 @@ beforeAll(async () => {
|
||||
experimental: {
|
||||
flags: {
|
||||
strictSchemaValidation: true,
|
||||
showUserDeviceCount: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -428,3 +429,36 @@ test('creates user with email md5 hash', async () => {
|
||||
|
||||
expect(user.email_hash).toBe(expectedHash);
|
||||
});
|
||||
|
||||
test('should return number of sessions per user', async () => {
|
||||
const user = await userStore.insert({ email: 'tester@example.com' });
|
||||
await sessionStore.insertSession({
|
||||
sid: '1',
|
||||
sess: { user: { id: user.id } },
|
||||
});
|
||||
await sessionStore.insertSession({
|
||||
sid: '2',
|
||||
sess: { user: { id: user.id } },
|
||||
});
|
||||
|
||||
const user2 = await userStore.insert({ email: 'tester2@example.com' });
|
||||
await sessionStore.insertSession({
|
||||
sid: '3',
|
||||
sess: { user: { id: user2.id } },
|
||||
});
|
||||
|
||||
const response = await app.request.get(`/api/admin/user-admin`).expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
users: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'tester@example.com',
|
||||
activeSessions: 2,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
email: 'tester2@example.com',
|
||||
activeSessions: 1,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
4
src/test/fixtures/fake-session-store.ts
vendored
4
src/test/fixtures/fake-session-store.ts
vendored
@ -52,4 +52,8 @@ export default class FakeSessionStore implements ISessionStore {
|
||||
this.sessions.push(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async getSessionsCount(): Promise<{ userId: number; count: number }[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user