Client half of #370. Lists every device signed in to your account, with a per-row sign-out and a "sign out all other devices" action. The card does one thing the API alone doesn't: it says "Address changed" when created_ip and last_ip differ, rather than printing two addresses and leaving you to compare them. That mismatch — same device string, different origin — is the shape of a stolen token, and it's the reason IP capture was worth a migration. Making the operator spot it by eye would have wasted the data. Placed with Password and API Token rather than at the bottom of the page: those three are the account-security group, and this is the one that tells you the other two need attention. Details worth naming: - The current session gets a "This device" badge and NO sign-out button — offering one would log you out of the page you're standing on. The server already excludes it from logout-others; this makes that visible. - Sign-out-all-others is a two-step confirm and states the count, so the button can't be a surprise. - A 404 on revoke reloads instead of erroring. It means the session is already gone — revoked elsewhere, or expired — so the list was simply stale and showing the truth is the right response. The code is `session_not_found`, not `not_found`: apierror.NotFound(what) prefixes it. - Empty and error states both handled (rule #24); the empty case is practically unreachable since listing requires an authenticated request, and is handled rather than assumed. - User-agent parsing is deliberately coarse. A real UA parser is a dependency and a maintenance burden for a string whose only job is "do you recognise this?" — the addresses carry the actual signal. Tests cover the parts that would be quiet if broken: the current-session badge suppressing its own sign-out button, the address-changed warning appearing and NOT appearing, the two-step confirm not firing on first click, and the load-failure retry. Android parity is a separate decision, not assumed.
This commit is contained in:
@@ -62,3 +62,33 @@ export async function regenerateAPIToken(): Promise<APITokenResponse> {
|
||||
export async function putMyTimezone(timezone: string): Promise<void> {
|
||||
await api.put<void>('/api/me/timezone', { timezone });
|
||||
}
|
||||
|
||||
// Active sessions (#370) ---------------------------------------------------
|
||||
|
||||
// created_ip is frozen at issue time; last_ip moves with the session. The
|
||||
// pair is the signal — the same device string arriving from an address you
|
||||
// don't recognise is what a stolen token looks like from the inside.
|
||||
export type ActiveSession = {
|
||||
id: string;
|
||||
user_agent: string;
|
||||
created_ip: string;
|
||||
last_ip: string;
|
||||
created_at: string;
|
||||
last_seen_at: string;
|
||||
current: boolean;
|
||||
};
|
||||
|
||||
export async function listSessions(): Promise<ActiveSession[]> {
|
||||
return api.get<ActiveSession[]>('/api/me/sessions');
|
||||
}
|
||||
|
||||
export async function revokeSession(id: string): Promise<void> {
|
||||
await api.del(`/api/me/sessions/${id}`);
|
||||
}
|
||||
|
||||
// Returns how many were ended. The server excludes the caller's own session,
|
||||
// so this never signs you out of the page you pressed it on.
|
||||
export async function revokeOtherSessions(): Promise<number> {
|
||||
const body = await api.post<{ revoked: number }>('/api/me/sessions/logout-others', {});
|
||||
return body.revoked;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user