@burdenoff/microfe-auth 2026.830.1 → 2026.904.1

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.
Files changed (66) hide show
  1. package/dist/auth/AuthRoot.js +43 -41
  2. package/dist/auth/AuthRoot.js.map +1 -1
  3. package/dist/auth/AuthRoutes.js +7 -0
  4. package/dist/auth/AuthRoutes.js.map +1 -1
  5. package/dist/auth/components/AuthLayout.js +65 -67
  6. package/dist/auth/components/AuthLayout.js.map +1 -1
  7. package/dist/auth/components/CaptchaV2.js +21 -24
  8. package/dist/auth/components/CaptchaV2.js.map +1 -1
  9. package/dist/auth/components/ConfirmActionDialog.js +33 -32
  10. package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
  11. package/dist/auth/components/profile/UserProfileMenu.js +44 -46
  12. package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
  13. package/dist/auth/components/workspace/WorkspaceSelector.js +62 -68
  14. package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
  15. package/dist/auth/pages/AuditLogPage.js +65 -68
  16. package/dist/auth/pages/AuditLogPage.js.map +1 -1
  17. package/dist/auth/pages/EmailVerifyPage.js +51 -54
  18. package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
  19. package/dist/auth/pages/LoginPage.js +223 -225
  20. package/dist/auth/pages/LoginPage.js.map +1 -1
  21. package/dist/auth/pages/MagicLinkPage.js +67 -69
  22. package/dist/auth/pages/MagicLinkPage.js.map +1 -1
  23. package/dist/auth/pages/MfaSetupPage.js +85 -88
  24. package/dist/auth/pages/MfaSetupPage.js.map +1 -1
  25. package/dist/auth/pages/MfaVerifyPage.js +60 -63
  26. package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
  27. package/dist/auth/pages/OAuthCallbackPage.js +37 -40
  28. package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
  29. package/dist/auth/pages/PasswordResetPage.js +102 -105
  30. package/dist/auth/pages/PasswordResetPage.js.map +1 -1
  31. package/dist/auth/pages/ReactivatePage.js +54 -57
  32. package/dist/auth/pages/ReactivatePage.js.map +1 -1
  33. package/dist/auth/pages/RegisterPage.js +171 -174
  34. package/dist/auth/pages/RegisterPage.js.map +1 -1
  35. package/dist/auth/pages/SSOClearPage.js +21 -20
  36. package/dist/auth/pages/SSOClearPage.js.map +1 -1
  37. package/dist/auth/pages/SSODepositPage.js +27 -26
  38. package/dist/auth/pages/SSODepositPage.js.map +1 -1
  39. package/dist/auth/pages/SSORelayCallbackPage.js +19 -18
  40. package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
  41. package/dist/auth/pages/SecurityPage.js +247 -250
  42. package/dist/auth/pages/SecurityPage.js.map +1 -1
  43. package/dist/auth/pages/SessionsPage.js +95 -98
  44. package/dist/auth/pages/SessionsPage.js.map +1 -1
  45. package/dist/auth/pages/SsoCallbackPage.js +53 -56
  46. package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
  47. package/dist/auth/pages/SsoConfigPage.js +296 -299
  48. package/dist/auth/pages/SsoConfigPage.js.map +1 -1
  49. package/dist/auth/pages/SsoLoginPage.js +65 -68
  50. package/dist/auth/pages/SsoLoginPage.js.map +1 -1
  51. package/dist/auth/pages/UserDetailsPage.js +124 -127
  52. package/dist/auth/pages/UserDetailsPage.js.map +1 -1
  53. package/dist/auth/pages/UserProfilePage.js +171 -174
  54. package/dist/auth/pages/UserProfilePage.js.map +1 -1
  55. package/dist/auth/pages/UsersPage.js +322 -325
  56. package/dist/auth/pages/UsersPage.js.map +1 -1
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.js +2 -1
  59. package/dist/shared/hooks/index.d.ts +1 -6
  60. package/dist/shared/hooks/useTr.d.ts +16 -0
  61. package/dist/shared/hooks/useTr.js +14 -0
  62. package/dist/shared/hooks/useTr.js.map +1 -0
  63. package/dist/translations/en.d.ts +806 -248
  64. package/dist/translations/en.js +870 -0
  65. package/dist/translations/en.js.map +1 -0
  66. package/package.json +1 -1
@@ -1,65 +1,62 @@
1
1
  import { useAuthStore as e } from "../store/authStore.js";
2
- import { AlertIcon as t, SpinnerIcon as n } from "../components/AuthIcons.js";
3
- import { useSessionOperations as r } from "../hooks/useSessionOperations.js";
4
- import { formatRelativeTime as i, parseBrowserName as a, parseDeviceName as o } from "../utils/formatters.js";
5
- import { ConfirmActionDialog as s } from "../components/ConfirmActionDialog.js";
6
- import { useMemo as c, useState as l } from "react";
7
- import { useNavigate as u } from "react-router";
8
- import { useI18n as d } from "@burdenoff/fe-libs/shared/providers/shell";
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 = u(), { t: y } = d(), b = (e, t) => {
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
- }), M = c(() => {
16
+ }), j = l(() => {
20
17
  let e = /* @__PURE__ */ new Map();
21
- for (let t of S) {
22
- let n = o(t.userAgent ?? ""), r = e.get(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
- }, [S]), [N, P] = l(null), [F, I] = l(null), [L, R] = l(null), [z, B] = l(!1), V = async (e) => {
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 C(e), I("Session revoked successfully"), R(null);
25
+ await S(e), F(v("auth.sessions.revokeSuccess", "Session revoked successfully")), L(null);
29
26
  } catch (e) {
30
- P(e instanceof Error ? e.message : "Failed to revoke session");
27
+ N(e instanceof Error ? e.message : v("auth.sessions.revokeFailed", "Failed to revoke session"));
31
28
  }
32
29
  };
33
- return x ? /* @__PURE__ */ p("div", {
30
+ return b ? /* @__PURE__ */ p("div", {
34
31
  className: "max-w-4xl mx-auto p-6",
35
32
  children: [
36
- /* @__PURE__ */ f(s, {
37
- open: L !== null,
33
+ /* @__PURE__ */ f(c, {
34
+ open: I !== null,
38
35
  onOpenChange: (e) => {
39
- e || R(null);
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: T,
42
+ isPending: w,
46
43
  onConfirm: async () => {
47
- L && await V(L);
44
+ I && await B(I);
48
45
  }
49
46
  }),
50
- /* @__PURE__ */ f(s, {
51
- open: z,
52
- onOpenChange: B,
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: T,
54
+ isPending: w,
58
55
  onConfirm: async () => {
59
56
  try {
60
- await w(), I("All other sessions have been revoked"), B(!1);
57
+ await C(), F(v("auth.sessions.revokeAllSuccess", "All other sessions have been revoked")), z(!1);
61
58
  } catch (e) {
62
- P(e instanceof Error ? e.message : "Failed to revoke all sessions");
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: F
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: N
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: b("auth.sessions.title", "Active Sessions")
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
- b("auth.sessions.sessionCount", "You have"),
84
+ v("auth.sessions.sessionCount", "You have"),
88
85
  " ",
89
- O,
86
+ D,
90
87
  " ",
91
- b(O === 1 ? "auth.sessions.sessionsSingular" : "auth.sessions.sessionsPlural", O === 1 ? "active session" : "active sessions")
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: b("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.")
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: b("auth.sessions.summaryLabel", "Devices signed in")
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: O
107
+ children: D
111
108
  }),
112
109
  /* @__PURE__ */ f("p", {
113
110
  className: "mt-1 text-sm text-text-secondary",
114
- children: b("auth.sessions.summaryHint", "Keep this list trimmed to devices you trust.")
111
+ children: v("auth.sessions.summaryHint", "Keep this list trimmed to devices you trust.")
115
112
  })
116
- ] }), O > 1 && /* @__PURE__ */ f("button", {
113
+ ] }), D > 1 && /* @__PURE__ */ f("button", {
117
114
  type: "button",
118
- onClick: () => B(!0),
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: b("auth.sessions.revokeAllOthers", "Revoke all other sessions")
117
+ children: v("auth.sessions.revokeAllOthers", "Revoke all other sessions")
121
118
  })]
122
119
  })
123
120
  }),
124
- F && /* @__PURE__ */ p("output", {
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: F
125
+ children: P
129
126
  }), /* @__PURE__ */ f("button", {
130
127
  type: "button",
131
- onClick: () => I(null),
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
- N && /* @__PURE__ */ p("div", {
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(t, {
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: N
143
+ children: M
147
144
  }), /* @__PURE__ */ f("button", {
148
145
  type: "button",
149
- onClick: () => P(null),
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
- T ? /* @__PURE__ */ p("div", {
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(n, {
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: b("auth.sessions.loading", "Loading sessions...")
161
+ children: v("auth.sessions.loading", "Loading sessions...")
165
162
  })]
166
- }) : M.length === 0 ? /* @__PURE__ */ f("output", {
163
+ }) : j.length === 0 ? /* @__PURE__ */ f("output", {
167
164
  className: "block",
168
165
  children: /* @__PURE__ */ f(h, {
169
166
  illustration: "empty-sessions",
170
- title: b("auth.sessions.noSessions", "No active sessions found"),
171
- description: b("auth.sessions.noSessionsDesc", "When you sign in on a device it will appear here so you can review and manage it.")
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": T,
177
- children: M.map((e) => /* @__PURE__ */ f("div", {
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 && o(e.userAgent) === "Unknown Device" ? b("auth.sessions.thisDevice", "This Device") : o(e.userAgent)
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: b("auth.sessions.currentSession", "Current Session")
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: a(e.userAgent) === "Unknown Browser" && e.isCurrent ? b("auth.sessions.currentBrowser", "Current Browser") : a(e.userAgent)
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
- b("auth.sessions.lastActive", "Last active"),
287
+ v("auth.sessions.lastActive", "Last active"),
291
288
  " ",
292
- i(e.lastActivityAt)
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: () => R(e.id),
303
- "aria-label": `${b("auth.sessions.revoke", "Revoke")} ${o(e.userAgent)} session`,
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: b("auth.sessions.revoke", "Revoke")
302
+ children: v("auth.sessions.revoke", "Revoke")
306
303
  })]
307
304
  })
308
305
  }, e.id))
309
306
  }),
310
- !T && D > 1 && /* @__PURE__ */ p("div", {
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: j,
316
- disabled: E === 0,
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": b("auth.sessions.prevPage", "Previous page"),
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
- }), b("auth.sessions.prev", "Previous")]
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
- b("auth.sessions.pageOf", "Page"),
333
+ v("auth.sessions.pageOf", "Page"),
337
334
  " ",
338
335
  /* @__PURE__ */ f("span", {
339
336
  className: "font-medium text-text-primary",
340
- children: E + 1
337
+ children: T + 1
341
338
  }),
342
339
  " ",
343
- b("auth.sessions.of", "of"),
340
+ v("auth.sessions.of", "of"),
344
341
  " ",
345
342
  /* @__PURE__ */ f("span", {
346
343
  className: "font-medium text-text-primary",
347
- children: D
344
+ children: E
348
345
  })
349
346
  ]
350
347
  }),
351
348
  /* @__PURE__ */ p("button", {
352
349
  type: "button",
353
- onClick: A,
354
- disabled: !k,
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": b("auth.sessions.nextPage", "Next page"),
357
- children: [b("auth.sessions.next", "Next"), /* @__PURE__ */ f("svg", {
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
- !T && M.length > 0 && /* @__PURE__ */ f(g, {
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: b("auth.sessions.nextEnableMfa", "Turn on two-factor authentication"),
379
- description: b("auth.sessions.nextEnableMfaDesc", "Add a second sign-in step so a stolen password isn't enough."),
380
- onClick: () => v("/auth/mfa")
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: b("auth.sessions.nextSecurityCenter", "Open the Security Center"),
384
- description: b("auth.sessions.nextSecurityCenterDesc", "Review your password and overall account protection."),
385
- onClick: () => v("/auth/security")
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: b("auth.sessions.authRequired", "Authentication Required"),
394
- description: b("auth.sessions.signInToView", "Please sign in to view your active sessions")
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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"}