@burdenoff/microfe-auth 2026.710.2 → 2026.713.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.
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import { useAuthAuditLogsQuery as e } from "../../generated/global-operations.js";
|
|
2
2
|
import { AlertIcon as t, SpinnerIcon as n } from "../components/AuthIcons.js";
|
|
3
3
|
import { formatDateTime as r } from "../utils/formatters.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
import { ConfirmActionDialog as i } from "../components/ConfirmActionDialog.js";
|
|
5
|
+
import { useCreateAuthExportJobMutation as a } from "../../generated/wspace-operations.js";
|
|
6
|
+
import { useState as o } from "react";
|
|
7
|
+
import { useI18n as s } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
8
|
+
import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
9
|
+
import { useFeatureFlagDetails as d } from "@burdenoff/fe-libs/shared/feature-flags";
|
|
10
|
+
import { isQuotaExhaustedError as f } from "@burdenoff/fe-libs/shared/utils";
|
|
11
|
+
import { EmphasisPanel as p, IllustratedEmptyState as m, PagePurpose as h } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { useGatewayClient as g } from "@burdenoff/fe-libs/shared/graphql";
|
|
11
13
|
//#region src/auth/pages/AuditLogPage.tsx
|
|
12
|
-
var
|
|
13
|
-
function
|
|
14
|
-
let { t:
|
|
15
|
-
let n =
|
|
14
|
+
var _ = 20;
|
|
15
|
+
function v() {
|
|
16
|
+
let { t: v } = s(), y = (e, t) => {
|
|
17
|
+
let n = v(e);
|
|
16
18
|
return n === e ? t : n;
|
|
17
|
-
}, [
|
|
18
|
-
skip: !
|
|
19
|
+
}, [b, x] = o(1), S = (b - 1) * _, C = d("auth.audit-log")?.value === !0, { data: w, loading: T, error: E } = e({
|
|
20
|
+
skip: !C,
|
|
19
21
|
fetchPolicy: "cache-and-network",
|
|
20
22
|
variables: { pagination: {
|
|
21
|
-
limit:
|
|
22
|
-
offset:
|
|
23
|
+
limit: _,
|
|
24
|
+
offset: S
|
|
23
25
|
} }
|
|
24
|
-
}),
|
|
25
|
-
|
|
26
|
+
}), D = w?.authAuditLogs ?? [], O = D.length === _, [k, { loading: A }] = a({ client: g("workspace") }), [j, M] = o(null), [N, P] = o(!1), F = async () => {
|
|
27
|
+
M(null);
|
|
26
28
|
try {
|
|
27
|
-
await
|
|
29
|
+
await k({ variables: { input: {
|
|
28
30
|
name: "Audit Log Export",
|
|
29
31
|
entityTypes: ["permission_audit_logs"],
|
|
30
32
|
format: "CSV",
|
|
@@ -32,9 +34,13 @@ function g() {
|
|
|
32
34
|
includeMetadata: !0
|
|
33
35
|
} } });
|
|
34
36
|
} catch (e) {
|
|
35
|
-
|
|
37
|
+
if (f(e)) {
|
|
38
|
+
P(!0), M(y("auth.auditLog.exportQuotaReached", "You have reached your export limit for this month."));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
M(e instanceof Error ? e.message : "Failed to start export");
|
|
36
42
|
}
|
|
37
|
-
},
|
|
43
|
+
}, I = (e) => {
|
|
38
44
|
switch (e) {
|
|
39
45
|
case "LOGIN_SUCCESS": return "OK";
|
|
40
46
|
case "LOGIN_FAILURE": return "ER";
|
|
@@ -46,169 +52,179 @@ function g() {
|
|
|
46
52
|
default: return "AU";
|
|
47
53
|
}
|
|
48
54
|
};
|
|
49
|
-
return
|
|
55
|
+
return C ? /* @__PURE__ */ u("div", {
|
|
50
56
|
className: "max-w-4xl mx-auto p-6",
|
|
51
57
|
children: [
|
|
52
|
-
/* @__PURE__ */
|
|
58
|
+
/* @__PURE__ */ u("div", {
|
|
53
59
|
className: "flex items-center justify-between mb-8",
|
|
54
|
-
children: [/* @__PURE__ */
|
|
60
|
+
children: [/* @__PURE__ */ l("h1", {
|
|
55
61
|
className: "text-3xl font-semibold text-text-primary",
|
|
56
|
-
children:
|
|
57
|
-
}), /* @__PURE__ */
|
|
62
|
+
children: y("auth.auditLog.title", "Security Audit Log")
|
|
63
|
+
}), /* @__PURE__ */ l("button", {
|
|
58
64
|
type: "button",
|
|
59
|
-
onClick: () => void
|
|
60
|
-
disabled:
|
|
61
|
-
"aria-label":
|
|
65
|
+
onClick: () => void F(),
|
|
66
|
+
disabled: A,
|
|
67
|
+
"aria-label": y("auth.auditLog.exportAriaLabel", "Export audit log"),
|
|
62
68
|
className: "flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium",
|
|
63
|
-
children:
|
|
69
|
+
children: A ? /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(n, {
|
|
64
70
|
className: "size-4 animate-spin",
|
|
65
71
|
"aria-hidden": "true"
|
|
66
|
-
}),
|
|
72
|
+
}), y("auth.auditLog.exporting", "Exporting...")] }) : /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l("svg", {
|
|
67
73
|
className: "size-4",
|
|
68
74
|
fill: "none",
|
|
69
75
|
viewBox: "0 0 24 24",
|
|
70
76
|
stroke: "currentColor",
|
|
71
77
|
"aria-hidden": "true",
|
|
72
|
-
children: /* @__PURE__ */
|
|
78
|
+
children: /* @__PURE__ */ l("path", {
|
|
73
79
|
strokeLinecap: "round",
|
|
74
80
|
strokeLinejoin: "round",
|
|
75
81
|
strokeWidth: 2,
|
|
76
82
|
d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
77
83
|
})
|
|
78
|
-
}),
|
|
84
|
+
}), y("auth.auditLog.export", "Export")] })
|
|
79
85
|
})]
|
|
80
86
|
}),
|
|
81
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ l(h, {
|
|
82
88
|
className: "mb-6",
|
|
83
|
-
children:
|
|
89
|
+
children: y("auth.auditLog.purpose", "A running record of security-relevant events on your account — sign-ins, password and MFA changes, and anomalies. Scan it to spot anything you didn't do, and export it for your own records or compliance.")
|
|
84
90
|
}),
|
|
85
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ u(p, {
|
|
86
92
|
className: "mb-6",
|
|
87
|
-
children: [/* @__PURE__ */
|
|
93
|
+
children: [/* @__PURE__ */ l("p", {
|
|
88
94
|
className: "text-sm font-medium text-text-primary",
|
|
89
|
-
children:
|
|
90
|
-
}), /* @__PURE__ */
|
|
95
|
+
children: y("auth.auditLog.emphasisTitle", "What this log captures")
|
|
96
|
+
}), /* @__PURE__ */ l("p", {
|
|
91
97
|
className: "mt-1 text-sm text-text-secondary",
|
|
92
|
-
children:
|
|
98
|
+
children: y("auth.auditLog.emphasisBody", "Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.")
|
|
93
99
|
})]
|
|
94
100
|
}),
|
|
95
|
-
|
|
101
|
+
j && /* @__PURE__ */ u("div", {
|
|
96
102
|
role: "alert",
|
|
97
103
|
className: "mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border",
|
|
98
|
-
children: [/* @__PURE__ */
|
|
104
|
+
children: [/* @__PURE__ */ l(t, {
|
|
99
105
|
className: "size-5 text-status-error-text flex-shrink-0 mt-0.5",
|
|
100
106
|
"aria-hidden": "true"
|
|
101
|
-
}), /* @__PURE__ */
|
|
107
|
+
}), /* @__PURE__ */ l("p", {
|
|
102
108
|
className: "text-sm text-status-error-text",
|
|
103
|
-
children:
|
|
109
|
+
children: j
|
|
104
110
|
})]
|
|
105
111
|
}),
|
|
106
|
-
|
|
112
|
+
/* @__PURE__ */ l(i, {
|
|
113
|
+
open: N,
|
|
114
|
+
onOpenChange: P,
|
|
115
|
+
title: y("auth.auditLog.exportQuotaTitle", "Export Limit Reached"),
|
|
116
|
+
description: y("auth.auditLog.exportQuotaDescription", "You have reached your export limit for this month."),
|
|
117
|
+
confirmText: y("auth.auditLog.exportQuotaViewBilling", "View Billing"),
|
|
118
|
+
cancelText: y("auth.auditLog.exportQuotaDismiss", "Dismiss"),
|
|
119
|
+
variant: "warning",
|
|
120
|
+
onConfirm: () => window.location.assign("/billing")
|
|
121
|
+
}),
|
|
122
|
+
T ? /* @__PURE__ */ u("div", {
|
|
107
123
|
"aria-live": "polite",
|
|
108
124
|
"aria-busy": "true",
|
|
109
125
|
className: "text-center py-12",
|
|
110
|
-
children: [/* @__PURE__ */
|
|
126
|
+
children: [/* @__PURE__ */ l(n, {
|
|
111
127
|
className: "size-8 mx-auto text-text-link animate-spin",
|
|
112
128
|
"aria-hidden": "true"
|
|
113
|
-
}), /* @__PURE__ */
|
|
129
|
+
}), /* @__PURE__ */ l("p", {
|
|
114
130
|
className: "mt-3 text-text-secondary",
|
|
115
|
-
children:
|
|
131
|
+
children: y("auth.auditLog.loading", "Loading audit events...")
|
|
116
132
|
})]
|
|
117
|
-
}) :
|
|
133
|
+
}) : E ? /* @__PURE__ */ l("div", {
|
|
118
134
|
role: "alert",
|
|
119
|
-
children: /* @__PURE__ */
|
|
135
|
+
children: /* @__PURE__ */ l(m, {
|
|
120
136
|
illustration: "empty-generic",
|
|
121
|
-
title:
|
|
122
|
-
description:
|
|
137
|
+
title: y("auth.auditLog.error", "Unable to load audit events"),
|
|
138
|
+
description: y("auth.auditLog.errorDesc", "Something went wrong fetching your security events. Please try again in a moment.")
|
|
123
139
|
})
|
|
124
|
-
}) :
|
|
140
|
+
}) : D.length === 0 ? /* @__PURE__ */ l("output", {
|
|
125
141
|
className: "block",
|
|
126
|
-
children: /* @__PURE__ */
|
|
142
|
+
children: /* @__PURE__ */ l(m, {
|
|
127
143
|
illustration: "empty-notifications",
|
|
128
|
-
title:
|
|
129
|
-
description:
|
|
144
|
+
title: y("auth.auditLog.noEvents", "No security events recorded"),
|
|
145
|
+
description: y("auth.auditLog.noEventsDesc", "As you sign in and change security settings, those events will appear here.")
|
|
130
146
|
})
|
|
131
|
-
}) : /* @__PURE__ */
|
|
147
|
+
}) : /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l("div", {
|
|
132
148
|
className: "space-y-4",
|
|
133
149
|
"aria-live": "polite",
|
|
134
|
-
"aria-busy":
|
|
135
|
-
children:
|
|
150
|
+
"aria-busy": T,
|
|
151
|
+
children: D.map((e) => /* @__PURE__ */ l("div", {
|
|
136
152
|
className: "p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm",
|
|
137
|
-
children: /* @__PURE__ */
|
|
153
|
+
children: /* @__PURE__ */ u("div", {
|
|
138
154
|
className: "flex items-start gap-4",
|
|
139
155
|
children: [
|
|
140
|
-
/* @__PURE__ */
|
|
156
|
+
/* @__PURE__ */ l("div", {
|
|
141
157
|
className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary",
|
|
142
158
|
"aria-hidden": "true",
|
|
143
|
-
children:
|
|
159
|
+
children: I(e.action)
|
|
144
160
|
}),
|
|
145
|
-
/* @__PURE__ */
|
|
161
|
+
/* @__PURE__ */ u("div", {
|
|
146
162
|
className: "flex-1",
|
|
147
163
|
children: [
|
|
148
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ l("h3", {
|
|
149
165
|
className: "text-lg font-medium text-text-primary mb-1",
|
|
150
166
|
children: e.action.replace(/_/g, " ")
|
|
151
167
|
}),
|
|
152
|
-
/* @__PURE__ */
|
|
168
|
+
/* @__PURE__ */ l("p", {
|
|
153
169
|
className: "text-sm text-text-secondary mb-2",
|
|
154
|
-
children: /* @__PURE__ */
|
|
170
|
+
children: /* @__PURE__ */ l("time", {
|
|
155
171
|
dateTime: e.createdAt,
|
|
156
172
|
children: r(e.createdAt)
|
|
157
173
|
})
|
|
158
174
|
}),
|
|
159
|
-
/* @__PURE__ */
|
|
175
|
+
/* @__PURE__ */ u("p", {
|
|
160
176
|
className: "text-sm text-text-secondary",
|
|
161
|
-
children: [e.description ??
|
|
177
|
+
children: [e.description ?? y("auth.auditLog.defaultDescription", "Authentication event"), e.ipAddress && ` • ${e.ipAddress}`]
|
|
162
178
|
})
|
|
163
179
|
]
|
|
164
180
|
}),
|
|
165
|
-
e.action.includes("FAILURE") && /* @__PURE__ */
|
|
181
|
+
e.action.includes("FAILURE") && /* @__PURE__ */ l("output", {
|
|
166
182
|
className: "px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium",
|
|
167
|
-
children:
|
|
183
|
+
children: y("auth.auditLog.failed", "Failed")
|
|
168
184
|
})
|
|
169
185
|
]
|
|
170
186
|
})
|
|
171
187
|
}, e.id))
|
|
172
|
-
}), /* @__PURE__ */
|
|
173
|
-
"aria-label":
|
|
188
|
+
}), /* @__PURE__ */ u("nav", {
|
|
189
|
+
"aria-label": y("auth.auditLog.paginationAriaLabel", "Audit log pagination"),
|
|
174
190
|
className: "mt-6 flex items-center justify-between",
|
|
175
|
-
children: [/* @__PURE__ */
|
|
191
|
+
children: [/* @__PURE__ */ u("p", {
|
|
176
192
|
className: "text-sm text-text-secondary",
|
|
177
193
|
children: [
|
|
178
|
-
|
|
194
|
+
y("auth.auditLog.page", "Page"),
|
|
179
195
|
" ",
|
|
180
|
-
|
|
196
|
+
b
|
|
181
197
|
]
|
|
182
|
-
}), /* @__PURE__ */
|
|
198
|
+
}), /* @__PURE__ */ u("div", {
|
|
183
199
|
className: "flex gap-2",
|
|
184
|
-
children: [/* @__PURE__ */
|
|
200
|
+
children: [/* @__PURE__ */ l("button", {
|
|
185
201
|
type: "button",
|
|
186
|
-
onClick: () =>
|
|
187
|
-
disabled:
|
|
188
|
-
"aria-label":
|
|
202
|
+
onClick: () => x((e) => Math.max(1, e - 1)),
|
|
203
|
+
disabled: b === 1,
|
|
204
|
+
"aria-label": y("auth.auditLog.previousPageAriaLabel", "Go to previous page"),
|
|
189
205
|
className: "px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors",
|
|
190
|
-
children:
|
|
191
|
-
}), /* @__PURE__ */
|
|
206
|
+
children: y("auth.auditLog.previous", "Previous")
|
|
207
|
+
}), /* @__PURE__ */ l("button", {
|
|
192
208
|
type: "button",
|
|
193
|
-
onClick: () =>
|
|
194
|
-
disabled: !
|
|
195
|
-
"aria-label":
|
|
209
|
+
onClick: () => x((e) => e + 1),
|
|
210
|
+
disabled: !O,
|
|
211
|
+
"aria-label": y("auth.auditLog.nextPageAriaLabel", "Go to next page"),
|
|
196
212
|
className: "px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors",
|
|
197
|
-
children:
|
|
213
|
+
children: y("auth.auditLog.next", "Next")
|
|
198
214
|
})]
|
|
199
215
|
})]
|
|
200
216
|
})] })
|
|
201
217
|
]
|
|
202
|
-
}) : /* @__PURE__ */
|
|
218
|
+
}) : /* @__PURE__ */ l("div", {
|
|
203
219
|
className: "max-w-4xl mx-auto p-6",
|
|
204
|
-
children: /* @__PURE__ */
|
|
220
|
+
children: /* @__PURE__ */ l(m, {
|
|
205
221
|
illustration: "empty-generic",
|
|
206
|
-
title:
|
|
207
|
-
description:
|
|
222
|
+
title: y("auth.auditLog.notAvailable", "Page Not Available"),
|
|
223
|
+
description: y("auth.auditLog.notEnabled", "The Security Audit Log is not enabled for your account.")
|
|
208
224
|
})
|
|
209
225
|
});
|
|
210
226
|
}
|
|
211
227
|
//#endregion
|
|
212
|
-
export {
|
|
228
|
+
export { v as AuditLogPage };
|
|
213
229
|
|
|
214
230
|
//# sourceMappingURL=AuditLogPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useFeatureFlagDetails } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { formatDateTime } from \"../utils/formatters\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport {\n useAuthAuditLogsQuery,\n type AuditLogEntry_AuthAuditLogFragment,\n} from \"../../generated/global-operations\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\nconst PAGE_SIZE = 20;\n\n/**\n * Security audit log page with pagination and export\n *\n * Fragment: AuditLogEntry_AuthAuditLog (graphql/operations/global/fragments/AuditLogEntry.fragment.graphql)\n * Query: AuthAuditLogs (graphql/operations/global/queries/auditLog.graphql)\n *\n * TODO: When authAuditLogs backend migrates to AuthAuditLogConnection type,\n * update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }\n * and remove the client-side hasMore approximation below.\n */\nexport function AuditLogPage() {\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 [page, setPage] = useState(1);\n const offset = (page - 1) * PAGE_SIZE;\n\n // Default-deny: only show when the flag is explicitly enabled in the system.\n // useFeatureFlag uses default-allow (returns true when not found), so we use\n // useFeatureFlagDetails + explicit value check instead.\n const auditLogFlag = useFeatureFlagDetails(\"auth.audit-log\");\n const showAuditLog = auditLogFlag?.value === true;\n\n const { data, loading, error } = useAuthAuditLogsQuery({\n skip: !showAuditLog,\n fetchPolicy: \"cache-and-network\",\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n });\n\n const auditLogs = data?.authAuditLogs ?? [];\n // Approximate hasMore: if we received a full page there may be more\n const hasMore = auditLogs.length === PAGE_SIZE;\n\n const wspaceClient = useGatewayClient(\"workspace\");\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation({ client: wspaceClient });\n const [exportError, setExportError] = useState<string | null>(null);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Audit Log Export\",\n // permission_audit_logs covers auth audit logs in the export system\n entityTypes: [\"permission_audit_logs\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n const getEventMarker = (eventType: string) => {\n switch (eventType) {\n case \"LOGIN_SUCCESS\":\n return \"OK\";\n case \"LOGIN_FAILURE\":\n return \"ER\";\n case \"LOGOUT\":\n return \"LO\";\n case \"PASSWORD_CHANGE\":\n return \"PW\";\n case \"MFA_ENABLED\":\n case \"MFA_VERIFIED\":\n return \"MF\";\n case \"ANOMALY_DETECTED\":\n return \"AL\";\n default:\n return \"AU\";\n }\n };\n\n if (!showAuditLog) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.notAvailable\", \"Page Not Available\")}\n description={tr(\n \"auth.auditLog.notEnabled\",\n \"The Security Audit Log is not enabled for your account.\",\n )}\n />\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.auditLog.title\", \"Security Audit Log\")}\n </h1>\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.auditLog.exportAriaLabel\", \"Export audit log\")}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.auditLog.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.auditLog.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.auditLog.purpose\",\n \"A running record of security-relevant events on your account — sign-ins, password and MFA changes, and anomalies. Scan it to spot anything you didn't do, and export it for your own records or compliance.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: what's tracked — the one highlighted zone */}\n <EmphasisPanel className=\"mb-6\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.auditLog.emphasisTitle\", \"What this log captures\")}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.auditLog.emphasisBody\",\n \"Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.\",\n )}\n </p>\n </EmphasisPanel>\n\n {/* Export error */}\n {exportError && (\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 <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n {loading ? (\n <div aria-live=\"polite\" aria-busy=\"true\" className=\"text-center py-12\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-3 text-text-secondary\">\n {tr(\"auth.auditLog.loading\", \"Loading audit events...\")}\n </p>\n </div>\n ) : error ? (\n <div role=\"alert\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.error\", \"Unable to load audit events\")}\n description={tr(\n \"auth.auditLog.errorDesc\",\n \"Something went wrong fetching your security events. Please try again in a moment.\",\n )}\n />\n </div>\n ) : auditLogs.length === 0 ? (\n <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={tr(\"auth.auditLog.noEvents\", \"No security events recorded\")}\n description={tr(\n \"auth.auditLog.noEventsDesc\",\n \"As you sign in and change security settings, those events will appear here.\",\n )}\n />\n </output>\n ) : (\n <>\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={loading}>\n {auditLogs.map((event: AuditLogEntry_AuthAuditLogFragment) => (\n <div\n key={event.id}\n className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\"\n >\n <div className=\"flex items-start gap-4\">\n <div\n className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {getEventMarker(event.action)}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-medium text-text-primary mb-1\">\n {event.action.replace(/_/g, \" \")}\n </h3>\n <p className=\"text-sm text-text-secondary mb-2\">\n <time dateTime={event.createdAt}>\n {formatDateTime(event.createdAt)}\n </time>\n </p>\n <p className=\"text-sm text-text-secondary\">\n {event.description ??\n tr(\n \"auth.auditLog.defaultDescription\",\n \"Authentication event\",\n )}\n {event.ipAddress && ` • ${event.ipAddress}`}\n </p>\n </div>\n {event.action.includes(\"FAILURE\") && (\n <output className=\"px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium\">\n {tr(\"auth.auditLog.failed\", \"Failed\")}\n </output>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <nav\n aria-label={tr(\n \"auth.auditLog.paginationAriaLabel\",\n \"Audit log pagination\",\n )}\n className=\"mt-6 flex items-center justify-between\"\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.auditLog.page\", \"Page\")} {page}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((prev) => Math.max(1, prev - 1))}\n disabled={page === 1}\n aria-label={tr(\n \"auth.auditLog.previousPageAriaLabel\",\n \"Go to previous page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.previous\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.auditLog.nextPageAriaLabel\",\n \"Go to next page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.next\", \"Next\")}\n </button>\n </div>\n </nav>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useFeatureFlagDetails } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { formatDateTime } from \"../utils/formatters\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n useAuthAuditLogsQuery,\n type AuditLogEntry_AuthAuditLogFragment,\n} from \"../../generated/global-operations\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\nconst PAGE_SIZE = 20;\n\n/**\n * Security audit log page with pagination and export\n *\n * Fragment: AuditLogEntry_AuthAuditLog (graphql/operations/global/fragments/AuditLogEntry.fragment.graphql)\n * Query: AuthAuditLogs (graphql/operations/global/queries/auditLog.graphql)\n *\n * TODO: When authAuditLogs backend migrates to AuthAuditLogConnection type,\n * update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }\n * and remove the client-side hasMore approximation below.\n */\nexport function AuditLogPage() {\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 [page, setPage] = useState(1);\n const offset = (page - 1) * PAGE_SIZE;\n\n // Default-deny: only show when the flag is explicitly enabled in the system.\n // useFeatureFlag uses default-allow (returns true when not found), so we use\n // useFeatureFlagDetails + explicit value check instead.\n const auditLogFlag = useFeatureFlagDetails(\"auth.audit-log\");\n const showAuditLog = auditLogFlag?.value === true;\n\n const { data, loading, error } = useAuthAuditLogsQuery({\n skip: !showAuditLog,\n fetchPolicy: \"cache-and-network\",\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n });\n\n const auditLogs = data?.authAuditLogs ?? [];\n // Approximate hasMore: if we received a full page there may be more\n const hasMore = auditLogs.length === PAGE_SIZE;\n\n const wspaceClient = useGatewayClient(\"workspace\");\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation({ client: wspaceClient });\n const [exportError, setExportError] = useState<string | null>(null);\n const [showExportQuotaDialog, setShowExportQuotaDialog] = useState(false);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Audit Log Export\",\n // permission_audit_logs covers auth audit logs in the export system\n entityTypes: [\"permission_audit_logs\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setShowExportQuotaDialog(true);\n setExportError(\n tr(\n \"auth.auditLog.exportQuotaReached\",\n \"You have reached your export limit for this month.\",\n ),\n );\n return;\n }\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n const getEventMarker = (eventType: string) => {\n switch (eventType) {\n case \"LOGIN_SUCCESS\":\n return \"OK\";\n case \"LOGIN_FAILURE\":\n return \"ER\";\n case \"LOGOUT\":\n return \"LO\";\n case \"PASSWORD_CHANGE\":\n return \"PW\";\n case \"MFA_ENABLED\":\n case \"MFA_VERIFIED\":\n return \"MF\";\n case \"ANOMALY_DETECTED\":\n return \"AL\";\n default:\n return \"AU\";\n }\n };\n\n if (!showAuditLog) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.notAvailable\", \"Page Not Available\")}\n description={tr(\n \"auth.auditLog.notEnabled\",\n \"The Security Audit Log is not enabled for your account.\",\n )}\n />\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.auditLog.title\", \"Security Audit Log\")}\n </h1>\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.auditLog.exportAriaLabel\", \"Export audit log\")}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.auditLog.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.auditLog.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.auditLog.purpose\",\n \"A running record of security-relevant events on your account — sign-ins, password and MFA changes, and anomalies. Scan it to spot anything you didn't do, and export it for your own records or compliance.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: what's tracked — the one highlighted zone */}\n <EmphasisPanel className=\"mb-6\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.auditLog.emphasisTitle\", \"What this log captures\")}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.auditLog.emphasisBody\",\n \"Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.\",\n )}\n </p>\n </EmphasisPanel>\n\n {/* Export error */}\n {exportError && (\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 <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n <ConfirmActionDialog\n open={showExportQuotaDialog}\n onOpenChange={setShowExportQuotaDialog}\n title={tr(\"auth.auditLog.exportQuotaTitle\", \"Export Limit Reached\")}\n description={tr(\n \"auth.auditLog.exportQuotaDescription\",\n \"You have reached your export limit for this month.\",\n )}\n confirmText={tr(\"auth.auditLog.exportQuotaViewBilling\", \"View Billing\")}\n cancelText={tr(\"auth.auditLog.exportQuotaDismiss\", \"Dismiss\")}\n variant=\"warning\"\n onConfirm={() => window.location.assign(\"/billing\")}\n />\n\n {loading ? (\n <div aria-live=\"polite\" aria-busy=\"true\" className=\"text-center py-12\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-3 text-text-secondary\">\n {tr(\"auth.auditLog.loading\", \"Loading audit events...\")}\n </p>\n </div>\n ) : error ? (\n <div role=\"alert\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.error\", \"Unable to load audit events\")}\n description={tr(\n \"auth.auditLog.errorDesc\",\n \"Something went wrong fetching your security events. Please try again in a moment.\",\n )}\n />\n </div>\n ) : auditLogs.length === 0 ? (\n <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={tr(\"auth.auditLog.noEvents\", \"No security events recorded\")}\n description={tr(\n \"auth.auditLog.noEventsDesc\",\n \"As you sign in and change security settings, those events will appear here.\",\n )}\n />\n </output>\n ) : (\n <>\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={loading}>\n {auditLogs.map((event: AuditLogEntry_AuthAuditLogFragment) => (\n <div\n key={event.id}\n className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\"\n >\n <div className=\"flex items-start gap-4\">\n <div\n className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {getEventMarker(event.action)}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-medium text-text-primary mb-1\">\n {event.action.replace(/_/g, \" \")}\n </h3>\n <p className=\"text-sm text-text-secondary mb-2\">\n <time dateTime={event.createdAt}>\n {formatDateTime(event.createdAt)}\n </time>\n </p>\n <p className=\"text-sm text-text-secondary\">\n {event.description ??\n tr(\n \"auth.auditLog.defaultDescription\",\n \"Authentication event\",\n )}\n {event.ipAddress && ` • ${event.ipAddress}`}\n </p>\n </div>\n {event.action.includes(\"FAILURE\") && (\n <output className=\"px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium\">\n {tr(\"auth.auditLog.failed\", \"Failed\")}\n </output>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <nav\n aria-label={tr(\n \"auth.auditLog.paginationAriaLabel\",\n \"Audit log pagination\",\n )}\n className=\"mt-6 flex items-center justify-between\"\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.auditLog.page\", \"Page\")} {page}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((prev) => Math.max(1, prev - 1))}\n disabled={page === 1}\n aria-label={tr(\n \"auth.auditLog.previousPageAriaLabel\",\n \"Go to previous page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.previous\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.auditLog.nextPageAriaLabel\",\n \"Go to next page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.next\", \"Next\")}\n </button>\n </div>\n </nav>\n </>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAmBA,IAAM,IAAY;AAYlB,SAAgB,IAAe;CAC7B,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAGnC,CAAC,GAAM,KAAW,EAAS,EAAE,EAC7B,KAAU,IAAO,KAAK,GAMtB,IADe,EAAsB,iBAAiB,EACzB,UAAU,IAEvC,EAAE,SAAM,YAAS,aAAU,EAAsB;EACrD,MAAM,CAAC;EACP,aAAa;EACb,WAAW,EAAE,YAAY;GAAE,OAAO;GAAW;GAAQ,EAAE;EACxD,CAAC,EAEI,IAAY,GAAM,iBAAiB,EAAE,EAErC,IAAU,EAAU,WAAW,GAG/B,CAAC,GAAiB,EAAE,SAAS,OACjC,EAA+B,EAAE,QAFd,EAAiB,YAAY,EAEO,CAAC,EACpD,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAuB,KAA4B,EAAS,GAAM,EAEnE,IAAe,YAAY;AAC/B,IAAe,KAAK;AACpB,MAAI;AACF,SAAM,EAAgB,EACpB,WAAW,EACT,OAAO;IACL,MAAM;IAEN,aAAa,CAAC,wBAAwB;IACtC,QAAQ;IACR,aAAa;IACb,iBAAiB;IAClB,EACF,EACF,CAAC;WACK,GAAK;AACZ,OAAI,EAAsB,EAAI,EAAE;AAE9B,IADA,EAAyB,GAAK,EAC9B,EACE,EACE,oCACA,qDACD,CACF;AACD;;AAEF,KACE,aAAe,QAAQ,EAAI,UAAU,yBACtC;;IAIC,KAAkB,MAAsB;AAC5C,UAAQ,GAAR;GACE,KAAK,gBACH,QAAO;GACT,KAAK,gBACH,QAAO;GACT,KAAK,SACH,QAAO;GACT,KAAK,kBACH,QAAO;GACT,KAAK;GACL,KAAK,eACH,QAAO;GACT,KAAK,mBACH,QAAO;GACT,QACE,QAAO;;;AAmBb,QAfK,IAgBH,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,uBAAuB,qBAAqB;KAC7C,CAAA,EACL,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,KAAK,GAAc;KAClC,UAAU;KACV,cAAY,EAAG,iCAAiC,mBAAmB;KACnE,WAAU;eAET,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAa,WAAU;MAAsB,eAAY;MAAS,CAAA,EACjE,EAAG,2BAA2B,eAAe,CAC7C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;MACP,eAAY;gBAEZ,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA,EACL,EAAG,wBAAwB,SAAS,CACpC,EAAA,CAAA;KAEE,CAAA,CACL;;GAEN,kBAAC,GAAD;IAAa,WAAU;cACpB,EACC,yBACA,8MACD;IACW,CAAA;GAGd,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,+BAA+B,yBAAyB;KAC1D,CAAA,EACJ,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,8BACA,mHACD;KACC,CAAA,CACU;;GAGf,KACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAFZ,CAIE,kBAAC,GAAD;KACE,WAAU;KACV,eAAY;KACZ,CAAA,EACF,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAgB,CAAA,CAC3D;;GAGR,kBAAC,GAAD;IACE,MAAM;IACN,cAAc;IACd,OAAO,EAAG,kCAAkC,uBAAuB;IACnE,aAAa,EACX,wCACA,qDACD;IACD,aAAa,EAAG,wCAAwC,eAAe;IACvE,YAAY,EAAG,oCAAoC,UAAU;IAC7D,SAAQ;IACR,iBAAiB,OAAO,SAAS,OAAO,WAAW;IACnD,CAAA;GAED,IACC,kBAAC,OAAD;IAAK,aAAU;IAAS,aAAU;IAAO,WAAU;cAAnD,CACE,kBAAC,GAAD;KACE,WAAU;KACV,eAAY;KACZ,CAAA,EACF,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,yBAAyB,0BAA0B;KACrD,CAAA,CACA;QACJ,IACF,kBAAC,OAAD;IAAK,MAAK;cACR,kBAAC,GAAD;KACE,cAAa;KACb,OAAO,EAAG,uBAAuB,8BAA8B;KAC/D,aAAa,EACX,2BACA,oFACD;KACD,CAAA;IACE,CAAA,GACJ,EAAU,WAAW,IACvB,kBAAC,UAAD;IAAQ,WAAU;cAChB,kBAAC,GAAD;KACE,cAAa;KACb,OAAO,EAAG,0BAA0B,8BAA8B;KAClE,aAAa,EACX,8BACA,8EACD;KACD,CAAA;IACK,CAAA,GAET,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;IAAY,aAAU;IAAS,aAAW;cACtD,EAAU,KAAK,MACd,kBAAC,OAAD;KAEE,WAAU;eAEV,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QACE,WAAU;QACV,eAAY;kBAEX,EAAe,EAAM,OAAO;QACzB,CAAA;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAM,OAAO,QAAQ,MAAM,IAAI;UAC7B,CAAA;SACL,kBAAC,KAAD;UAAG,WAAU;oBACX,kBAAC,QAAD;WAAM,UAAU,EAAM;qBACnB,EAAe,EAAM,UAAU;WAC3B,CAAA;UACL,CAAA;SACJ,kBAAC,KAAD;UAAG,WAAU;oBAAb,CACG,EAAM,eACL,EACE,oCACA,uBACD,EACF,EAAM,aAAa,MAAM,EAAM,YAC9B;;SACA;;OACL,EAAM,OAAO,SAAS,UAAU,IAC/B,kBAAC,UAAD;QAAQ,WAAU;kBACf,EAAG,wBAAwB,SAAS;QAC9B,CAAA;OAEP;;KACF,EAlCC,EAAM,GAkCP,CACN;IACE,CAAA,EAGN,kBAAC,OAAD;IACE,cAAY,EACV,qCACA,uBACD;IACD,WAAU;cALZ,CAOE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAG,sBAAsB,OAAO;MAAC;MAAE;MAClC;QACJ,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAS,MAAS,KAAK,IAAI,GAAG,IAAO,EAAE,CAAC;MACvD,UAAU,MAAS;MACnB,cAAY,EACV,uCACA,sBACD;MACD,WAAU;gBAET,EAAG,0BAA0B,WAAW;MAClC,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,GAAS,MAAS,IAAO,EAAE;MAC1C,UAAU,CAAC;MACX,cAAY,EACV,mCACA,kBACD;MACD,WAAU;gBAET,EAAG,sBAAsB,OAAO;MAC1B,CAAA,CACL;OACF;MACL,EAAA,CAAA;GAED;MAzNJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAG,8BAA8B,qBAAqB;GAC7D,aAAa,EACX,4BACA,0DACD;GACD,CAAA;EACE,CAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
2
|
import { AlertIcon as t, SpinnerIcon as n } from "../components/AuthIcons.js";
|
|
3
3
|
import { useMfaOperations as r } from "../hooks/useMfaOperations.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { ConfirmActionDialog as i } from "../components/ConfirmActionDialog.js";
|
|
5
|
+
import { QRCodeSVG as a } from "../../node_modules/qrcode.react/lib/esm/index.js";
|
|
6
6
|
import { nativeCopyText as o, nativeImpact as s, nativeNotify as c } from "../../utils/nativeBridge.js";
|
|
7
7
|
import { useState as l } from "react";
|
|
8
8
|
import { useNavigate as u } from "react-router-dom";
|
|
@@ -25,7 +25,7 @@ function v() {
|
|
|
25
25
|
return E ? /* @__PURE__ */ m("div", {
|
|
26
26
|
className: "max-w-2xl mx-auto p-6",
|
|
27
27
|
children: [
|
|
28
|
-
/* @__PURE__ */ p(
|
|
28
|
+
/* @__PURE__ */ p(i, {
|
|
29
29
|
open: L,
|
|
30
30
|
onOpenChange: R,
|
|
31
31
|
title: y("auth.mfaSetup.disableTitle", "Disable two-factor authentication?"),
|
|
@@ -126,7 +126,7 @@ function v() {
|
|
|
126
126
|
className: "flex justify-center",
|
|
127
127
|
children: /* @__PURE__ */ p("div", {
|
|
128
128
|
className: "p-4 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)]",
|
|
129
|
-
children: /* @__PURE__ */ p(
|
|
129
|
+
children: /* @__PURE__ */ p(a, {
|
|
130
130
|
value: D.qrCode,
|
|
131
131
|
size: 192,
|
|
132
132
|
level: "M",
|
|
@@ -2,15 +2,16 @@ import { useAuthStore as e } from "../store/authStore.js";
|
|
|
2
2
|
import { AlertIcon as t, SpinnerIcon as n } from "../components/AuthIcons.js";
|
|
3
3
|
import { useUserOperations as r } from "../hooks/useUserOperations.js";
|
|
4
4
|
import { useAuthPermissions as i } from "../hooks/useAuthPermissions.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { ConfirmActionDialog as a } from "../components/ConfirmActionDialog.js";
|
|
6
|
+
import { useCreateAuthExportJobMutation as o } from "../../generated/wspace-operations.js";
|
|
7
7
|
import { useEffect as s, useState as c } from "react";
|
|
8
8
|
import { useNavigate as l, useParams as u } from "react-router-dom";
|
|
9
9
|
import { useI18n as d } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
10
10
|
import { Fragment as f, jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
11
|
-
import {
|
|
11
|
+
import { isQuotaExhaustedError as h } from "@burdenoff/fe-libs/shared/utils";
|
|
12
|
+
import { EmphasisPanel as g, IllustratedEmptyState as _, NextSteps as v, PagePurpose as y } from "@burdenoff/fe-libs/ui";
|
|
12
13
|
//#region src/auth/pages/UserDetailsPage.tsx
|
|
13
|
-
var
|
|
14
|
+
var b = (e) => {
|
|
14
15
|
switch (e) {
|
|
15
16
|
case "SUPER_ADMIN": return "bg-accent-purple-subtle text-accent-purple";
|
|
16
17
|
case "TENANT_ADMIN": return "bg-status-info-bg-subtle text-status-info-text";
|
|
@@ -20,7 +21,7 @@ var y = (e) => {
|
|
|
20
21
|
case "GUEST": return "bg-bg-sunken text-text-primary";
|
|
21
22
|
default: return "bg-bg-sunken text-text-primary";
|
|
22
23
|
}
|
|
23
|
-
},
|
|
24
|
+
}, x = (e) => {
|
|
24
25
|
switch (e) {
|
|
25
26
|
case "ACTIVE": return "bg-status-success-bg-subtle text-status-success-text";
|
|
26
27
|
case "INACTIVE": return "bg-status-warning-bg-subtle text-status-warning-text";
|
|
@@ -29,59 +30,59 @@ var y = (e) => {
|
|
|
29
30
|
default: return "bg-bg-sunken text-text-primary";
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
|
-
function
|
|
33
|
-
let { id:
|
|
34
|
-
let n =
|
|
33
|
+
function S() {
|
|
34
|
+
let { id: S } = u(), C = l(), { t: w } = d(), T = (e, t) => {
|
|
35
|
+
let n = w(e);
|
|
35
36
|
return n === e ? t : n;
|
|
36
|
-
},
|
|
37
|
+
}, E = e((e) => e.isAuthenticated), D = e((e) => e.user), { canViewUsers: O, canDeleteUsers: k, isLoading: A } = i(), { getUser: j, deleteUser: M, hardDeleteUser: N } = r(), [P, F] = c(null), [I, L] = c(!0), [R, z] = c(null), [B, V] = c(null), [H, U] = c(null), [W, { loading: G }] = o(), [K, q] = c(null), [J, Y] = c(!1);
|
|
37
38
|
s(() => {
|
|
38
39
|
let e = !1;
|
|
39
|
-
if (!
|
|
40
|
-
|
|
40
|
+
if (!S) {
|
|
41
|
+
z("Missing user id"), L(!1);
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
|
-
if (!
|
|
44
|
-
|
|
44
|
+
if (!E) {
|
|
45
|
+
L(!1);
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
|
-
return
|
|
48
|
-
e || (t ?
|
|
48
|
+
return L(!0), z(null), j(S).then((t) => {
|
|
49
|
+
e || (t ? F(t) : z(T("auth.userDetails.userNotFound", "User not found")));
|
|
49
50
|
}).catch((t) => {
|
|
50
|
-
e ||
|
|
51
|
+
e || z(t instanceof Error ? t.message : "Failed to fetch user details");
|
|
51
52
|
}).finally(() => {
|
|
52
|
-
e ||
|
|
53
|
+
e || L(!1);
|
|
53
54
|
}), () => {
|
|
54
55
|
e = !0;
|
|
55
56
|
};
|
|
56
57
|
}, [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
S,
|
|
59
|
+
j,
|
|
60
|
+
E
|
|
60
61
|
]);
|
|
61
|
-
let
|
|
62
|
-
if (
|
|
63
|
-
let e = await
|
|
62
|
+
let X = async () => {
|
|
63
|
+
if (P) try {
|
|
64
|
+
let e = await M(P.id);
|
|
64
65
|
if (e.success) {
|
|
65
|
-
|
|
66
|
-
let e = await
|
|
67
|
-
e &&
|
|
68
|
-
} else
|
|
66
|
+
V(T("auth.userDetails.userDeactivated", "User \"{email}\" has been deactivated.").replace("{email}", P.email)), U(null);
|
|
67
|
+
let e = await j(P.id);
|
|
68
|
+
e && F(e);
|
|
69
|
+
} else z(e.message);
|
|
69
70
|
} catch (e) {
|
|
70
|
-
|
|
71
|
+
z(e instanceof Error ? e.message : "Failed to delete user");
|
|
71
72
|
}
|
|
72
|
-
},
|
|
73
|
-
if (
|
|
74
|
-
let e = await
|
|
75
|
-
e.success ? (
|
|
73
|
+
}, Z = async () => {
|
|
74
|
+
if (P) try {
|
|
75
|
+
let e = await N(P.id);
|
|
76
|
+
e.success ? (U(null), C("/auth/users")) : z(e.message);
|
|
76
77
|
} catch (e) {
|
|
77
|
-
|
|
78
|
+
z(e instanceof Error ? e.message : "Failed to hard delete user");
|
|
78
79
|
}
|
|
79
|
-
},
|
|
80
|
-
if (
|
|
81
|
-
|
|
80
|
+
}, Q = async () => {
|
|
81
|
+
if (P) {
|
|
82
|
+
q(null);
|
|
82
83
|
try {
|
|
83
|
-
await
|
|
84
|
-
name: `User Data Export - ${
|
|
84
|
+
await W({ variables: { input: {
|
|
85
|
+
name: `User Data Export - ${P.email}`,
|
|
85
86
|
entityTypes: [
|
|
86
87
|
"user_profile",
|
|
87
88
|
"user_sessions",
|
|
@@ -92,11 +93,15 @@ function x() {
|
|
|
92
93
|
includeMetadata: !0
|
|
93
94
|
} } });
|
|
94
95
|
} catch (e) {
|
|
95
|
-
|
|
96
|
+
if (h(e)) {
|
|
97
|
+
Y(!0), q(T("auth.userDetails.exportQuotaReached", "You have reached your export limit for this month."));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
q(e instanceof Error ? e.message : "Failed to start export");
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
};
|
|
99
|
-
return
|
|
104
|
+
return E ? A || I ? /* @__PURE__ */ p("div", {
|
|
100
105
|
className: "max-w-4xl mx-auto p-6",
|
|
101
106
|
children: /* @__PURE__ */ m("div", {
|
|
102
107
|
className: "text-center py-16",
|
|
@@ -107,14 +112,14 @@ function x() {
|
|
|
107
112
|
"aria-hidden": "true"
|
|
108
113
|
}), /* @__PURE__ */ p("p", {
|
|
109
114
|
className: "mt-4 text-text-secondary",
|
|
110
|
-
children:
|
|
115
|
+
children: T("auth.userDetails.loading", "Loading user...")
|
|
111
116
|
})]
|
|
112
117
|
})
|
|
113
|
-
}) :
|
|
118
|
+
}) : O ? R || !P ? /* @__PURE__ */ m("div", {
|
|
114
119
|
className: "max-w-4xl mx-auto p-6",
|
|
115
120
|
children: [/* @__PURE__ */ m("button", {
|
|
116
121
|
type: "button",
|
|
117
|
-
onClick: () =>
|
|
122
|
+
onClick: () => C("/auth/users"),
|
|
118
123
|
className: "mb-6 flex items-center gap-2 text-text-link hover:underline font-medium",
|
|
119
124
|
children: [/* @__PURE__ */ p("svg", {
|
|
120
125
|
className: "size-4",
|
|
@@ -128,36 +133,36 @@ function x() {
|
|
|
128
133
|
strokeWidth: 2,
|
|
129
134
|
d: "M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
130
135
|
})
|
|
131
|
-
}),
|
|
136
|
+
}), T("auth.userDetails.backToUsers", "Back to Users")]
|
|
132
137
|
}), /* @__PURE__ */ p("div", {
|
|
133
138
|
role: "alert",
|
|
134
|
-
children: /* @__PURE__ */ p(
|
|
139
|
+
children: /* @__PURE__ */ p(_, {
|
|
135
140
|
illustration: "empty-search",
|
|
136
|
-
title:
|
|
137
|
-
description:
|
|
141
|
+
title: T("auth.userDetails.userNotFound", "User not found"),
|
|
142
|
+
description: R ?? T("auth.userDetails.userNotFoundDesc", "We couldn't find this user. They may have been removed, or the link is out of date.")
|
|
138
143
|
})
|
|
139
144
|
})]
|
|
140
145
|
}) : /* @__PURE__ */ m("div", {
|
|
141
146
|
className: "max-w-4xl mx-auto p-6",
|
|
142
147
|
children: [
|
|
143
|
-
/* @__PURE__ */ p(
|
|
144
|
-
open:
|
|
148
|
+
/* @__PURE__ */ p(a, {
|
|
149
|
+
open: H !== null,
|
|
145
150
|
onOpenChange: (e) => {
|
|
146
|
-
e ||
|
|
151
|
+
e || U(null);
|
|
147
152
|
},
|
|
148
|
-
title:
|
|
149
|
-
description:
|
|
150
|
-
confirmText:
|
|
151
|
-
variant:
|
|
153
|
+
title: H === "hard-delete" ? T("auth.userDetails.hardDeleteTitle", "Permanently delete user?") : T("auth.userDetails.deactivateTitle", "Deactivate user?"),
|
|
154
|
+
description: H === "hard-delete" ? T("auth.userDetails.hardDeleteDesc", "This permanently deletes \"{email}\" and cannot be undone.").replace("{email}", P.email) : T("auth.userDetails.deactivateDesc", "\"{email}\" will be prevented from signing in until reactivated.").replace("{email}", P.email),
|
|
155
|
+
confirmText: H === "hard-delete" ? T("auth.userDetails.hardDeleteConfirm", "Delete permanently") : T("auth.userDetails.deactivateConfirm", "Deactivate user"),
|
|
156
|
+
variant: H === "hard-delete" ? "danger" : "warning",
|
|
152
157
|
onConfirm: async () => {
|
|
153
|
-
|
|
158
|
+
H === "hard-delete" ? await Z() : H === "deactivate" && await X();
|
|
154
159
|
}
|
|
155
160
|
}),
|
|
156
161
|
/* @__PURE__ */ m("div", {
|
|
157
162
|
className: "flex items-center justify-between mb-6",
|
|
158
163
|
children: [/* @__PURE__ */ m("button", {
|
|
159
164
|
type: "button",
|
|
160
|
-
onClick: () =>
|
|
165
|
+
onClick: () => C("/auth/users"),
|
|
161
166
|
className: "flex items-center gap-2 text-text-link hover:underline font-medium",
|
|
162
167
|
children: [/* @__PURE__ */ p("svg", {
|
|
163
168
|
className: "size-4",
|
|
@@ -171,17 +176,17 @@ function x() {
|
|
|
171
176
|
strokeWidth: 2,
|
|
172
177
|
d: "M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
173
178
|
})
|
|
174
|
-
}),
|
|
179
|
+
}), T("auth.userDetails.backToUsers", "Back to Users")]
|
|
175
180
|
}), /* @__PURE__ */ p("button", {
|
|
176
181
|
type: "button",
|
|
177
|
-
onClick: () => void
|
|
178
|
-
disabled:
|
|
179
|
-
"aria-label":
|
|
182
|
+
onClick: () => void Q(),
|
|
183
|
+
disabled: G,
|
|
184
|
+
"aria-label": T("auth.userDetails.exportAriaLabel", "Export user data"),
|
|
180
185
|
className: "flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium",
|
|
181
|
-
children:
|
|
186
|
+
children: G ? /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p(n, {
|
|
182
187
|
className: "size-4 animate-spin",
|
|
183
188
|
"aria-hidden": "true"
|
|
184
|
-
}),
|
|
189
|
+
}), T("auth.userDetails.exporting", "Exporting...")] }) : /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p("svg", {
|
|
185
190
|
className: "size-4",
|
|
186
191
|
fill: "none",
|
|
187
192
|
viewBox: "0 0 24 24",
|
|
@@ -193,14 +198,14 @@ function x() {
|
|
|
193
198
|
strokeWidth: 2,
|
|
194
199
|
d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
195
200
|
})
|
|
196
|
-
}),
|
|
201
|
+
}), T("auth.userDetails.exportButton", "Export User Data")] })
|
|
197
202
|
})]
|
|
198
203
|
}),
|
|
199
|
-
/* @__PURE__ */ p(
|
|
204
|
+
/* @__PURE__ */ p(y, {
|
|
200
205
|
className: "mb-6",
|
|
201
|
-
children:
|
|
206
|
+
children: T("auth.userDetails.purpose", "The full record for this person — their role, status, verification, and activity. From here you can export their data, deactivate them, or (for super admins) permanently delete a removed account.")
|
|
202
207
|
}),
|
|
203
|
-
|
|
208
|
+
K && /* @__PURE__ */ m("div", {
|
|
204
209
|
role: "alert",
|
|
205
210
|
className: "mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border",
|
|
206
211
|
children: [/* @__PURE__ */ p(t, {
|
|
@@ -208,50 +213,50 @@ function x() {
|
|
|
208
213
|
"aria-hidden": "true"
|
|
209
214
|
}), /* @__PURE__ */ p("p", {
|
|
210
215
|
className: "text-sm text-status-error-text",
|
|
211
|
-
children:
|
|
216
|
+
children: K
|
|
212
217
|
})]
|
|
213
218
|
}),
|
|
214
|
-
|
|
219
|
+
B && /* @__PURE__ */ m("div", {
|
|
215
220
|
className: "mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border",
|
|
216
221
|
children: [/* @__PURE__ */ p("p", {
|
|
217
222
|
className: "text-status-success-text",
|
|
218
|
-
children:
|
|
223
|
+
children: B
|
|
219
224
|
}), /* @__PURE__ */ p("button", {
|
|
220
225
|
type: "button",
|
|
221
|
-
onClick: () =>
|
|
226
|
+
onClick: () => V(null),
|
|
222
227
|
className: "mt-2 text-sm text-status-success-text hover:underline",
|
|
223
|
-
children:
|
|
228
|
+
children: T("auth.userDetails.dismiss", "Dismiss")
|
|
224
229
|
})]
|
|
225
230
|
}),
|
|
226
231
|
/* @__PURE__ */ m("div", {
|
|
227
232
|
className: "p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm",
|
|
228
233
|
children: [
|
|
229
|
-
/* @__PURE__ */ p(
|
|
234
|
+
/* @__PURE__ */ p(g, {
|
|
230
235
|
className: "mb-8",
|
|
231
236
|
children: /* @__PURE__ */ m("div", {
|
|
232
237
|
className: "flex items-center gap-4",
|
|
233
238
|
children: [/* @__PURE__ */ p("div", {
|
|
234
239
|
className: "auth-brand-badge flex size-20 items-center justify-center rounded-full text-3xl font-semibold",
|
|
235
240
|
"aria-hidden": "true",
|
|
236
|
-
children: (
|
|
241
|
+
children: (P.name || P.username || P.email).charAt(0).toUpperCase()
|
|
237
242
|
}), /* @__PURE__ */ m("div", { children: [
|
|
238
243
|
/* @__PURE__ */ p("h1", {
|
|
239
244
|
className: "text-2xl font-semibold text-text-primary",
|
|
240
|
-
children:
|
|
245
|
+
children: P.name || P.username
|
|
241
246
|
}),
|
|
242
247
|
/* @__PURE__ */ p("p", {
|
|
243
248
|
className: "text-text-secondary",
|
|
244
249
|
"data-testid": "user-email",
|
|
245
|
-
children:
|
|
250
|
+
children: P.email
|
|
246
251
|
}),
|
|
247
252
|
/* @__PURE__ */ m("div", {
|
|
248
253
|
className: "flex items-center gap-2 mt-2",
|
|
249
254
|
children: [/* @__PURE__ */ p("span", {
|
|
250
|
-
className: `px-2.5 py-1 rounded-full text-xs font-medium ${
|
|
251
|
-
children:
|
|
255
|
+
className: `px-2.5 py-1 rounded-full text-xs font-medium ${b(P.role)}`,
|
|
256
|
+
children: P.role.replace("_", " ")
|
|
252
257
|
}), /* @__PURE__ */ p("span", {
|
|
253
|
-
className: `px-2.5 py-1 rounded-full text-xs font-medium ${
|
|
254
|
-
children:
|
|
258
|
+
className: `px-2.5 py-1 rounded-full text-xs font-medium ${x(P.status)}`,
|
|
259
|
+
children: P.status
|
|
255
260
|
})]
|
|
256
261
|
})
|
|
257
262
|
] })]
|
|
@@ -264,35 +269,35 @@ function x() {
|
|
|
264
269
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
265
270
|
children: [/* @__PURE__ */ p("dt", {
|
|
266
271
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
267
|
-
children:
|
|
272
|
+
children: T("auth.userDetails.username", "Username")
|
|
268
273
|
}), /* @__PURE__ */ p("dd", {
|
|
269
274
|
className: "text-text-primary font-medium",
|
|
270
|
-
children:
|
|
275
|
+
children: P.username
|
|
271
276
|
})]
|
|
272
277
|
}),
|
|
273
278
|
/* @__PURE__ */ m("div", {
|
|
274
279
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
275
280
|
children: [/* @__PURE__ */ p("dt", {
|
|
276
281
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
277
|
-
children:
|
|
282
|
+
children: T("auth.userDetails.email", "Email")
|
|
278
283
|
}), /* @__PURE__ */ p("dd", {
|
|
279
284
|
className: "text-text-primary font-medium",
|
|
280
|
-
children:
|
|
285
|
+
children: P.email
|
|
281
286
|
})]
|
|
282
287
|
}),
|
|
283
288
|
/* @__PURE__ */ m("div", {
|
|
284
289
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
285
290
|
children: [/* @__PURE__ */ p("dt", {
|
|
286
291
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
287
|
-
children:
|
|
292
|
+
children: T("auth.userDetails.emailVerified", "Email Verified")
|
|
288
293
|
}), /* @__PURE__ */ p("dd", {
|
|
289
294
|
className: "text-text-primary font-medium",
|
|
290
|
-
children:
|
|
295
|
+
children: P.emailVerified ? /* @__PURE__ */ p("span", {
|
|
291
296
|
className: "text-status-success-text",
|
|
292
|
-
children:
|
|
297
|
+
children: T("auth.userDetails.yes", "Yes")
|
|
293
298
|
}) : /* @__PURE__ */ p("span", {
|
|
294
299
|
className: "text-status-warning-text",
|
|
295
|
-
children:
|
|
300
|
+
children: T("auth.userDetails.no", "No")
|
|
296
301
|
})
|
|
297
302
|
})]
|
|
298
303
|
}),
|
|
@@ -300,109 +305,119 @@ function x() {
|
|
|
300
305
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
301
306
|
children: [/* @__PURE__ */ p("dt", {
|
|
302
307
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
303
|
-
children:
|
|
308
|
+
children: T("auth.userDetails.mfaEnabled", "MFA Enabled")
|
|
304
309
|
}), /* @__PURE__ */ p("dd", {
|
|
305
310
|
className: "text-text-primary font-medium",
|
|
306
|
-
children:
|
|
311
|
+
children: P.mfaEnabled ? /* @__PURE__ */ p("span", {
|
|
307
312
|
className: "text-status-success-text",
|
|
308
|
-
children:
|
|
313
|
+
children: T("auth.userDetails.yes", "Yes")
|
|
309
314
|
}) : /* @__PURE__ */ p("span", {
|
|
310
315
|
className: "text-text-secondary",
|
|
311
|
-
children:
|
|
316
|
+
children: T("auth.userDetails.no", "No")
|
|
312
317
|
})
|
|
313
318
|
})]
|
|
314
319
|
}),
|
|
315
|
-
|
|
320
|
+
P.phoneNumber && /* @__PURE__ */ m("div", {
|
|
316
321
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
317
322
|
children: [/* @__PURE__ */ p("dt", {
|
|
318
323
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
319
|
-
children:
|
|
324
|
+
children: T("auth.userDetails.phoneNumber", "Phone Number")
|
|
320
325
|
}), /* @__PURE__ */ p("dd", {
|
|
321
326
|
className: "text-text-primary font-medium",
|
|
322
|
-
children:
|
|
327
|
+
children: P.phoneNumber
|
|
323
328
|
})]
|
|
324
329
|
}),
|
|
325
330
|
/* @__PURE__ */ m("div", {
|
|
326
331
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
327
332
|
children: [/* @__PURE__ */ p("dt", {
|
|
328
333
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
329
|
-
children:
|
|
334
|
+
children: T("auth.userDetails.lastLogin", "Last Login")
|
|
330
335
|
}), /* @__PURE__ */ p("dd", {
|
|
331
336
|
className: "text-text-primary font-medium",
|
|
332
|
-
children:
|
|
337
|
+
children: P.lastLoginAt ? new Date(P.lastLoginAt).toLocaleString() : T("auth.userDetails.never", "Never")
|
|
333
338
|
})]
|
|
334
339
|
}),
|
|
335
340
|
/* @__PURE__ */ m("div", {
|
|
336
341
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
337
342
|
children: [/* @__PURE__ */ p("dt", {
|
|
338
343
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
339
|
-
children:
|
|
344
|
+
children: T("auth.userDetails.created", "Created")
|
|
340
345
|
}), /* @__PURE__ */ p("dd", {
|
|
341
346
|
className: "text-text-primary font-medium",
|
|
342
|
-
children: new Date(
|
|
347
|
+
children: new Date(P.createdAt).toLocaleString()
|
|
343
348
|
})]
|
|
344
349
|
}),
|
|
345
350
|
/* @__PURE__ */ m("div", {
|
|
346
351
|
className: "p-4 rounded-lg bg-bg-canvas/50",
|
|
347
352
|
children: [/* @__PURE__ */ p("dt", {
|
|
348
353
|
className: "text-sm font-medium text-text-secondary mb-1",
|
|
349
|
-
children:
|
|
354
|
+
children: T("auth.userDetails.lastUpdated", "Last Updated")
|
|
350
355
|
}), /* @__PURE__ */ p("dd", {
|
|
351
356
|
className: "text-text-primary font-medium",
|
|
352
|
-
children: new Date(
|
|
357
|
+
children: new Date(P.updatedAt).toLocaleString()
|
|
353
358
|
})]
|
|
354
359
|
})
|
|
355
360
|
]
|
|
356
361
|
}),
|
|
357
362
|
/* @__PURE__ */ m("div", {
|
|
358
363
|
className: "mt-8 pt-6 border-t border-border-subtle flex gap-3",
|
|
359
|
-
children: [
|
|
364
|
+
children: [P.status !== "DELETED" && k && /* @__PURE__ */ p("button", {
|
|
360
365
|
type: "button",
|
|
361
|
-
onClick: () =>
|
|
366
|
+
onClick: () => U("deactivate"),
|
|
362
367
|
className: "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",
|
|
363
|
-
children:
|
|
364
|
-
}),
|
|
368
|
+
children: T("auth.userDetails.deactivateUser", "Deactivate User")
|
|
369
|
+
}), k && P.status === "DELETED" && D?.role === "SUPER_ADMIN" && /* @__PURE__ */ p("button", {
|
|
365
370
|
type: "button",
|
|
366
|
-
onClick: () =>
|
|
371
|
+
onClick: () => U("hard-delete"),
|
|
367
372
|
className: "px-4 py-2.5 rounded-xl bg-status-error-bg text-white font-medium hover:bg-status-error-bg-hover transition-colors",
|
|
368
|
-
children:
|
|
373
|
+
children: T("auth.userDetails.permanentlyDelete", "Permanently Delete")
|
|
369
374
|
})]
|
|
370
375
|
}),
|
|
371
|
-
/* @__PURE__ */ p(
|
|
376
|
+
/* @__PURE__ */ p(v, {
|
|
372
377
|
className: "mt-8",
|
|
373
378
|
storageKey: "auth-user-details",
|
|
374
379
|
steps: [{
|
|
375
380
|
id: "back-to-users",
|
|
376
|
-
label:
|
|
377
|
-
description:
|
|
378
|
-
onClick: () =>
|
|
381
|
+
label: T("auth.userDetails.nextBack", "Return to the user list"),
|
|
382
|
+
description: T("auth.userDetails.nextBackDesc", "Browse or search everyone in your organization."),
|
|
383
|
+
onClick: () => C("/auth/users")
|
|
379
384
|
}, {
|
|
380
385
|
id: "export-user",
|
|
381
|
-
label:
|
|
382
|
-
description:
|
|
383
|
-
onClick: () => void
|
|
386
|
+
label: T("auth.userDetails.nextExport", "Export this user's data"),
|
|
387
|
+
description: T("auth.userDetails.nextExportDesc", "Download their profile, sessions, and audit history as CSV."),
|
|
388
|
+
onClick: () => void Q()
|
|
384
389
|
}]
|
|
390
|
+
}),
|
|
391
|
+
/* @__PURE__ */ p(a, {
|
|
392
|
+
open: J,
|
|
393
|
+
onOpenChange: Y,
|
|
394
|
+
title: T("auth.userDetails.exportQuotaTitle", "Export Limit Reached"),
|
|
395
|
+
description: T("auth.userDetails.exportQuotaDescription", "You have reached your export limit for this month."),
|
|
396
|
+
confirmText: T("auth.userDetails.exportQuotaViewBilling", "View Billing"),
|
|
397
|
+
cancelText: T("auth.userDetails.exportQuotaDismiss", "Dismiss"),
|
|
398
|
+
variant: "warning",
|
|
399
|
+
onConfirm: () => window.location.assign("/billing")
|
|
385
400
|
})
|
|
386
401
|
]
|
|
387
402
|
})
|
|
388
403
|
]
|
|
389
404
|
}) : /* @__PURE__ */ p("div", {
|
|
390
405
|
className: "max-w-4xl mx-auto p-6",
|
|
391
|
-
children: /* @__PURE__ */ p(
|
|
406
|
+
children: /* @__PURE__ */ p(_, {
|
|
392
407
|
illustration: "empty-generic",
|
|
393
|
-
title:
|
|
394
|
-
description:
|
|
408
|
+
title: T("auth.userDetails.adminRequired", "Admin Access Required"),
|
|
409
|
+
description: T("auth.userDetails.adminRequiredDesc", "You need administrator privileges to view user details.")
|
|
395
410
|
})
|
|
396
411
|
}) : /* @__PURE__ */ p("div", {
|
|
397
412
|
className: "max-w-4xl mx-auto p-6",
|
|
398
|
-
children: /* @__PURE__ */ p(
|
|
413
|
+
children: /* @__PURE__ */ p(_, {
|
|
399
414
|
illustration: "empty-generic",
|
|
400
|
-
title:
|
|
401
|
-
description:
|
|
415
|
+
title: T("auth.userDetails.authRequired", "Authentication Required"),
|
|
416
|
+
description: T("auth.userDetails.signInToView", "Please sign in to view user details")
|
|
402
417
|
})
|
|
403
418
|
});
|
|
404
419
|
}
|
|
405
420
|
//#endregion
|
|
406
|
-
export {
|
|
421
|
+
export { S as UserDetailsPage };
|
|
407
422
|
|
|
408
423
|
//# sourceMappingURL=UserDetailsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserDetailsPage.js","names":[],"sources":["../../../src/auth/pages/UserDetailsPage.tsx"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\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 { useAuthPermissions } from \"../hooks/useAuthPermissions\";\nimport { useUserOperations } from \"../hooks/useUserOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\nimport type { User, UserRole, UserStatus } from \"../types\";\n\nconst getRoleColor = (role: UserRole) => {\n switch (role) {\n case \"SUPER_ADMIN\":\n return \"bg-accent-purple-subtle text-accent-purple\";\n case \"TENANT_ADMIN\":\n return \"bg-status-info-bg-subtle text-status-info-text\";\n case \"ORG_ADMIN\":\n return \"bg-accent-indigo-subtle text-accent-indigo\";\n case \"WORKSPACE_ADMIN\":\n return \"bg-accent-cyan-subtle text-accent-cyan\";\n case \"MEMBER\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"GUEST\":\n return \"bg-bg-sunken text-text-primary\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n};\n\nconst getStatusColor = (status: UserStatus) => {\n switch (status) {\n case \"ACTIVE\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"INACTIVE\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"SUSPENDED\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"DELETED\":\n return \"bg-status-error-bg-subtle text-status-error-text\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n};\n\n/**\n * User details page (admin only).\n *\n * Route: /auth/users/:id\n *\n * Renders the full profile of a single user, with admin actions\n * (edit, deactivate, hard-delete, export). Reuses the same getUser query\n * that UsersPage relies on.\n */\nexport function UserDetailsPage() {\n const { id } = useParams<{ id: string }>();\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 currentUser = useAuthStore((state) => state.user);\n const {\n canViewUsers,\n canDeleteUsers,\n isLoading: permissionsLoading,\n } = useAuthPermissions();\n const { getUser, deleteUser, hardDeleteUser } = useUserOperations();\n\n const [user, setUser] = useState<User | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingAction, setPendingAction] = useState<\n \"deactivate\" | \"hard-delete\" | null\n >(null);\n\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation();\n const [exportError, setExportError] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n if (!id) {\n setError(\"Missing user id\");\n setLoading(false);\n return;\n }\n if (!isAuthenticated) {\n setLoading(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n getUser(id)\n .then((fetched) => {\n if (cancelled) return;\n if (!fetched) {\n setError(tr(\"auth.userDetails.userNotFound\", \"User not found\"));\n } else {\n setUser(fetched);\n }\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n setError(\n err instanceof Error ? err.message : \"Failed to fetch user details\",\n );\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n // tr is stable within the render, id/getUser/isAuthenticated drive the effect\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [id, getUser, isAuthenticated]);\n\n const handleDeactivate = async () => {\n if (!user) return;\n try {\n const result = await deleteUser(user.id);\n if (result.success) {\n setSuccessMessage(\n tr(\n \"auth.userDetails.userDeactivated\",\n 'User \"{email}\" has been deactivated.',\n ).replace(\"{email}\", user.email),\n );\n setPendingAction(null);\n // Refresh\n const fresh = await getUser(user.id);\n if (fresh) setUser(fresh);\n } else {\n setError(result.message);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to delete user\");\n }\n };\n\n const handleHardDelete = async () => {\n if (!user) return;\n try {\n const result = await hardDeleteUser(user.id);\n if (result.success) {\n setPendingAction(null);\n navigate(\"/auth/users\");\n } else {\n setError(result.message);\n }\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to hard delete user\",\n );\n }\n };\n\n const handleExport = async () => {\n if (!user) return;\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: `User Data Export - ${user.email}`,\n entityTypes: [\n \"user_profile\",\n \"user_sessions\",\n \"permission_audit_logs\",\n ],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n // Auth + role guards\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.userDetails.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.userDetails.signInToView\",\n \"Please sign in to view user details\",\n )}\n />\n </div>\n );\n }\n\n if (permissionsLoading || loading) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <div className=\"text-center py-16\" aria-live=\"polite\" aria-busy=\"true\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-secondary animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-4 text-text-secondary\">\n {tr(\"auth.userDetails.loading\", \"Loading user...\")}\n </p>\n </div>\n </div>\n );\n }\n\n if (!canViewUsers) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.userDetails.adminRequired\", \"Admin Access Required\")}\n description={tr(\n \"auth.userDetails.adminRequiredDesc\",\n \"You need administrator privileges to view user details.\",\n )}\n />\n </div>\n );\n }\n\n if (error || !user) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/users\")}\n className=\"mb-6 flex items-center gap-2 text-text-link hover:underline font-medium\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.userDetails.backToUsers\", \"Back to Users\")}\n </button>\n <div role=\"alert\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={tr(\"auth.userDetails.userNotFound\", \"User not found\")}\n description={\n error ??\n tr(\n \"auth.userDetails.userNotFoundDesc\",\n \"We couldn't find this user. They may have been removed, or the link is out of date.\",\n )\n }\n />\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <ConfirmActionDialog\n open={pendingAction !== null}\n onOpenChange={(open) => {\n if (!open) setPendingAction(null);\n }}\n title={\n pendingAction === \"hard-delete\"\n ? tr(\"auth.userDetails.hardDeleteTitle\", \"Permanently delete user?\")\n : tr(\"auth.userDetails.deactivateTitle\", \"Deactivate user?\")\n }\n description={\n pendingAction === \"hard-delete\"\n ? tr(\n \"auth.userDetails.hardDeleteDesc\",\n 'This permanently deletes \"{email}\" and cannot be undone.',\n ).replace(\"{email}\", user.email)\n : tr(\n \"auth.userDetails.deactivateDesc\",\n '\"{email}\" will be prevented from signing in until reactivated.',\n ).replace(\"{email}\", user.email)\n }\n confirmText={\n pendingAction === \"hard-delete\"\n ? tr(\"auth.userDetails.hardDeleteConfirm\", \"Delete permanently\")\n : tr(\"auth.userDetails.deactivateConfirm\", \"Deactivate user\")\n }\n variant={pendingAction === \"hard-delete\" ? \"danger\" : \"warning\"}\n onConfirm={async () => {\n if (pendingAction === \"hard-delete\") await handleHardDelete();\n else if (pendingAction === \"deactivate\") await handleDeactivate();\n }}\n />\n\n {/* Header row */}\n <div className=\"flex items-center justify-between mb-6\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/users\")}\n className=\"flex items-center gap-2 text-text-link hover:underline font-medium\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.userDetails.backToUsers\", \"Back to Users\")}\n </button>\n\n {/* Export button */}\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\n \"auth.userDetails.exportAriaLabel\",\n \"Export user data\",\n )}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.userDetails.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.userDetails.exportButton\", \"Export User Data\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.userDetails.purpose\",\n \"The full record for this person — their role, status, verification, and activity. From here you can export their data, deactivate them, or (for super admins) permanently delete a removed account.\",\n )}\n </PagePurpose>\n\n {/* Export error */}\n {exportError && (\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 <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n {successMessage && (\n <div className=\"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\"\n >\n {tr(\"auth.userDetails.dismiss\", \"Dismiss\")}\n </button>\n </div>\n )}\n\n <div className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n {/* Identity header — the one emphasis zone on this page */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"flex items-center gap-4\">\n <div\n className=\"auth-brand-badge flex size-20 items-center justify-center rounded-full text-3xl font-semibold\"\n aria-hidden=\"true\"\n >\n {(user.name || user.username || user.email)\n .charAt(0)\n .toUpperCase()}\n </div>\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">\n {user.name || user.username}\n </h1>\n <p className=\"text-text-secondary\" data-testid=\"user-email\">\n {user.email}\n </p>\n <div className=\"flex items-center gap-2 mt-2\">\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getRoleColor(user.role)}`}\n >\n {user.role.replace(\"_\", \" \")}\n </span>\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getStatusColor(user.status)}`}\n >\n {user.status}\n </span>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <dl className=\"grid grid-cols-2 gap-6\">\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.username\", \"Username\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">{user.username}</dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.email\", \"Email\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">{user.email}</dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.emailVerified\", \"Email Verified\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.emailVerified ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.userDetails.yes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-status-warning-text\">\n {tr(\"auth.userDetails.no\", \"No\")}\n </span>\n )}\n </dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.mfaEnabled\", \"MFA Enabled\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.mfaEnabled ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.userDetails.yes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-text-secondary\">\n {tr(\"auth.userDetails.no\", \"No\")}\n </span>\n )}\n </dd>\n </div>\n {user.phoneNumber && (\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.phoneNumber\", \"Phone Number\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.phoneNumber}\n </dd>\n </div>\n )}\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.lastLogin\", \"Last Login\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.lastLoginAt\n ? new Date(user.lastLoginAt).toLocaleString()\n : tr(\"auth.userDetails.never\", \"Never\")}\n </dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.created\", \"Created\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {new Date(user.createdAt).toLocaleString()}\n </dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.lastUpdated\", \"Last Updated\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {new Date(user.updatedAt).toLocaleString()}\n </dd>\n </div>\n </dl>\n\n <div className=\"mt-8 pt-6 border-t border-border-subtle flex gap-3\">\n {user.status !== \"DELETED\" && canDeleteUsers && (\n <button\n type=\"button\"\n onClick={() => setPendingAction(\"deactivate\")}\n className=\"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\"\n >\n {tr(\"auth.userDetails.deactivateUser\", \"Deactivate User\")}\n </button>\n )}\n {canDeleteUsers &&\n user.status === \"DELETED\" &&\n currentUser?.role === \"SUPER_ADMIN\" && (\n <button\n type=\"button\"\n onClick={() => setPendingAction(\"hard-delete\")}\n className=\"px-4 py-2.5 rounded-xl bg-status-error-bg text-white font-medium hover:bg-status-error-bg-hover transition-colors\"\n >\n {tr(\"auth.userDetails.permanentlyDelete\", \"Permanently Delete\")}\n </button>\n )}\n </div>\n\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-user-details\"\n steps={[\n {\n id: \"back-to-users\",\n label: tr(\"auth.userDetails.nextBack\", \"Return to the user list\"),\n description: tr(\n \"auth.userDetails.nextBackDesc\",\n \"Browse or search everyone in your organization.\",\n ),\n onClick: () => navigate(\"/auth/users\"),\n },\n {\n id: \"export-user\",\n label: tr(\n \"auth.userDetails.nextExport\",\n \"Export this user's data\",\n ),\n description: tr(\n \"auth.userDetails.nextExportDesc\",\n \"Download their profile, sessions, and audit history as CSV.\",\n ),\n onClick: () => void handleExport(),\n },\n ]}\n />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,IAAM,KAAgB,MAAmB;AACvC,SAAQ,GAAR;EACE,KAAK,cACH,QAAO;EACT,KAAK,eACH,QAAO;EACT,KAAK,YACH,QAAO;EACT,KAAK,kBACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,QACH,QAAO;EACT,QACE,QAAO;;GAIP,KAAkB,MAAuB;AAC7C,SAAQ,GAAR;EACE,KAAK,SACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,KAAK,YACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,QACE,QAAO;;;AAab,SAAgB,IAAkB;CAChC,IAAM,EAAE,UAAO,GAA2B,EACpC,IAAW,GAAa,EACxB,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAa,EAAE,EAAI;AACzB,SAAO,MAAe,IAAM,IAAW;IAGnC,IAAkB,GAAc,MAAU,EAAM,gBAAgB,EAChE,IAAc,GAAc,MAAU,EAAM,KAAK,EACjD,EACJ,iBACA,mBACA,WAAW,MACT,GAAoB,EAClB,EAAE,YAAS,eAAY,sBAAmB,GAAmB,EAE7D,CAAC,GAAM,KAAW,EAAsB,KAAK,EAC7C,CAAC,GAAS,KAAc,EAAS,GAAK,EACtC,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAgB,KAAqB,EAAwB,KAAK,EACnE,CAAC,GAAe,KAAoB,EAExC,KAAK,EAED,CAAC,GAAiB,EAAE,SAAS,OACjC,GAAgC,EAC5B,CAAC,GAAa,KAAkB,EAAwB,KAAK;AAEnE,SAAgB;EACd,IAAI,IAAY;AAChB,MAAI,CAAC,GAAI;AAEP,GADA,EAAS,kBAAkB,EAC3B,EAAW,GAAM;AACjB;;AAEF,MAAI,CAAC,GAAiB;AACpB,KAAW,GAAM;AACjB;;AAwBF,SArBA,EAAW,GAAK,EAChB,EAAS,KAAK,EACd,EAAQ,EAAG,CACR,MAAM,MAAY;AACb,SACC,IAGH,EAAQ,EAAQ,GAFhB,EAAS,EAAG,iCAAiC,iBAAiB,CAAC;IAIjE,CACD,OAAO,MAAiB;AACnB,QACJ,EACE,aAAe,QAAQ,EAAI,UAAU,+BACtC;IACD,CACD,cAAc;AACb,GAAK,KAAW,EAAW,GAAM;IACjC,QAES;AACX,OAAY;;IAIb;EAAC;EAAI;EAAS;EAAgB,CAAC;CAElC,IAAM,IAAmB,YAAY;AAC9B,QACL,KAAI;GACF,IAAM,IAAS,MAAM,EAAW,EAAK,GAAG;AACxC,OAAI,EAAO,SAAS;AAOlB,IANA,EACE,EACE,oCACA,yCACD,CAAC,QAAQ,WAAW,EAAK,MAAM,CACjC,EACD,EAAiB,KAAK;IAEtB,IAAM,IAAQ,MAAM,EAAQ,EAAK,GAAG;AACpC,IAAI,KAAO,EAAQ,EAAM;SAEzB,GAAS,EAAO,QAAQ;WAEnB,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;;IAIpE,IAAmB,YAAY;AAC9B,QACL,KAAI;GACF,IAAM,IAAS,MAAM,EAAe,EAAK,GAAG;AAC5C,GAAI,EAAO,WACT,EAAiB,KAAK,EACtB,EAAS,cAAc,IAEvB,EAAS,EAAO,QAAQ;WAEnB,GAAK;AACZ,KACE,aAAe,QAAQ,EAAI,UAAU,6BACtC;;IAIC,IAAe,YAAY;AAC1B,SACL;KAAe,KAAK;AACpB,OAAI;AACF,UAAM,EAAgB,EACpB,WAAW,EACT,OAAO;KACL,MAAM,sBAAsB,EAAK;KACjC,aAAa;MACX;MACA;MACA;MACD;KACD,QAAQ;KACR,aAAa;KACb,iBAAiB;KAClB,EACF,EACF,CAAC;YACK,GAAK;AACZ,MACE,aAAe,QAAQ,EAAI,UAAU,yBACtC;;;;AA4FL,QAvFK,IAeD,KAAsB,IAEtB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;GAAoB,aAAU;GAAS,aAAU;aAAhE,CACE,kBAAC,GAAD;IACE,WAAU;IACV,eAAY;IACZ,CAAA,EACF,kBAAC,KAAD;IAAG,WAAU;cACV,EAAG,4BAA4B,kBAAkB;IAChD,CAAA,CACA;;EACF,CAAA,GAIL,IAeD,KAAS,CAAC,IAEV,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAS,cAAc;GACtC,WAAU;aAHZ,CAKE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,SAAQ;IACR,QAAO;IACP,eAAY;cAEZ,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACL,EAAG,gCAAgC,gBAAgB,CAC7C;MACT,kBAAC,OAAD;GAAK,MAAK;aACR,kBAAC,GAAD;IACE,cAAa;IACb,OAAO,EAAG,iCAAiC,iBAAiB;IAC5D,aACE,KACA,EACE,qCACA,sFACD;IAEH,CAAA;GACE,CAAA,CACF;MAKR,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,MAAM,MAAkB;IACxB,eAAe,MAAS;AACtB,KAAK,KAAM,EAAiB,KAAK;;IAEnC,OACE,MAAkB,gBACd,EAAG,oCAAoC,2BAA2B,GAClE,EAAG,oCAAoC,mBAAmB;IAEhE,aACE,MAAkB,gBACd,EACE,mCACA,6DACD,CAAC,QAAQ,WAAW,EAAK,MAAM,GAChC,EACE,mCACA,mEACD,CAAC,QAAQ,WAAW,EAAK,MAAM;IAEtC,aACE,MAAkB,gBACd,EAAG,sCAAsC,qBAAqB,GAC9D,EAAG,sCAAsC,kBAAkB;IAEjE,SAAS,MAAkB,gBAAgB,WAAW;IACtD,WAAW,YAAY;AACrB,KAAI,MAAkB,gBAAe,MAAM,GAAkB,GACpD,MAAkB,gBAAc,MAAM,GAAkB;;IAEnE,CAAA;GAGF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,cAAc;KACtC,WAAU;eAHZ,CAKE,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;MACP,eAAY;gBAEZ,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA,EACL,EAAG,gCAAgC,gBAAgB,CAC7C;QAGT,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,KAAK,GAAc;KAClC,UAAU;KACV,cAAY,EACV,oCACA,mBACD;KACD,WAAU;eAET,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAa,WAAU;MAAsB,eAAY;MAAS,CAAA,EACjE,EAAG,8BAA8B,eAAe,CAChD,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,SAAQ;MACR,QAAO;MACP,eAAY;gBAEZ,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA;MACE,CAAA,EACL,EAAG,iCAAiC,mBAAmB,CACvD,EAAA,CAAA;KAEE,CAAA,CACL;;GAEN,kBAAC,GAAD;IAAa,WAAU;cACpB,EACC,4BACA,sMACD;IACW,CAAA;GAGb,KACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAFZ,CAIE,kBAAC,GAAD;KACE,WAAU;KACV,eAAY;KACZ,CAAA,EACF,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAgB,CAAA,CAC3D;;GAGP,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAA4B;KAAmB,CAAA,EAC5D,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAkB,KAAK;KACtC,WAAU;eAET,EAAG,4BAA4B,UAAU;KACnC,CAAA,CACL;;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEE,kBAAC,GAAD;MAAe,WAAU;gBACvB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QACE,WAAU;QACV,eAAY;mBAEV,EAAK,QAAQ,EAAK,YAAY,EAAK,OAClC,OAAO,EAAE,CACT,aAAa;QACZ,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAK,QAAQ,EAAK;SAChB,CAAA;QACL,kBAAC,KAAD;SAAG,WAAU;SAAsB,eAAY;mBAC5C,EAAK;SACJ,CAAA;QACJ,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UACE,WAAW,gDAAgD,EAAa,EAAK,KAAK;oBAEjF,EAAK,KAAK,QAAQ,KAAK,IAAI;UACvB,CAAA,EACP,kBAAC,QAAD;UACE,WAAW,gDAAgD,EAAe,EAAK,OAAO;oBAErF,EAAK;UACD,CAAA,CACH;;QACF,EAAA,CAAA,CACF;;MACQ,CAAA;KAEhB,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,6BAA6B,WAAW;SACzC,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBAAiC,EAAK;SAAc,CAAA,CAC9D;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,0BAA0B,QAAQ;SACnC,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBAAiC,EAAK;SAAW,CAAA,CAC3D;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,kCAAkC,iBAAiB;SACpD,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAK,gBACJ,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAG,wBAAwB,MAAM;UAC7B,CAAA,GAEP,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAG,uBAAuB,KAAK;UAC3B,CAAA;SAEN,CAAA,CACD;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,+BAA+B,cAAc;SAC9C,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAK,aACJ,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAG,wBAAwB,MAAM;UAC7B,CAAA,GAEP,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAG,uBAAuB,KAAK;UAC3B,CAAA;SAEN,CAAA,CACD;;OACL,EAAK,eACJ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,gCAAgC,eAAe;SAChD,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAK;SACH,CAAA,CACD;;OAER,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,8BAA8B,aAAa;SAC5C,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAK,cACF,IAAI,KAAK,EAAK,YAAY,CAAC,gBAAgB,GAC3C,EAAG,0BAA0B,QAAQ;SACtC,CAAA,CACD;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,4BAA4B,UAAU;SACvC,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBACX,IAAI,KAAK,EAAK,UAAU,CAAC,gBAAgB;SACvC,CAAA,CACD;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,gCAAgC,eAAe;SAChD,CAAA,EACL,kBAAC,MAAD;SAAI,WAAU;mBACX,IAAI,KAAK,EAAK,UAAU,CAAC,gBAAgB;SACvC,CAAA,CACD;;OACH;;KAEL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAK,WAAW,aAAa,KAC5B,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAiB,aAAa;OAC7C,WAAU;iBAET,EAAG,mCAAmC,kBAAkB;OAClD,CAAA,EAEV,KACC,EAAK,WAAW,aAChB,GAAa,SAAS,iBACpB,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAiB,cAAc;OAC9C,WAAU;iBAET,EAAG,sCAAsC,qBAAqB;OACxD,CAAA,CAET;;KAEN,kBAAC,GAAD;MACE,WAAU;MACV,YAAW;MACX,OAAO,CACL;OACE,IAAI;OACJ,OAAO,EAAG,6BAA6B,0BAA0B;OACjE,aAAa,EACX,iCACA,kDACD;OACD,eAAe,EAAS,cAAc;OACvC,EACD;OACE,IAAI;OACJ,OAAO,EACL,+BACA,0BACD;OACD,aAAa,EACX,mCACA,8DACD;OACD,eAAe,KAAK,GAAc;OACnC,CACF;MACD,CAAA;KACE;;GACF;MAlWJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAG,kCAAkC,wBAAwB;GACpE,aAAa,EACX,sCACA,0DACD;GACD,CAAA;EACE,CAAA,GAxCN,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAG,iCAAiC,0BAA0B;GACrE,aAAa,EACX,iCACA,sCACD;GACD,CAAA;EACE,CAAA"}
|
|
1
|
+
{"version":3,"file":"UserDetailsPage.js","names":[],"sources":["../../../src/auth/pages/UserDetailsPage.tsx"],"sourcesContent":["import { useState, useEffect } from \"react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n NextSteps,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useAuthPermissions } from \"../hooks/useAuthPermissions\";\nimport { useUserOperations } from \"../hooks/useUserOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\nimport type { User, UserRole, UserStatus } from \"../types\";\n\nconst getRoleColor = (role: UserRole) => {\n switch (role) {\n case \"SUPER_ADMIN\":\n return \"bg-accent-purple-subtle text-accent-purple\";\n case \"TENANT_ADMIN\":\n return \"bg-status-info-bg-subtle text-status-info-text\";\n case \"ORG_ADMIN\":\n return \"bg-accent-indigo-subtle text-accent-indigo\";\n case \"WORKSPACE_ADMIN\":\n return \"bg-accent-cyan-subtle text-accent-cyan\";\n case \"MEMBER\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"GUEST\":\n return \"bg-bg-sunken text-text-primary\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n};\n\nconst getStatusColor = (status: UserStatus) => {\n switch (status) {\n case \"ACTIVE\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"INACTIVE\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"SUSPENDED\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"DELETED\":\n return \"bg-status-error-bg-subtle text-status-error-text\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n};\n\n/**\n * User details page (admin only).\n *\n * Route: /auth/users/:id\n *\n * Renders the full profile of a single user, with admin actions\n * (edit, deactivate, hard-delete, export). Reuses the same getUser query\n * that UsersPage relies on.\n */\nexport function UserDetailsPage() {\n const { id } = useParams<{ id: string }>();\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 currentUser = useAuthStore((state) => state.user);\n const {\n canViewUsers,\n canDeleteUsers,\n isLoading: permissionsLoading,\n } = useAuthPermissions();\n const { getUser, deleteUser, hardDeleteUser } = useUserOperations();\n\n const [user, setUser] = useState<User | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingAction, setPendingAction] = useState<\n \"deactivate\" | \"hard-delete\" | null\n >(null);\n\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation();\n const [exportError, setExportError] = useState<string | null>(null);\n const [showExportQuotaDialog, setShowExportQuotaDialog] = useState(false);\n\n useEffect(() => {\n let cancelled = false;\n if (!id) {\n setError(\"Missing user id\");\n setLoading(false);\n return;\n }\n if (!isAuthenticated) {\n setLoading(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n getUser(id)\n .then((fetched) => {\n if (cancelled) return;\n if (!fetched) {\n setError(tr(\"auth.userDetails.userNotFound\", \"User not found\"));\n } else {\n setUser(fetched);\n }\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n setError(\n err instanceof Error ? err.message : \"Failed to fetch user details\",\n );\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n // tr is stable within the render, id/getUser/isAuthenticated drive the effect\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [id, getUser, isAuthenticated]);\n\n const handleDeactivate = async () => {\n if (!user) return;\n try {\n const result = await deleteUser(user.id);\n if (result.success) {\n setSuccessMessage(\n tr(\n \"auth.userDetails.userDeactivated\",\n 'User \"{email}\" has been deactivated.',\n ).replace(\"{email}\", user.email),\n );\n setPendingAction(null);\n // Refresh\n const fresh = await getUser(user.id);\n if (fresh) setUser(fresh);\n } else {\n setError(result.message);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to delete user\");\n }\n };\n\n const handleHardDelete = async () => {\n if (!user) return;\n try {\n const result = await hardDeleteUser(user.id);\n if (result.success) {\n setPendingAction(null);\n navigate(\"/auth/users\");\n } else {\n setError(result.message);\n }\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to hard delete user\",\n );\n }\n };\n\n const handleExport = async () => {\n if (!user) return;\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: `User Data Export - ${user.email}`,\n entityTypes: [\n \"user_profile\",\n \"user_sessions\",\n \"permission_audit_logs\",\n ],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setShowExportQuotaDialog(true);\n setExportError(\n tr(\n \"auth.userDetails.exportQuotaReached\",\n \"You have reached your export limit for this month.\",\n ),\n );\n return;\n }\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n // Auth + role guards\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.userDetails.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.userDetails.signInToView\",\n \"Please sign in to view user details\",\n )}\n />\n </div>\n );\n }\n\n if (permissionsLoading || loading) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <div className=\"text-center py-16\" aria-live=\"polite\" aria-busy=\"true\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-secondary animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-4 text-text-secondary\">\n {tr(\"auth.userDetails.loading\", \"Loading user...\")}\n </p>\n </div>\n </div>\n );\n }\n\n if (!canViewUsers) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.userDetails.adminRequired\", \"Admin Access Required\")}\n description={tr(\n \"auth.userDetails.adminRequiredDesc\",\n \"You need administrator privileges to view user details.\",\n )}\n />\n </div>\n );\n }\n\n if (error || !user) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/users\")}\n className=\"mb-6 flex items-center gap-2 text-text-link hover:underline font-medium\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.userDetails.backToUsers\", \"Back to Users\")}\n </button>\n <div role=\"alert\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={tr(\"auth.userDetails.userNotFound\", \"User not found\")}\n description={\n error ??\n tr(\n \"auth.userDetails.userNotFoundDesc\",\n \"We couldn't find this user. They may have been removed, or the link is out of date.\",\n )\n }\n />\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <ConfirmActionDialog\n open={pendingAction !== null}\n onOpenChange={(open) => {\n if (!open) setPendingAction(null);\n }}\n title={\n pendingAction === \"hard-delete\"\n ? tr(\"auth.userDetails.hardDeleteTitle\", \"Permanently delete user?\")\n : tr(\"auth.userDetails.deactivateTitle\", \"Deactivate user?\")\n }\n description={\n pendingAction === \"hard-delete\"\n ? tr(\n \"auth.userDetails.hardDeleteDesc\",\n 'This permanently deletes \"{email}\" and cannot be undone.',\n ).replace(\"{email}\", user.email)\n : tr(\n \"auth.userDetails.deactivateDesc\",\n '\"{email}\" will be prevented from signing in until reactivated.',\n ).replace(\"{email}\", user.email)\n }\n confirmText={\n pendingAction === \"hard-delete\"\n ? tr(\"auth.userDetails.hardDeleteConfirm\", \"Delete permanently\")\n : tr(\"auth.userDetails.deactivateConfirm\", \"Deactivate user\")\n }\n variant={pendingAction === \"hard-delete\" ? \"danger\" : \"warning\"}\n onConfirm={async () => {\n if (pendingAction === \"hard-delete\") await handleHardDelete();\n else if (pendingAction === \"deactivate\") await handleDeactivate();\n }}\n />\n\n {/* Header row */}\n <div className=\"flex items-center justify-between mb-6\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/users\")}\n className=\"flex items-center gap-2 text-text-link hover:underline font-medium\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.userDetails.backToUsers\", \"Back to Users\")}\n </button>\n\n {/* Export button */}\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\n \"auth.userDetails.exportAriaLabel\",\n \"Export user data\",\n )}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.userDetails.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.userDetails.exportButton\", \"Export User Data\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.userDetails.purpose\",\n \"The full record for this person — their role, status, verification, and activity. From here you can export their data, deactivate them, or (for super admins) permanently delete a removed account.\",\n )}\n </PagePurpose>\n\n {/* Export error */}\n {exportError && (\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 <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n {successMessage && (\n <div className=\"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\"\n >\n {tr(\"auth.userDetails.dismiss\", \"Dismiss\")}\n </button>\n </div>\n )}\n\n <div className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n {/* Identity header — the one emphasis zone on this page */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"flex items-center gap-4\">\n <div\n className=\"auth-brand-badge flex size-20 items-center justify-center rounded-full text-3xl font-semibold\"\n aria-hidden=\"true\"\n >\n {(user.name || user.username || user.email)\n .charAt(0)\n .toUpperCase()}\n </div>\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">\n {user.name || user.username}\n </h1>\n <p className=\"text-text-secondary\" data-testid=\"user-email\">\n {user.email}\n </p>\n <div className=\"flex items-center gap-2 mt-2\">\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getRoleColor(user.role)}`}\n >\n {user.role.replace(\"_\", \" \")}\n </span>\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getStatusColor(user.status)}`}\n >\n {user.status}\n </span>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <dl className=\"grid grid-cols-2 gap-6\">\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.username\", \"Username\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">{user.username}</dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.email\", \"Email\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">{user.email}</dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.emailVerified\", \"Email Verified\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.emailVerified ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.userDetails.yes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-status-warning-text\">\n {tr(\"auth.userDetails.no\", \"No\")}\n </span>\n )}\n </dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.mfaEnabled\", \"MFA Enabled\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.mfaEnabled ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.userDetails.yes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-text-secondary\">\n {tr(\"auth.userDetails.no\", \"No\")}\n </span>\n )}\n </dd>\n </div>\n {user.phoneNumber && (\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.phoneNumber\", \"Phone Number\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.phoneNumber}\n </dd>\n </div>\n )}\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.lastLogin\", \"Last Login\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {user.lastLoginAt\n ? new Date(user.lastLoginAt).toLocaleString()\n : tr(\"auth.userDetails.never\", \"Never\")}\n </dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.created\", \"Created\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {new Date(user.createdAt).toLocaleString()}\n </dd>\n </div>\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <dt className=\"text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.userDetails.lastUpdated\", \"Last Updated\")}\n </dt>\n <dd className=\"text-text-primary font-medium\">\n {new Date(user.updatedAt).toLocaleString()}\n </dd>\n </div>\n </dl>\n\n <div className=\"mt-8 pt-6 border-t border-border-subtle flex gap-3\">\n {user.status !== \"DELETED\" && canDeleteUsers && (\n <button\n type=\"button\"\n onClick={() => setPendingAction(\"deactivate\")}\n className=\"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\"\n >\n {tr(\"auth.userDetails.deactivateUser\", \"Deactivate User\")}\n </button>\n )}\n {canDeleteUsers &&\n user.status === \"DELETED\" &&\n currentUser?.role === \"SUPER_ADMIN\" && (\n <button\n type=\"button\"\n onClick={() => setPendingAction(\"hard-delete\")}\n className=\"px-4 py-2.5 rounded-xl bg-status-error-bg text-white font-medium hover:bg-status-error-bg-hover transition-colors\"\n >\n {tr(\"auth.userDetails.permanentlyDelete\", \"Permanently Delete\")}\n </button>\n )}\n </div>\n\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-user-details\"\n steps={[\n {\n id: \"back-to-users\",\n label: tr(\"auth.userDetails.nextBack\", \"Return to the user list\"),\n description: tr(\n \"auth.userDetails.nextBackDesc\",\n \"Browse or search everyone in your organization.\",\n ),\n onClick: () => navigate(\"/auth/users\"),\n },\n {\n id: \"export-user\",\n label: tr(\n \"auth.userDetails.nextExport\",\n \"Export this user's data\",\n ),\n description: tr(\n \"auth.userDetails.nextExportDesc\",\n \"Download their profile, sessions, and audit history as CSV.\",\n ),\n onClick: () => void handleExport(),\n },\n ]}\n />\n\n <ConfirmActionDialog\n open={showExportQuotaDialog}\n onOpenChange={setShowExportQuotaDialog}\n title={tr(\"auth.userDetails.exportQuotaTitle\", \"Export Limit Reached\")}\n description={tr(\n \"auth.userDetails.exportQuotaDescription\",\n \"You have reached your export limit for this month.\",\n )}\n confirmText={tr(\"auth.userDetails.exportQuotaViewBilling\", \"View Billing\")}\n cancelText={tr(\"auth.userDetails.exportQuotaDismiss\", \"Dismiss\")}\n variant=\"warning\"\n onConfirm={() => window.location.assign(\"/billing\")}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|