@burdenoff/microfe-auth 2026.828.2 → 2026.830.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/AuthRoot.js +43 -41
- package/dist/auth/AuthRoot.js.map +1 -1
- package/dist/auth/components/AuthLayout.js +65 -67
- package/dist/auth/components/AuthLayout.js.map +1 -1
- package/dist/auth/components/CaptchaV2.js +21 -24
- package/dist/auth/components/CaptchaV2.js.map +1 -1
- package/dist/auth/components/ConfirmActionDialog.js +33 -32
- package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
- package/dist/auth/components/profile/UserProfileMenu.js +44 -46
- package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
- package/dist/auth/components/workspace/WorkspaceSelector.js +62 -68
- package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
- package/dist/auth/pages/AuditLogPage.js +65 -68
- package/dist/auth/pages/AuditLogPage.js.map +1 -1
- package/dist/auth/pages/EmailVerifyPage.js +51 -54
- package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
- package/dist/auth/pages/LoginPage.js +223 -225
- package/dist/auth/pages/LoginPage.js.map +1 -1
- package/dist/auth/pages/MagicLinkPage.js +67 -69
- package/dist/auth/pages/MagicLinkPage.js.map +1 -1
- package/dist/auth/pages/MfaSetupPage.js +85 -88
- package/dist/auth/pages/MfaSetupPage.js.map +1 -1
- package/dist/auth/pages/MfaVerifyPage.js +60 -63
- package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
- package/dist/auth/pages/OAuthCallbackPage.js +37 -40
- package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
- package/dist/auth/pages/PasswordResetPage.js +102 -105
- package/dist/auth/pages/PasswordResetPage.js.map +1 -1
- package/dist/auth/pages/ReactivatePage.js +54 -57
- package/dist/auth/pages/ReactivatePage.js.map +1 -1
- package/dist/auth/pages/RegisterPage.js +171 -174
- package/dist/auth/pages/RegisterPage.js.map +1 -1
- package/dist/auth/pages/SSOClearPage.js +21 -20
- package/dist/auth/pages/SSOClearPage.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.js +27 -26
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.js +19 -18
- package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
- package/dist/auth/pages/SecurityPage.js +247 -250
- package/dist/auth/pages/SecurityPage.js.map +1 -1
- package/dist/auth/pages/SessionsPage.js +95 -98
- package/dist/auth/pages/SessionsPage.js.map +1 -1
- package/dist/auth/pages/SsoCallbackPage.js +53 -56
- package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
- package/dist/auth/pages/SsoConfigPage.js +296 -299
- package/dist/auth/pages/SsoConfigPage.js.map +1 -1
- package/dist/auth/pages/SsoLoginPage.js +65 -68
- package/dist/auth/pages/SsoLoginPage.js.map +1 -1
- package/dist/auth/pages/UserDetailsPage.js +124 -127
- package/dist/auth/pages/UserDetailsPage.js.map +1 -1
- package/dist/auth/pages/UserProfilePage.js +171 -174
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.js +322 -325
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/shared/hooks/index.d.ts +1 -6
- package/dist/shared/hooks/useTr.d.ts +16 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/dist/translations/en.d.ts +806 -248
- package/dist/translations/en.js +870 -0
- package/dist/translations/en.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,65 +1,62 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { useTr as t } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { AlertIcon as n, SpinnerIcon as r } from "../components/AuthIcons.js";
|
|
4
|
+
import { useSessionOperations as i } from "../hooks/useSessionOperations.js";
|
|
5
|
+
import { formatRelativeTime as a, parseBrowserName as o, parseDeviceName as s } from "../utils/formatters.js";
|
|
6
|
+
import { ConfirmActionDialog as c } from "../components/ConfirmActionDialog.js";
|
|
7
|
+
import { useMemo as l, useState as u } from "react";
|
|
8
|
+
import { useNavigate as d } from "react-router";
|
|
9
9
|
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
10
10
|
import { EmphasisPanel as m, IllustratedEmptyState as h, NextSteps as g, PagePurpose as _ } from "@burdenoff/fe-libs/ui";
|
|
11
11
|
//#region src/auth/pages/SessionsPage.tsx
|
|
12
12
|
function v() {
|
|
13
|
-
let v =
|
|
14
|
-
let n = y(e);
|
|
15
|
-
return n === e ? t : n;
|
|
16
|
-
}, x = e((e) => e.isAuthenticated), { activeSessions: S, revokeSession: C, revokeAllSessions: w, isLoading: T, page: E, totalPages: D, totalCount: O, hasMore: k, goToNextPage: A, goToPrevPage: j } = r({
|
|
13
|
+
let v = t(), y = d(), b = e((e) => e.isAuthenticated), { activeSessions: x, revokeSession: S, revokeAllSessions: C, isLoading: w, page: T, totalPages: E, totalCount: D, hasMore: O, goToNextPage: k, goToPrevPage: A } = i({
|
|
17
14
|
disableTokenInfoQuery: !0,
|
|
18
15
|
pageSize: 20
|
|
19
|
-
}),
|
|
16
|
+
}), j = l(() => {
|
|
20
17
|
let e = /* @__PURE__ */ new Map();
|
|
21
|
-
for (let t of
|
|
22
|
-
let n =
|
|
18
|
+
for (let t of x) {
|
|
19
|
+
let n = s(t.userAgent ?? ""), r = e.get(n);
|
|
23
20
|
(!r || new Date(t.lastActivityAt) > new Date(r.lastActivityAt)) && e.set(n, t);
|
|
24
21
|
}
|
|
25
22
|
return [...e.values()];
|
|
26
|
-
}, [
|
|
23
|
+
}, [x]), [M, N] = u(null), [P, F] = u(null), [I, L] = u(null), [R, z] = u(!1), B = async (e) => {
|
|
27
24
|
try {
|
|
28
|
-
await
|
|
25
|
+
await S(e), F(v("auth.sessions.revokeSuccess", "Session revoked successfully")), L(null);
|
|
29
26
|
} catch (e) {
|
|
30
|
-
|
|
27
|
+
N(e instanceof Error ? e.message : v("auth.sessions.revokeFailed", "Failed to revoke session"));
|
|
31
28
|
}
|
|
32
29
|
};
|
|
33
|
-
return
|
|
30
|
+
return b ? /* @__PURE__ */ p("div", {
|
|
34
31
|
className: "max-w-4xl mx-auto p-6",
|
|
35
32
|
children: [
|
|
36
|
-
/* @__PURE__ */ f(
|
|
37
|
-
open:
|
|
33
|
+
/* @__PURE__ */ f(c, {
|
|
34
|
+
open: I !== null,
|
|
38
35
|
onOpenChange: (e) => {
|
|
39
|
-
e ||
|
|
36
|
+
e || L(null);
|
|
40
37
|
},
|
|
41
|
-
title: "Revoke this session?",
|
|
42
|
-
description: "The selected device will be signed out immediately and will need to authenticate again.",
|
|
43
|
-
confirmText: "Revoke session",
|
|
38
|
+
title: v("auth.sessions.revokeSessionTitle", "Revoke this session?"),
|
|
39
|
+
description: v("auth.sessions.revokeSessionDesc", "The selected device will be signed out immediately and will need to authenticate again."),
|
|
40
|
+
confirmText: v("auth.sessions.revokeSessionConfirm", "Revoke session"),
|
|
44
41
|
variant: "danger",
|
|
45
|
-
isPending:
|
|
42
|
+
isPending: w,
|
|
46
43
|
onConfirm: async () => {
|
|
47
|
-
|
|
44
|
+
I && await B(I);
|
|
48
45
|
}
|
|
49
46
|
}),
|
|
50
|
-
/* @__PURE__ */ f(
|
|
51
|
-
open:
|
|
52
|
-
onOpenChange:
|
|
53
|
-
title: "Revoke every other session?",
|
|
54
|
-
description: "All devices except your current one will be signed out immediately.",
|
|
55
|
-
confirmText: "Revoke all other sessions",
|
|
47
|
+
/* @__PURE__ */ f(c, {
|
|
48
|
+
open: R,
|
|
49
|
+
onOpenChange: z,
|
|
50
|
+
title: v("auth.sessions.revokeAllTitle", "Revoke every other session?"),
|
|
51
|
+
description: v("auth.sessions.revokeAllDesc", "All devices except your current one will be signed out immediately."),
|
|
52
|
+
confirmText: v("auth.sessions.revokeAllConfirm", "Revoke all other sessions"),
|
|
56
53
|
variant: "warning",
|
|
57
|
-
isPending:
|
|
54
|
+
isPending: w,
|
|
58
55
|
onConfirm: async () => {
|
|
59
56
|
try {
|
|
60
|
-
await
|
|
57
|
+
await C(), F(v("auth.sessions.revokeAllSuccess", "All other sessions have been revoked")), z(!1);
|
|
61
58
|
} catch (e) {
|
|
62
|
-
|
|
59
|
+
N(e instanceof Error ? e.message : v("auth.sessions.revokeAllFailed", "Failed to revoke all sessions"));
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
62
|
}),
|
|
@@ -67,34 +64,34 @@ function v() {
|
|
|
67
64
|
"aria-live": "polite",
|
|
68
65
|
"aria-atomic": "true",
|
|
69
66
|
className: "sr-only",
|
|
70
|
-
children:
|
|
67
|
+
children: P
|
|
71
68
|
}),
|
|
72
69
|
/* @__PURE__ */ f("div", {
|
|
73
70
|
role: "alert",
|
|
74
71
|
"aria-live": "assertive",
|
|
75
72
|
"aria-atomic": "true",
|
|
76
73
|
className: "sr-only",
|
|
77
|
-
children:
|
|
74
|
+
children: M
|
|
78
75
|
}),
|
|
79
76
|
/* @__PURE__ */ p("div", {
|
|
80
77
|
className: "flex items-center justify-between mb-4",
|
|
81
78
|
children: [/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("h1", {
|
|
82
79
|
className: "text-3xl font-semibold text-text-primary",
|
|
83
|
-
children:
|
|
80
|
+
children: v("auth.sessions.title", "Active Sessions")
|
|
84
81
|
}), /* @__PURE__ */ p("p", {
|
|
85
82
|
className: "text-text-secondary mt-2",
|
|
86
83
|
children: [
|
|
87
|
-
|
|
84
|
+
v("auth.sessions.sessionCount", "You have"),
|
|
88
85
|
" ",
|
|
89
|
-
|
|
86
|
+
D,
|
|
90
87
|
" ",
|
|
91
|
-
|
|
88
|
+
v(D === 1 ? "auth.sessions.sessionsSingular" : "auth.sessions.sessionsPlural", D === 1 ? "active session" : "active sessions")
|
|
92
89
|
]
|
|
93
90
|
})] }), /* @__PURE__ */ f("div", { className: "flex items-center gap-3" })]
|
|
94
91
|
}),
|
|
95
92
|
/* @__PURE__ */ f(_, {
|
|
96
93
|
className: "mb-6",
|
|
97
|
-
children:
|
|
94
|
+
children: v("auth.sessions.purpose", "This is every device currently signed in to your account. Recognise them all? If a device looks unfamiliar or you've lost it, revoke its session to sign it out immediately.")
|
|
98
95
|
}),
|
|
99
96
|
/* @__PURE__ */ f(m, {
|
|
100
97
|
className: "mb-8",
|
|
@@ -103,78 +100,78 @@ function v() {
|
|
|
103
100
|
children: [/* @__PURE__ */ p("div", { children: [
|
|
104
101
|
/* @__PURE__ */ f("p", {
|
|
105
102
|
className: "text-sm font-medium text-text-secondary",
|
|
106
|
-
children:
|
|
103
|
+
children: v("auth.sessions.summaryLabel", "Devices signed in")
|
|
107
104
|
}),
|
|
108
105
|
/* @__PURE__ */ f("p", {
|
|
109
106
|
className: "mt-1 text-3xl font-semibold text-text-primary",
|
|
110
|
-
children:
|
|
107
|
+
children: D
|
|
111
108
|
}),
|
|
112
109
|
/* @__PURE__ */ f("p", {
|
|
113
110
|
className: "mt-1 text-sm text-text-secondary",
|
|
114
|
-
children:
|
|
111
|
+
children: v("auth.sessions.summaryHint", "Keep this list trimmed to devices you trust.")
|
|
115
112
|
})
|
|
116
|
-
] }),
|
|
113
|
+
] }), D > 1 && /* @__PURE__ */ f("button", {
|
|
117
114
|
type: "button",
|
|
118
|
-
onClick: () =>
|
|
115
|
+
onClick: () => z(!0),
|
|
119
116
|
className: "shrink-0 px-4 py-2.5 rounded-xl border border-status-error-border text-status-error-text font-medium hover:bg-status-error-bg-subtle transition-colors duration-200",
|
|
120
|
-
children:
|
|
117
|
+
children: v("auth.sessions.revokeAllOthers", "Revoke all other sessions")
|
|
121
118
|
})]
|
|
122
119
|
})
|
|
123
120
|
}),
|
|
124
|
-
|
|
121
|
+
P && /* @__PURE__ */ p("output", {
|
|
125
122
|
className: "block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border",
|
|
126
123
|
children: [/* @__PURE__ */ f("p", {
|
|
127
124
|
className: "text-status-success-text",
|
|
128
|
-
children:
|
|
125
|
+
children: P
|
|
129
126
|
}), /* @__PURE__ */ f("button", {
|
|
130
127
|
type: "button",
|
|
131
|
-
onClick: () =>
|
|
128
|
+
onClick: () => F(null),
|
|
132
129
|
className: "mt-2 text-sm text-status-success-text hover:underline transition-colors duration-200",
|
|
133
|
-
children: "Dismiss"
|
|
130
|
+
children: v("auth.sessions.dismiss", "Dismiss")
|
|
134
131
|
})]
|
|
135
132
|
}),
|
|
136
|
-
|
|
133
|
+
M && /* @__PURE__ */ p("div", {
|
|
137
134
|
role: "alert",
|
|
138
135
|
className: "mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border",
|
|
139
|
-
children: [/* @__PURE__ */ f(
|
|
136
|
+
children: [/* @__PURE__ */ f(n, {
|
|
140
137
|
className: "size-5 text-status-error-text flex-shrink-0 mt-0.5",
|
|
141
138
|
"aria-hidden": "true"
|
|
142
139
|
}), /* @__PURE__ */ p("div", {
|
|
143
140
|
className: "flex-1",
|
|
144
141
|
children: [/* @__PURE__ */ f("p", {
|
|
145
142
|
className: "text-sm text-status-error-text",
|
|
146
|
-
children:
|
|
143
|
+
children: M
|
|
147
144
|
}), /* @__PURE__ */ f("button", {
|
|
148
145
|
type: "button",
|
|
149
|
-
onClick: () =>
|
|
146
|
+
onClick: () => N(null),
|
|
150
147
|
className: "mt-2 text-sm text-status-error-text hover:underline transition-colors duration-200",
|
|
151
|
-
children: "Dismiss"
|
|
148
|
+
children: v("auth.sessions.dismiss", "Dismiss")
|
|
152
149
|
})]
|
|
153
150
|
})]
|
|
154
151
|
}),
|
|
155
|
-
|
|
152
|
+
w ? /* @__PURE__ */ p("div", {
|
|
156
153
|
"aria-live": "polite",
|
|
157
154
|
"aria-busy": "true",
|
|
158
155
|
className: "flex items-center justify-center py-12",
|
|
159
|
-
children: [/* @__PURE__ */ f(
|
|
156
|
+
children: [/* @__PURE__ */ f(r, {
|
|
160
157
|
className: "size-8 text-text-link animate-spin",
|
|
161
158
|
"aria-hidden": "true"
|
|
162
159
|
}), /* @__PURE__ */ f("span", {
|
|
163
160
|
className: "ml-3 text-text-secondary",
|
|
164
|
-
children:
|
|
161
|
+
children: v("auth.sessions.loading", "Loading sessions...")
|
|
165
162
|
})]
|
|
166
|
-
}) :
|
|
163
|
+
}) : j.length === 0 ? /* @__PURE__ */ f("output", {
|
|
167
164
|
className: "block",
|
|
168
165
|
children: /* @__PURE__ */ f(h, {
|
|
169
166
|
illustration: "empty-sessions",
|
|
170
|
-
title:
|
|
171
|
-
description:
|
|
167
|
+
title: v("auth.sessions.noSessions", "No active sessions found"),
|
|
168
|
+
description: v("auth.sessions.noSessionsDesc", "When you sign in on a device it will appear here so you can review and manage it.")
|
|
172
169
|
})
|
|
173
170
|
}) : /* @__PURE__ */ f("div", {
|
|
174
171
|
className: "space-y-4",
|
|
175
172
|
"aria-live": "polite",
|
|
176
|
-
"aria-busy":
|
|
177
|
-
children:
|
|
173
|
+
"aria-busy": w,
|
|
174
|
+
children: j.map((e) => /* @__PURE__ */ f("div", {
|
|
178
175
|
className: "p-6 rounded-xl border border-border-default bg-bg-surface shadow-sm hover:shadow-md transition-shadow",
|
|
179
176
|
children: /* @__PURE__ */ p("div", {
|
|
180
177
|
className: "flex items-start justify-between",
|
|
@@ -202,15 +199,15 @@ function v() {
|
|
|
202
199
|
className: "flex items-center gap-3 mb-1",
|
|
203
200
|
children: [/* @__PURE__ */ f("h3", {
|
|
204
201
|
className: "text-lg font-semibold text-text-primary",
|
|
205
|
-
children: e.isCurrent &&
|
|
202
|
+
children: e.isCurrent && s(e.userAgent) === "Unknown Device" ? v("auth.sessions.thisDevice", "This Device") : s(e.userAgent)
|
|
206
203
|
}), e.isCurrent && /* @__PURE__ */ f("output", {
|
|
207
204
|
className: "px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium",
|
|
208
|
-
children:
|
|
205
|
+
children: v("auth.sessions.currentSession", "Current Session")
|
|
209
206
|
})]
|
|
210
207
|
}),
|
|
211
208
|
/* @__PURE__ */ f("p", {
|
|
212
209
|
className: "text-sm text-text-secondary mb-2",
|
|
213
|
-
children:
|
|
210
|
+
children: o(e.userAgent) === "Unknown Browser" && e.isCurrent ? v("auth.sessions.currentBrowser", "Current Browser") : o(e.userAgent)
|
|
214
211
|
}),
|
|
215
212
|
/* @__PURE__ */ p("dl", {
|
|
216
213
|
className: "flex flex-wrap gap-4 text-sm text-text-secondary",
|
|
@@ -233,7 +230,7 @@ function v() {
|
|
|
233
230
|
}),
|
|
234
231
|
/* @__PURE__ */ f("dt", {
|
|
235
232
|
className: "sr-only",
|
|
236
|
-
children: "IP address"
|
|
233
|
+
children: v("auth.sessions.ipAddress", "IP address")
|
|
237
234
|
}),
|
|
238
235
|
/* @__PURE__ */ f("dd", { children: e.ipAddress })
|
|
239
236
|
]
|
|
@@ -261,7 +258,7 @@ function v() {
|
|
|
261
258
|
}),
|
|
262
259
|
/* @__PURE__ */ f("dt", {
|
|
263
260
|
className: "sr-only",
|
|
264
|
-
children: "Location"
|
|
261
|
+
children: v("auth.sessions.location", "Location")
|
|
265
262
|
}),
|
|
266
263
|
/* @__PURE__ */ f("dd", { children: e.location })
|
|
267
264
|
]
|
|
@@ -284,12 +281,12 @@ function v() {
|
|
|
284
281
|
}),
|
|
285
282
|
/* @__PURE__ */ f("dt", {
|
|
286
283
|
className: "sr-only",
|
|
287
|
-
children: "Last active"
|
|
284
|
+
children: v("auth.sessions.lastActive", "Last active")
|
|
288
285
|
}),
|
|
289
286
|
/* @__PURE__ */ p("dd", { children: [
|
|
290
|
-
|
|
287
|
+
v("auth.sessions.lastActive", "Last active"),
|
|
291
288
|
" ",
|
|
292
|
-
|
|
289
|
+
a(e.lastActivityAt)
|
|
293
290
|
] })
|
|
294
291
|
]
|
|
295
292
|
})
|
|
@@ -299,23 +296,23 @@ function v() {
|
|
|
299
296
|
})]
|
|
300
297
|
}), !e.isCurrent && /* @__PURE__ */ f("button", {
|
|
301
298
|
type: "button",
|
|
302
|
-
onClick: () =>
|
|
303
|
-
"aria-label": `${
|
|
299
|
+
onClick: () => L(e.id),
|
|
300
|
+
"aria-label": `${v("auth.sessions.revoke", "Revoke")} ${s(e.userAgent)} session`,
|
|
304
301
|
className: "ml-4 px-4 py-2 rounded-lg text-sm font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors duration-200",
|
|
305
|
-
children:
|
|
302
|
+
children: v("auth.sessions.revoke", "Revoke")
|
|
306
303
|
})]
|
|
307
304
|
})
|
|
308
305
|
}, e.id))
|
|
309
306
|
}),
|
|
310
|
-
!
|
|
307
|
+
!w && E > 1 && /* @__PURE__ */ p("div", {
|
|
311
308
|
className: "flex items-center justify-between mt-6 pt-4 border-t border-border-subtle",
|
|
312
309
|
children: [
|
|
313
310
|
/* @__PURE__ */ p("button", {
|
|
314
311
|
type: "button",
|
|
315
|
-
onClick:
|
|
316
|
-
disabled:
|
|
312
|
+
onClick: A,
|
|
313
|
+
disabled: T === 0,
|
|
317
314
|
className: "flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm",
|
|
318
|
-
"aria-label":
|
|
315
|
+
"aria-label": v("auth.sessions.prevPage", "Previous page"),
|
|
319
316
|
children: [/* @__PURE__ */ f("svg", {
|
|
320
317
|
className: "size-4",
|
|
321
318
|
fill: "none",
|
|
@@ -328,33 +325,33 @@ function v() {
|
|
|
328
325
|
strokeWidth: 2,
|
|
329
326
|
d: "M15 19l-7-7 7-7"
|
|
330
327
|
})
|
|
331
|
-
}),
|
|
328
|
+
}), v("auth.sessions.prev", "Previous")]
|
|
332
329
|
}),
|
|
333
330
|
/* @__PURE__ */ p("span", {
|
|
334
331
|
className: "text-sm text-text-secondary",
|
|
335
332
|
children: [
|
|
336
|
-
|
|
333
|
+
v("auth.sessions.pageOf", "Page"),
|
|
337
334
|
" ",
|
|
338
335
|
/* @__PURE__ */ f("span", {
|
|
339
336
|
className: "font-medium text-text-primary",
|
|
340
|
-
children:
|
|
337
|
+
children: T + 1
|
|
341
338
|
}),
|
|
342
339
|
" ",
|
|
343
|
-
|
|
340
|
+
v("auth.sessions.of", "of"),
|
|
344
341
|
" ",
|
|
345
342
|
/* @__PURE__ */ f("span", {
|
|
346
343
|
className: "font-medium text-text-primary",
|
|
347
|
-
children:
|
|
344
|
+
children: E
|
|
348
345
|
})
|
|
349
346
|
]
|
|
350
347
|
}),
|
|
351
348
|
/* @__PURE__ */ p("button", {
|
|
352
349
|
type: "button",
|
|
353
|
-
onClick:
|
|
354
|
-
disabled: !
|
|
350
|
+
onClick: k,
|
|
351
|
+
disabled: !O,
|
|
355
352
|
className: "flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm",
|
|
356
|
-
"aria-label":
|
|
357
|
-
children: [
|
|
353
|
+
"aria-label": v("auth.sessions.nextPage", "Next page"),
|
|
354
|
+
children: [v("auth.sessions.next", "Next"), /* @__PURE__ */ f("svg", {
|
|
358
355
|
className: "size-4",
|
|
359
356
|
fill: "none",
|
|
360
357
|
viewBox: "0 0 24 24",
|
|
@@ -370,19 +367,19 @@ function v() {
|
|
|
370
367
|
})
|
|
371
368
|
]
|
|
372
369
|
}),
|
|
373
|
-
!
|
|
370
|
+
!w && j.length > 0 && /* @__PURE__ */ f(g, {
|
|
374
371
|
className: "mt-8",
|
|
375
372
|
storageKey: "auth-sessions",
|
|
376
373
|
steps: [{
|
|
377
374
|
id: "enable-mfa",
|
|
378
|
-
label:
|
|
379
|
-
description:
|
|
380
|
-
onClick: () =>
|
|
375
|
+
label: v("auth.sessions.nextEnableMfa", "Turn on two-factor authentication"),
|
|
376
|
+
description: v("auth.sessions.nextEnableMfaDesc", "Add a second sign-in step so a stolen password isn't enough."),
|
|
377
|
+
onClick: () => y("/auth/mfa")
|
|
381
378
|
}, {
|
|
382
379
|
id: "security-center",
|
|
383
|
-
label:
|
|
384
|
-
description:
|
|
385
|
-
onClick: () =>
|
|
380
|
+
label: v("auth.sessions.nextSecurityCenter", "Open the Security Center"),
|
|
381
|
+
description: v("auth.sessions.nextSecurityCenterDesc", "Review your password and overall account protection."),
|
|
382
|
+
onClick: () => y("/auth/security")
|
|
386
383
|
}]
|
|
387
384
|
})
|
|
388
385
|
]
|
|
@@ -390,8 +387,8 @@ function v() {
|
|
|
390
387
|
className: "max-w-4xl mx-auto p-6",
|
|
391
388
|
children: /* @__PURE__ */ f(h, {
|
|
392
389
|
illustration: "empty-generic",
|
|
393
|
-
title:
|
|
394
|
-
description:
|
|
390
|
+
title: v("auth.sessions.authRequired", "Authentication Required"),
|
|
391
|
+
description: v("auth.sessions.signInToView", "Please sign in to view your active sessions")
|
|
395
392
|
})
|
|
396
393
|
});
|
|
397
394
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionsPage.js","names":[],"sources":["../../../src/auth/pages/SessionsPage.tsx"],"sourcesContent":["import { useState, useMemo } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n NextSteps,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useSessionOperations } from \"../hooks/useSessionOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n formatRelativeTime,\n parseDeviceName,\n parseBrowserName,\n} from \"../utils/formatters\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\n\n/**\n * Active sessions management page with professional UI\n *\n * Fragment: SessionCard_Session (src/operations/global/fragments/SessionCard.fragment.graphql)\n * Query: ActiveSessions (src/operations/global/queries/sessions.graphql)\n */\nexport function SessionsPage() {\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n\n const isAuthenticated = useAuthStore((state) => state.isAuthenticated);\n const {\n activeSessions,\n revokeSession,\n revokeAllSessions,\n isLoading,\n page,\n totalPages,\n totalCount,\n hasMore,\n goToNextPage,\n goToPrevPage,\n } = useSessionOperations({ disableTokenInfoQuery: true, pageSize: 20 });\n\n // One entry per device: keep the most-recently-active session for each device name\n const dedupedSessions = useMemo(() => {\n const byDevice = new Map<string, (typeof activeSessions)[number]>();\n for (const s of activeSessions) {\n const device = parseDeviceName(s.userAgent ?? \"\");\n const existing = byDevice.get(device);\n if (\n !existing ||\n new Date(s.lastActivityAt) > new Date(existing.lastActivityAt)\n ) {\n byDevice.set(device, s);\n }\n }\n return [...byDevice.values()];\n }, [activeSessions]);\n\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingSessionId, setPendingSessionId] = useState<string | null>(null);\n const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false);\n\n const handleRevokeSession = async (sessionId: string) => {\n try {\n await revokeSession(sessionId);\n setSuccessMessage(\"Session revoked successfully\");\n setPendingSessionId(null);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to revoke session\");\n }\n };\n\n const handleRevokeAll = async () => {\n try {\n await revokeAllSessions();\n setSuccessMessage(\"All other sessions have been revoked\");\n setShowRevokeAllDialog(false);\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to revoke all sessions\",\n );\n }\n };\n\n if (!isAuthenticated) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.sessions.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.sessions.signInToView\",\n \"Please sign in to view your active sessions\",\n )}\n />\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <ConfirmActionDialog\n open={pendingSessionId !== null}\n onOpenChange={(open) => {\n if (!open) {\n setPendingSessionId(null);\n }\n }}\n title=\"Revoke this session?\"\n description=\"The selected device will be signed out immediately and will need to authenticate again.\"\n confirmText=\"Revoke session\"\n variant=\"danger\"\n isPending={isLoading}\n onConfirm={async () => {\n if (pendingSessionId) {\n await handleRevokeSession(pendingSessionId);\n }\n }}\n />\n <ConfirmActionDialog\n open={showRevokeAllDialog}\n onOpenChange={setShowRevokeAllDialog}\n title=\"Revoke every other session?\"\n description=\"All devices except your current one will be signed out immediately.\"\n confirmText=\"Revoke all other sessions\"\n variant=\"warning\"\n isPending={isLoading}\n onConfirm={handleRevokeAll}\n />\n\n {/* Live regions for screen reader announcements */}\n <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {successMessage}\n </div>\n <div\n role=\"alert\"\n aria-live=\"assertive\"\n aria-atomic=\"true\"\n className=\"sr-only\"\n >\n {error}\n </div>\n\n {/* Header */}\n <div className=\"flex items-center justify-between mb-4\">\n <div>\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.sessions.title\", \"Active Sessions\")}\n </h1>\n <p className=\"text-text-secondary mt-2\">\n {tr(\"auth.sessions.sessionCount\", \"You have\")} {totalCount}{\" \"}\n {tr(\n totalCount !== 1\n ? \"auth.sessions.sessionsPlural\"\n : \"auth.sessions.sessionsSingular\",\n totalCount !== 1 ? \"active sessions\" : \"active session\",\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n {/* Export button — commented out until wspace-export-svc is wired for auth_sessions\n <button\n type=\"button\"\n onClick={handleExport}\n disabled={isExporting}\n aria-label={tr(\n \"auth.sessions.exportAriaLabel\",\n \"Export sessions data\",\n )}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon\n className=\"size-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.sessions.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n {tr(\"auth.sessions.export\", \"Export\")}\n </>\n )}\n </button>\n */}\n </div>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.sessions.purpose\",\n \"This is every device currently signed in to your account. Recognise them all? If a device looks unfamiliar or you've lost it, revoke its session to sign it out immediately.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: account-access summary — the one highlighted zone */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <p className=\"text-sm font-medium text-text-secondary\">\n {tr(\"auth.sessions.summaryLabel\", \"Devices signed in\")}\n </p>\n <p className=\"mt-1 text-3xl font-semibold text-text-primary\">\n {totalCount}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.sessions.summaryHint\",\n \"Keep this list trimmed to devices you trust.\",\n )}\n </p>\n </div>\n {totalCount > 1 && (\n <button\n type=\"button\"\n onClick={() => setShowRevokeAllDialog(true)}\n className=\"shrink-0 px-4 py-2.5 rounded-xl border border-status-error-border text-status-error-text font-medium hover:bg-status-error-bg-subtle transition-colors duration-200\"\n >\n {tr(\"auth.sessions.revokeAllOthers\", \"Revoke all other sessions\")}\n </button>\n )}\n </div>\n </EmphasisPanel>\n\n {/* Success Message */}\n {successMessage && (\n <output className=\"block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border\">\n <p className=\"text-status-success-text\">{successMessage}</p>\n <button\n type=\"button\"\n onClick={() => setSuccessMessage(null)}\n className=\"mt-2 text-sm text-status-success-text hover:underline transition-colors duration-200\"\n >\n Dismiss\n </button>\n </output>\n )}\n\n {/* Error Message */}\n {error && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\"\n aria-hidden=\"true\"\n />\n <div className=\"flex-1\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n <button\n type=\"button\"\n onClick={() => setError(null)}\n className=\"mt-2 text-sm text-status-error-text hover:underline transition-colors duration-200\"\n >\n Dismiss\n </button>\n </div>\n </div>\n )}\n\n {isLoading ? (\n <div\n aria-live=\"polite\"\n aria-busy=\"true\"\n className=\"flex items-center justify-center py-12\"\n >\n <SpinnerIcon\n className=\"size-8 text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <span className=\"ml-3 text-text-secondary\">\n {tr(\"auth.sessions.loading\", \"Loading sessions...\")}\n </span>\n </div>\n ) : dedupedSessions.length === 0 ? (\n <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-sessions\"\n title={tr(\"auth.sessions.noSessions\", \"No active sessions found\")}\n description={tr(\n \"auth.sessions.noSessionsDesc\",\n \"When you sign in on a device it will appear here so you can review and manage it.\",\n )}\n />\n </output>\n ) : (\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={isLoading}>\n {dedupedSessions.map((session) => (\n <div\n key={session.id}\n className=\"p-6 rounded-xl border border-border-default bg-bg-surface shadow-sm hover:shadow-md transition-shadow\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-4\">\n {/* Device Icon */}\n <div\n className=\"size-12 rounded-xl bg-bg-accent flex items-center justify-center flex-shrink-0\"\n aria-hidden=\"true\"\n >\n <svg\n className=\"size-6 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center gap-3 mb-1\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {session.isCurrent &&\n parseDeviceName(session.userAgent) === \"Unknown Device\"\n ? tr(\"auth.sessions.thisDevice\", \"This Device\")\n : parseDeviceName(session.userAgent)}\n </h3>\n {session.isCurrent && (\n <output className=\"px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium\">\n {tr(\n \"auth.sessions.currentSession\",\n \"Current Session\",\n )}\n </output>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mb-2\">\n {parseBrowserName(session.userAgent) ===\n \"Unknown Browser\" && session.isCurrent\n ? tr(\"auth.sessions.currentBrowser\", \"Current Browser\")\n : parseBrowserName(session.userAgent)}\n </p>\n <dl className=\"flex flex-wrap gap-4 text-sm text-text-secondary\">\n {session.ipAddress && session.ipAddress !== \"unknown\" && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9\"\n />\n </svg>\n <dt className=\"sr-only\">IP address</dt>\n <dd>{session.ipAddress}</dd>\n </div>\n )}\n {session.location && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <dt className=\"sr-only\">Location</dt>\n <dd>{session.location}</dd>\n </div>\n )}\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n <dt className=\"sr-only\">Last active</dt>\n <dd>\n {tr(\"auth.sessions.lastActive\", \"Last active\")}{\" \"}\n {formatRelativeTime(session.lastActivityAt)}\n </dd>\n </div>\n </dl>\n </div>\n </div>\n\n {!session.isCurrent && (\n <button\n type=\"button\"\n onClick={() => setPendingSessionId(session.id)}\n aria-label={`${tr(\"auth.sessions.revoke\", \"Revoke\")} ${parseDeviceName(session.userAgent)} session`}\n className=\"ml-4 px-4 py-2 rounded-lg text-sm font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors duration-200\"\n >\n {tr(\"auth.sessions.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Pagination controls */}\n {!isLoading && totalPages > 1 && (\n <div className=\"flex items-center justify-between mt-6 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={goToPrevPage}\n disabled={page === 0}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.prevPage\", \"Previous page\")}\n >\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n {tr(\"auth.sessions.prev\", \"Previous\")}\n </button>\n\n <span className=\"text-sm text-text-secondary\">\n {tr(\"auth.sessions.pageOf\", \"Page\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{page + 1}</span>{\" \"}\n {tr(\"auth.sessions.of\", \"of\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{totalPages}</span>\n </span>\n\n <button\n type=\"button\"\n onClick={goToNextPage}\n disabled={!hasMore}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.nextPage\", \"Next page\")}\n >\n {tr(\"auth.sessions.next\", \"Next\")}\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </div>\n )}\n\n {!isLoading && dedupedSessions.length > 0 && (\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-sessions\"\n steps={[\n {\n id: \"enable-mfa\",\n label: tr(\n \"auth.sessions.nextEnableMfa\",\n \"Turn on two-factor authentication\",\n ),\n description: tr(\n \"auth.sessions.nextEnableMfaDesc\",\n \"Add a second sign-in step so a stolen password isn't enough.\",\n ),\n onClick: () => navigate(\"/auth/mfa\"),\n },\n {\n id: \"security-center\",\n label: tr(\n \"auth.sessions.nextSecurityCenter\",\n \"Open the Security Center\",\n ),\n description: tr(\n \"auth.sessions.nextSecurityCenterDesc\",\n \"Review your password and overall account protection.\",\n ),\n onClick: () => navigate(\"/auth/security\"),\n },\n ]}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionsPage.js","names":[],"sources":["../../../src/auth/pages/SessionsPage.tsx"],"sourcesContent":["import { useState, useMemo } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n NextSteps,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useSessionOperations } from \"../hooks/useSessionOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n formatRelativeTime,\n parseDeviceName,\n parseBrowserName,\n} from \"../utils/formatters\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\n\n/**\n * Active sessions management page with professional UI\n *\n * Fragment: SessionCard_Session (src/operations/global/fragments/SessionCard.fragment.graphql)\n * Query: ActiveSessions (src/operations/global/queries/sessions.graphql)\n */\nexport function SessionsPage() {\n const tr = useTr();\n const navigate = useNavigate();\n\n const isAuthenticated = useAuthStore((state) => state.isAuthenticated);\n const {\n activeSessions,\n revokeSession,\n revokeAllSessions,\n isLoading,\n page,\n totalPages,\n totalCount,\n hasMore,\n goToNextPage,\n goToPrevPage,\n } = useSessionOperations({ disableTokenInfoQuery: true, pageSize: 20 });\n\n // One entry per device: keep the most-recently-active session for each device name\n const dedupedSessions = useMemo(() => {\n const byDevice = new Map<string, (typeof activeSessions)[number]>();\n for (const s of activeSessions) {\n const device = parseDeviceName(s.userAgent ?? \"\");\n const existing = byDevice.get(device);\n if (\n !existing ||\n new Date(s.lastActivityAt) > new Date(existing.lastActivityAt)\n ) {\n byDevice.set(device, s);\n }\n }\n return [...byDevice.values()];\n }, [activeSessions]);\n\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingSessionId, setPendingSessionId] = useState<string | null>(null);\n const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false);\n\n const handleRevokeSession = async (sessionId: string) => {\n try {\n await revokeSession(sessionId);\n setSuccessMessage(\n tr(\"auth.sessions.revokeSuccess\", \"Session revoked successfully\"),\n );\n setPendingSessionId(null);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\"auth.sessions.revokeFailed\", \"Failed to revoke session\"),\n );\n }\n };\n\n const handleRevokeAll = async () => {\n try {\n await revokeAllSessions();\n setSuccessMessage(\n tr(\n \"auth.sessions.revokeAllSuccess\",\n \"All other sessions have been revoked\",\n ),\n );\n setShowRevokeAllDialog(false);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.sessions.revokeAllFailed\",\n \"Failed to revoke all sessions\",\n ),\n );\n }\n };\n\n if (!isAuthenticated) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.sessions.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.sessions.signInToView\",\n \"Please sign in to view your active sessions\",\n )}\n />\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <ConfirmActionDialog\n open={pendingSessionId !== null}\n onOpenChange={(open) => {\n if (!open) {\n setPendingSessionId(null);\n }\n }}\n title={tr(\"auth.sessions.revokeSessionTitle\", \"Revoke this session?\")}\n description={tr(\n \"auth.sessions.revokeSessionDesc\",\n \"The selected device will be signed out immediately and will need to authenticate again.\",\n )}\n confirmText={tr(\"auth.sessions.revokeSessionConfirm\", \"Revoke session\")}\n variant=\"danger\"\n isPending={isLoading}\n onConfirm={async () => {\n if (pendingSessionId) {\n await handleRevokeSession(pendingSessionId);\n }\n }}\n />\n <ConfirmActionDialog\n open={showRevokeAllDialog}\n onOpenChange={setShowRevokeAllDialog}\n title={tr(\n \"auth.sessions.revokeAllTitle\",\n \"Revoke every other session?\",\n )}\n description={tr(\n \"auth.sessions.revokeAllDesc\",\n \"All devices except your current one will be signed out immediately.\",\n )}\n confirmText={tr(\n \"auth.sessions.revokeAllConfirm\",\n \"Revoke all other sessions\",\n )}\n variant=\"warning\"\n isPending={isLoading}\n onConfirm={handleRevokeAll}\n />\n\n {/* Live regions for screen reader announcements */}\n <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {successMessage}\n </div>\n <div\n role=\"alert\"\n aria-live=\"assertive\"\n aria-atomic=\"true\"\n className=\"sr-only\"\n >\n {error}\n </div>\n\n {/* Header */}\n <div className=\"flex items-center justify-between mb-4\">\n <div>\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.sessions.title\", \"Active Sessions\")}\n </h1>\n <p className=\"text-text-secondary mt-2\">\n {tr(\"auth.sessions.sessionCount\", \"You have\")} {totalCount}{\" \"}\n {tr(\n totalCount !== 1\n ? \"auth.sessions.sessionsPlural\"\n : \"auth.sessions.sessionsSingular\",\n totalCount !== 1 ? \"active sessions\" : \"active session\",\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n {/* Export button — commented out until wspace-export-svc is wired for auth_sessions\n <button\n type=\"button\"\n onClick={handleExport}\n disabled={isExporting}\n aria-label={tr(\n \"auth.sessions.exportAriaLabel\",\n \"Export sessions data\",\n )}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon\n className=\"size-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.sessions.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n {tr(\"auth.sessions.export\", \"Export\")}\n </>\n )}\n </button>\n */}\n </div>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.sessions.purpose\",\n \"This is every device currently signed in to your account. Recognise them all? If a device looks unfamiliar or you've lost it, revoke its session to sign it out immediately.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: account-access summary — the one highlighted zone */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <p className=\"text-sm font-medium text-text-secondary\">\n {tr(\"auth.sessions.summaryLabel\", \"Devices signed in\")}\n </p>\n <p className=\"mt-1 text-3xl font-semibold text-text-primary\">\n {totalCount}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.sessions.summaryHint\",\n \"Keep this list trimmed to devices you trust.\",\n )}\n </p>\n </div>\n {totalCount > 1 && (\n <button\n type=\"button\"\n onClick={() => setShowRevokeAllDialog(true)}\n className=\"shrink-0 px-4 py-2.5 rounded-xl border border-status-error-border text-status-error-text font-medium hover:bg-status-error-bg-subtle transition-colors duration-200\"\n >\n {tr(\"auth.sessions.revokeAllOthers\", \"Revoke all other sessions\")}\n </button>\n )}\n </div>\n </EmphasisPanel>\n\n {/* Success Message */}\n {successMessage && (\n <output className=\"block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border\">\n <p className=\"text-status-success-text\">{successMessage}</p>\n <button\n type=\"button\"\n onClick={() => setSuccessMessage(null)}\n className=\"mt-2 text-sm text-status-success-text hover:underline transition-colors duration-200\"\n >\n {tr(\"auth.sessions.dismiss\", \"Dismiss\")}\n </button>\n </output>\n )}\n\n {/* Error Message */}\n {error && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\"\n aria-hidden=\"true\"\n />\n <div className=\"flex-1\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n <button\n type=\"button\"\n onClick={() => setError(null)}\n className=\"mt-2 text-sm text-status-error-text hover:underline transition-colors duration-200\"\n >\n {tr(\"auth.sessions.dismiss\", \"Dismiss\")}\n </button>\n </div>\n </div>\n )}\n\n {isLoading ? (\n <div\n aria-live=\"polite\"\n aria-busy=\"true\"\n className=\"flex items-center justify-center py-12\"\n >\n <SpinnerIcon\n className=\"size-8 text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <span className=\"ml-3 text-text-secondary\">\n {tr(\"auth.sessions.loading\", \"Loading sessions...\")}\n </span>\n </div>\n ) : dedupedSessions.length === 0 ? (\n <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-sessions\"\n title={tr(\"auth.sessions.noSessions\", \"No active sessions found\")}\n description={tr(\n \"auth.sessions.noSessionsDesc\",\n \"When you sign in on a device it will appear here so you can review and manage it.\",\n )}\n />\n </output>\n ) : (\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={isLoading}>\n {dedupedSessions.map((session) => (\n <div\n key={session.id}\n className=\"p-6 rounded-xl border border-border-default bg-bg-surface shadow-sm hover:shadow-md transition-shadow\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-4\">\n {/* Device Icon */}\n <div\n className=\"size-12 rounded-xl bg-bg-accent flex items-center justify-center flex-shrink-0\"\n aria-hidden=\"true\"\n >\n <svg\n className=\"size-6 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center gap-3 mb-1\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {session.isCurrent &&\n parseDeviceName(session.userAgent) === \"Unknown Device\"\n ? tr(\"auth.sessions.thisDevice\", \"This Device\")\n : parseDeviceName(session.userAgent)}\n </h3>\n {session.isCurrent && (\n <output className=\"px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium\">\n {tr(\n \"auth.sessions.currentSession\",\n \"Current Session\",\n )}\n </output>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mb-2\">\n {parseBrowserName(session.userAgent) ===\n \"Unknown Browser\" && session.isCurrent\n ? tr(\"auth.sessions.currentBrowser\", \"Current Browser\")\n : parseBrowserName(session.userAgent)}\n </p>\n <dl className=\"flex flex-wrap gap-4 text-sm text-text-secondary\">\n {session.ipAddress && session.ipAddress !== \"unknown\" && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9\"\n />\n </svg>\n <dt className=\"sr-only\">\n {tr(\"auth.sessions.ipAddress\", \"IP address\")}\n </dt>\n <dd>{session.ipAddress}</dd>\n </div>\n )}\n {session.location && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <dt className=\"sr-only\">\n {tr(\"auth.sessions.location\", \"Location\")}\n </dt>\n <dd>{session.location}</dd>\n </div>\n )}\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n <dt className=\"sr-only\">\n {tr(\"auth.sessions.lastActive\", \"Last active\")}\n </dt>\n <dd>\n {tr(\"auth.sessions.lastActive\", \"Last active\")}{\" \"}\n {formatRelativeTime(session.lastActivityAt)}\n </dd>\n </div>\n </dl>\n </div>\n </div>\n\n {!session.isCurrent && (\n <button\n type=\"button\"\n onClick={() => setPendingSessionId(session.id)}\n aria-label={`${tr(\"auth.sessions.revoke\", \"Revoke\")} ${parseDeviceName(session.userAgent)} session`}\n className=\"ml-4 px-4 py-2 rounded-lg text-sm font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors duration-200\"\n >\n {tr(\"auth.sessions.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Pagination controls */}\n {!isLoading && totalPages > 1 && (\n <div className=\"flex items-center justify-between mt-6 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={goToPrevPage}\n disabled={page === 0}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.prevPage\", \"Previous page\")}\n >\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n {tr(\"auth.sessions.prev\", \"Previous\")}\n </button>\n\n <span className=\"text-sm text-text-secondary\">\n {tr(\"auth.sessions.pageOf\", \"Page\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{page + 1}</span>{\" \"}\n {tr(\"auth.sessions.of\", \"of\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{totalPages}</span>\n </span>\n\n <button\n type=\"button\"\n onClick={goToNextPage}\n disabled={!hasMore}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.nextPage\", \"Next page\")}\n >\n {tr(\"auth.sessions.next\", \"Next\")}\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </div>\n )}\n\n {!isLoading && dedupedSessions.length > 0 && (\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-sessions\"\n steps={[\n {\n id: \"enable-mfa\",\n label: tr(\n \"auth.sessions.nextEnableMfa\",\n \"Turn on two-factor authentication\",\n ),\n description: tr(\n \"auth.sessions.nextEnableMfaDesc\",\n \"Add a second sign-in step so a stolen password isn't enough.\",\n ),\n onClick: () => navigate(\"/auth/mfa\"),\n },\n {\n id: \"security-center\",\n label: tr(\n \"auth.sessions.nextSecurityCenter\",\n \"Open the Security Center\",\n ),\n description: tr(\n \"auth.sessions.nextSecurityCenterDesc\",\n \"Review your password and overall account protection.\",\n ),\n onClick: () => navigate(\"/auth/security\"),\n },\n ]}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|