mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +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,
|
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;
|
module.exports = SessionStore;
|
||||||
|
@ -99,6 +99,12 @@ export const userSchema = {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
example: '01HTMEXAMPLESCIMID7SWWGHN6',
|
example: '01HTMEXAMPLESCIMID7SWWGHN6',
|
||||||
},
|
},
|
||||||
|
activeSessions: {
|
||||||
|
description: 'Count of active browser sessions for this user',
|
||||||
|
type: 'integer',
|
||||||
|
nullable: true,
|
||||||
|
example: 2,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -60,6 +60,14 @@ export default class SessionService {
|
|||||||
}: Pick<ISession, 'sid' | 'sess'>): Promise<ISession> {
|
}: Pick<ISession, 'sid' | 'sess'>): Promise<ISession> {
|
||||||
return this.sessionStore.insertSession({ sid, sess });
|
return this.sessionStore.insertSession({ sid, sess });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSessionsCount() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
(await this.sessionStore.getSessionsCount()).map(
|
||||||
|
({ userId, count }) => [userId, count],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SessionService;
|
module.exports = SessionService;
|
||||||
|
@ -209,6 +209,15 @@ class UserService {
|
|||||||
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
||||||
return { ...u, rootRole: roleId };
|
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;
|
return usersWithRootRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ export type IFlagKey =
|
|||||||
| 'enterprise-payg'
|
| 'enterprise-payg'
|
||||||
| 'simplifyProjectOverview'
|
| 'simplifyProjectOverview'
|
||||||
| 'flagOverviewRedesign'
|
| 'flagOverviewRedesign'
|
||||||
|
| 'showUserDeviceCount'
|
||||||
| 'deleteStaleUserSessions';
|
| 'deleteStaleUserSessions';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
@ -283,6 +284,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_FLAG_OVERVIEW_REDESIGN,
|
process.env.UNLEASH_EXPERIMENTAL_FLAG_OVERVIEW_REDESIGN,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
showUserDeviceCount: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_SHOW_USER_DEVICE_COUNT,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -12,4 +12,5 @@ export interface ISessionStore extends Store<ISession, string> {
|
|||||||
getSessionsForUser(userId: number): Promise<ISession[]>;
|
getSessionsForUser(userId: number): Promise<ISession[]>;
|
||||||
deleteSessionsForUser(userId: number): Promise<void>;
|
deleteSessionsForUser(userId: number): Promise<void>;
|
||||||
insertSession(data: Omit<ISession, 'createdAt'>): Promise<ISession>;
|
insertSession(data: Omit<ISession, 'createdAt'>): Promise<ISession>;
|
||||||
|
getSessionsCount(): Promise<{ userId: number; count: number }[]>;
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ process.nextTick(async () => {
|
|||||||
webhookDomainLogging: true,
|
webhookDomainLogging: true,
|
||||||
releasePlans: false,
|
releasePlans: false,
|
||||||
simplifyProjectOverview: true,
|
simplifyProjectOverview: true,
|
||||||
|
showUserDeviceCount: true,
|
||||||
flagOverviewRedesign: true,
|
flagOverviewRedesign: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -38,6 +38,7 @@ beforeAll(async () => {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
|
showUserDeviceCount: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -428,3 +429,36 @@ test('creates user with email md5 hash', async () => {
|
|||||||
|
|
||||||
expect(user.email_hash).toBe(expectedHash);
|
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);
|
this.sessions.push(session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSessionsCount(): Promise<{ userId: number; count: number }[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user