mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 07:38:12 +00:00
feat: implement ActiveSessions component and JWT session tracking to manage user device access
This commit is contained in:
65
libs/auth/src/lib/hooks/useSessions.ts
Normal file
65
libs/auth/src/lib/hooks/useSessions.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useApiLazyQuery, useApiMutation, useApiQuery } from '@ema-platform/api';
|
||||
|
||||
export interface MySession {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
/** IP address the session was created from — IAM sends no user agent. */
|
||||
device: string;
|
||||
expiryTime: string;
|
||||
refreshCount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** `/sessions/my-sessions` answers with a tuple, not the usual `{items, count}`. */
|
||||
type SessionsResponse = [MySession[], number];
|
||||
|
||||
const SESSIONS_URL = '/sessions/my-sessions';
|
||||
const ORDER_BY = 'CreatedAt:DESC';
|
||||
|
||||
function unwrapList(data: unknown): SessionsResponse {
|
||||
if (!Array.isArray(data)) return [[], 0];
|
||||
const [items, total] = data as Partial<SessionsResponse>;
|
||||
return [items ?? [], total ?? 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* The signed-in user's login sessions, and the two ways to end them.
|
||||
*
|
||||
* Uses the generic query/mutation endpoints rather than its own slice, so
|
||||
* freshness comes from `refetch()` rather than cache tags — the same shape as
|
||||
* `useTwoFactor`.
|
||||
*/
|
||||
export function useSessions({ skip, take }: { skip: number; take: number }) {
|
||||
const { data, isFetching, refetch } = useApiQuery<SessionsResponse>({
|
||||
url: SESSIONS_URL,
|
||||
params: { skip, take, orderBy: ORDER_BY },
|
||||
});
|
||||
const [fetchAll] = useApiLazyQuery<SessionsResponse>();
|
||||
const [send, { isLoading: isRevoking }] = useApiMutation();
|
||||
|
||||
const [sessions, total] = unwrapList(data);
|
||||
|
||||
/** Every session id the user has, not just the ones on the current page. */
|
||||
const allSessionIds = async (): Promise<string[]> => {
|
||||
// `total` is one page stale at worst; ask for a page big enough to cover it
|
||||
// growing between render and click.
|
||||
const result = await fetchAll({
|
||||
url: SESSIONS_URL,
|
||||
params: { skip: 0, take: Math.max(total, sessions.length) + 20, orderBy: ORDER_BY },
|
||||
}).unwrap();
|
||||
return unwrapList(result)[0].map((s) => s.id);
|
||||
};
|
||||
|
||||
const revoke = async (ids: string[]) => {
|
||||
if (ids.length === 0) return;
|
||||
await send(
|
||||
ids.length === 1
|
||||
? { url: `/sessions/revoke/${ids[0]}`, method: 'DELETE' }
|
||||
: { url: '/sessions/bulk-revoke', method: 'POST', body: { sessionIds: ids } },
|
||||
).unwrap();
|
||||
await refetch();
|
||||
};
|
||||
|
||||
return { sessions, total, isFetching, refetch, revoke, isRevoking, allSessionIds };
|
||||
}
|
||||
Reference in New Issue
Block a user