@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.3 → 1.0.0-alpha.4

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.
@@ -9,7 +9,7 @@ const client$2 = require("@better-auth/infra/client");
9
9
  const client$1 = require("better-auth/client");
10
10
  const icons = require("@strapi/icons");
11
11
  const admin = require("@strapi/strapi/admin");
12
- const index = require("./index-xZ2FHX3i.js");
12
+ const index = require("./index-CIxfFlzU.js");
13
13
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
14
14
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
15
15
  const dashPathMethods = () => ({
@@ -1042,6 +1042,7 @@ function OrganizationDetail({
1042
1042
  const schemaQuery = useModelSchema("organization");
1043
1043
  const qc = reactQuery.useQueryClient();
1044
1044
  const { toggleNotification } = admin.useNotification();
1045
+ const { get, put } = admin.useFetchClient();
1045
1046
  const orgQuery = reactQuery.useQuery({
1046
1047
  queryKey: ["dash-org", organizationId],
1047
1048
  queryFn: async () => {
@@ -1088,6 +1089,16 @@ function OrganizationDetail({
1088
1089
  return result.data ?? [];
1089
1090
  }
1090
1091
  });
1092
+ const strapiOrgQuery = reactQuery.useQuery({
1093
+ queryKey: ["dash-strapi-org", organizationId],
1094
+ enabled: !!orgQuery.data,
1095
+ queryFn: async () => {
1096
+ const { data } = await get(
1097
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
1098
+ );
1099
+ return data.results?.[0] ?? null;
1100
+ }
1101
+ });
1091
1102
  const [activeTab, setActiveTab] = react.useState("details");
1092
1103
  const [editName, setEditName] = react.useState(void 0);
1093
1104
  const [editSlug, setEditSlug] = react.useState(void 0);
@@ -1117,13 +1128,13 @@ function OrganizationDetail({
1117
1128
  if (editName !== void 0) body.name = editName;
1118
1129
  if (editSlug !== void 0) body.slug = editSlug;
1119
1130
  if (editLogo !== void 0) body.logo = editLogo;
1120
- const result = await client.dash.organization.update(
1121
- body,
1122
- withContext({ organizationId })
1131
+ const documentId = strapiOrgQuery.data?.documentId;
1132
+ if (!documentId)
1133
+ throw new Error("Could not resolve documentId for organization");
1134
+ await put(
1135
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
1136
+ body
1123
1137
  );
1124
- if (result.error)
1125
- throw new Error(result.error.message ?? "Update failed");
1126
- return result.data;
1127
1138
  },
1128
1139
  onSuccess: () => {
1129
1140
  qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
@@ -2136,6 +2147,7 @@ function OrgAvatar({ name, logo }) {
2136
2147
  }
2137
2148
  function OrganizationsPage({ teamsEnabled }) {
2138
2149
  const qc = reactQuery.useQueryClient();
2150
+ const { toggleNotification } = admin.useNotification();
2139
2151
  const [page, setPage] = react.useState(1);
2140
2152
  const [search, setSearch] = react.useState("");
2141
2153
  const [searchInput, setSearchInput] = react.useState("");
@@ -2175,6 +2187,13 @@ function OrganizationsPage({ teamsEnabled }) {
2175
2187
  onSuccess: () => {
2176
2188
  setConfirmDelete(null);
2177
2189
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2190
+ toggleNotification({ type: "success", message: "Organization deleted" });
2191
+ },
2192
+ onError: (err) => {
2193
+ toggleNotification({
2194
+ type: "danger",
2195
+ message: err.message ?? "Failed to delete organization"
2196
+ });
2178
2197
  }
2179
2198
  });
2180
2199
  const deleteManyMutation = reactQuery.useMutation({
@@ -2187,10 +2206,20 @@ function OrganizationsPage({ teamsEnabled }) {
2187
2206
  throw new Error(result.error.message ?? "Delete failed");
2188
2207
  return result.data;
2189
2208
  },
2190
- onSuccess: () => {
2209
+ onSuccess: (_data, organizationIds) => {
2191
2210
  setConfirmDeleteMany(false);
2192
2211
  setSelected(/* @__PURE__ */ new Set());
2193
2212
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2213
+ toggleNotification({
2214
+ type: "success",
2215
+ message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
2216
+ });
2217
+ },
2218
+ onError: (err) => {
2219
+ toggleNotification({
2220
+ type: "danger",
2221
+ message: err.message ?? "Failed to delete organizations"
2222
+ });
2194
2223
  }
2195
2224
  });
2196
2225
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
@@ -3273,6 +3302,42 @@ function OverviewPage() {
3273
3302
  return r.data;
3274
3303
  }
3275
3304
  });
3305
+ const sessionsRaw = sessionsQuery.data ?? [];
3306
+ const activeUserIds = [];
3307
+ const lastActiveByUserId = /* @__PURE__ */ new Map();
3308
+ for (const s of sessionsRaw) {
3309
+ if (!lastActiveByUserId.has(s.userId)) {
3310
+ lastActiveByUserId.set(s.userId, s.createdAt);
3311
+ activeUserIds.push(s.userId);
3312
+ }
3313
+ }
3314
+ const activeUsersQuery = reactQuery.useQuery({
3315
+ queryKey: ["dash-active-users", activeUserIds],
3316
+ queryFn: async () => {
3317
+ if (activeUserIds.length === 0) return [];
3318
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
3319
+ for (let i = 0; i < activeUserIds.length; i++) {
3320
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
3321
+ }
3322
+ params.set("pagination[pageSize]", "12");
3323
+ const { data } = await get(
3324
+ `/better-auth-dashboard/db?${params}`
3325
+ );
3326
+ return data.results ?? [];
3327
+ },
3328
+ enabled: feedMode === "active" && activeUserIds.length > 0
3329
+ });
3330
+ const chartRef = react.useRef(null);
3331
+ const [chartHeight, setChartHeight] = react.useState(0);
3332
+ react.useEffect(() => {
3333
+ const el = chartRef.current;
3334
+ if (!el) return;
3335
+ const ro = new ResizeObserver((entries) => {
3336
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
3337
+ });
3338
+ ro.observe(el);
3339
+ return () => ro.disconnect();
3340
+ }, []);
3276
3341
  if (statsQuery.isLoading) {
3277
3342
  return /* @__PURE__ */ jsxRuntime.jsx(
3278
3343
  designSystem.Flex,
@@ -3290,8 +3355,12 @@ function OverviewPage() {
3290
3355
  const graphData = graphQuery.data?.data ?? [];
3291
3356
  const rtnData = retentionQuery.data?.data ?? [];
3292
3357
  const users = usersQuery.data?.users ?? [];
3293
- const sessions = sessionsQuery.data ?? [];
3294
3358
  const orgCount = orgsQuery.data?.total ?? 0;
3359
+ const activeUserMap = /* @__PURE__ */ new Map();
3360
+ for (const u of activeUsersQuery.data ?? []) {
3361
+ activeUserMap.set(String(u.id), u);
3362
+ }
3363
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
3295
3364
  const totalSpark = graphData.map((d) => d.totalUsers);
3296
3365
  const newSpark = graphData.map((d) => d.newUsers);
3297
3366
  const activeSpark = graphData.map((d) => d.activeUsers);
@@ -3395,7 +3464,7 @@ function OverviewPage() {
3395
3464
  /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3396
3465
  ] }),
3397
3466
  /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3398
- /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { $delay: 5, children: [
3467
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
3399
3468
  /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
3400
3469
  /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
3401
3470
  /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3431,57 +3500,57 @@ function OverviewPage() {
3431
3500
  }
3432
3501
  )
3433
3502
  ] }),
3434
- /* @__PURE__ */ jsxRuntime.jsxs(FeedCard, { $delay: 6, children: [
3435
- /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
3436
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recent Active" }),
3437
- /* @__PURE__ */ jsxRuntime.jsxs(
3438
- FeedSelect,
3439
- {
3440
- value: feedMode,
3441
- onChange: (e) => setFeedMode(e.target.value),
3442
- children: [
3443
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3444
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recent Active" })
3445
- ]
3446
- }
3447
- )
3448
- ] }),
3449
- /* @__PURE__ */ jsxRuntime.jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3450
- /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3451
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3452
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3453
- /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3503
+ /* @__PURE__ */ jsxRuntime.jsxs(
3504
+ FeedCard,
3505
+ {
3506
+ $delay: 6,
3507
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
3508
+ children: [
3509
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
3510
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
3511
+ /* @__PURE__ */ jsxRuntime.jsxs(
3512
+ FeedSelect,
3513
+ {
3514
+ value: feedMode,
3515
+ onChange: (e) => setFeedMode(e.target.value),
3516
+ children: [
3517
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3518
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
3519
+ ]
3520
+ }
3521
+ )
3454
3522
  ] }),
3455
- /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
3456
- ] }),
3457
- /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3458
- ] }, u.id)) : sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No sessions yet" }) : sessions.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3459
- /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3460
- /* @__PURE__ */ jsxRuntime.jsx(
3461
- FeedName,
3462
- {
3463
- title: s.userAgent ?? void 0,
3464
- style: { fontFamily: T.mono, fontSize: 10 },
3465
- children: s.ipAddress ?? "—"
3466
- }
3467
- ),
3468
- /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(s.createdAt) })
3469
- ] }),
3470
- /* @__PURE__ */ jsxRuntime.jsx(
3471
- FeedEmail,
3472
- {
3473
- title: s.userAgent ?? void 0,
3474
- style: {
3475
- fontSize: 9,
3476
- overflow: "hidden",
3477
- textOverflow: "ellipsis",
3478
- whiteSpace: "nowrap"
3479
- },
3480
- children: s.userAgent ?? "Unknown agent"
3481
- }
3482
- )
3483
- ] }, s.documentId)) })
3484
- ] })
3523
+ /* @__PURE__ */ jsxRuntime.jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3524
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3525
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3526
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3527
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3528
+ ] }),
3529
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
3530
+ ] }),
3531
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3532
+ ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3533
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3534
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3535
+ /* @__PURE__ */ jsxRuntime.jsx(
3536
+ Avatar,
3537
+ {
3538
+ name: u.name,
3539
+ src: u.image ?? void 0,
3540
+ size: 20
3541
+ }
3542
+ ),
3543
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3544
+ ] }),
3545
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
3546
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
3547
+ ) })
3548
+ ] }),
3549
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3550
+ ] }, u.documentId)) })
3551
+ ]
3552
+ }
3553
+ )
3485
3554
  ] }),
3486
3555
  /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3487
3556
  /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
@@ -3857,9 +3926,10 @@ const SessionCard = styled__default.default.div`
3857
3926
  align-items: flex-start;
3858
3927
  justify-content: space-between;
3859
3928
  gap: 12px;
3860
- padding: 10px 0;
3861
- border-bottom: 1px solid #f5f5f9;
3862
- &:last-child { border-bottom: none; }
3929
+ padding: 10px 14px;
3930
+ background: white;
3931
+ border: 1px solid #eaeaef;
3932
+ border-radius: 8px;
3863
3933
  `;
3864
3934
  const SessionMeta = styled__default.default.div`
3865
3935
  display: flex;
@@ -3965,6 +4035,7 @@ function UserDetailDrawer({
3965
4035
  const [banExpiresDays, setBanExpiresDays] = react.useState("");
3966
4036
  const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
3967
4037
  const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
4038
+ const [confirmBan, setConfirmBan] = react.useState(false);
3968
4039
  const [confirmUnban, setConfirmUnban] = react.useState(false);
3969
4040
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
3970
4041
  const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
@@ -3988,33 +4059,18 @@ function UserDetailDrawer({
3988
4059
  };
3989
4060
  const updateMutation = reactQuery.useMutation({
3990
4061
  mutationFn: async () => {
3991
- const baBody = {};
3992
- if (editName !== void 0) baBody.name = editName;
3993
- if (editEmail !== void 0) baBody.email = editEmail;
4062
+ const body = { ...editExtra };
4063
+ if (editName !== void 0) body.name = editName;
4064
+ if (editEmail !== void 0) body.email = editEmail;
3994
4065
  if (editEmailVerified !== void 0)
3995
- baBody.emailVerified = editEmailVerified;
3996
- if (editImage !== void 0) baBody.image = editImage;
3997
- const ops = [];
3998
- if (Object.keys(baBody).length > 0) {
3999
- ops.push(
4000
- client.dash.updateUser(baBody, withContext({ userId })).then((result) => {
4001
- if (result.error)
4002
- throw new Error(result.error.message ?? "Update failed");
4003
- })
4004
- );
4005
- }
4006
- if (Object.keys(editExtra).length > 0) {
4007
- const documentId = strapiUserQuery.data?.documentId;
4008
- if (!documentId)
4009
- throw new Error("Could not resolve documentId for user");
4010
- ops.push(
4011
- put(
4012
- `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4013
- editExtra
4014
- )
4015
- );
4016
- }
4017
- await Promise.all(ops);
4066
+ body.emailVerified = editEmailVerified;
4067
+ if (editImage !== void 0) body.image = editImage;
4068
+ const documentId = strapiUserQuery.data?.documentId;
4069
+ if (!documentId) throw new Error("Could not resolve documentId for user");
4070
+ await put(
4071
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4072
+ body
4073
+ );
4018
4074
  },
4019
4075
  onSuccess: () => {
4020
4076
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
@@ -4096,6 +4152,7 @@ function UserDetailDrawer({
4096
4152
  return result.data;
4097
4153
  },
4098
4154
  onSuccess: () => {
4155
+ setConfirmBan(false);
4099
4156
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
4100
4157
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4101
4158
  setBanReason("");
@@ -4711,7 +4768,8 @@ function UserDetailDrawer({
4711
4768
  ] })
4712
4769
  ] })
4713
4770
  ] }) }),
4714
- banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 4, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4771
+ banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4772
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Ban status" }),
4715
4773
  /* @__PURE__ */ jsxRuntime.jsxs(WarnCard, { children: [
4716
4774
  /* @__PURE__ */ jsxRuntime.jsx(
4717
4775
  designSystem.Typography,
@@ -4726,7 +4784,7 @@ function UserDetailDrawer({
4726
4784
  "Reason: ",
4727
4785
  user.banReason
4728
4786
  ] }),
4729
- user.banExpires && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4787
+ user.banExpires ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4730
4788
  "Expires:",
4731
4789
  " ",
4732
4790
  new Date(user.banExpires).toLocaleDateString(
@@ -4738,8 +4796,7 @@ function UserDetailDrawer({
4738
4796
  day: "numeric"
4739
4797
  }
4740
4798
  )
4741
- ] }),
4742
- !user.banExpires && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4799
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4743
4800
  ] }),
4744
4801
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
4745
4802
  designSystem.Button,
@@ -4749,27 +4806,28 @@ function UserDetailDrawer({
4749
4806
  children: "Lift ban"
4750
4807
  }
4751
4808
  ) })
4752
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4809
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4810
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Apply ban" }),
4811
+ /* @__PURE__ */ jsxRuntime.jsxs(
4812
+ designSystem.Field.Root,
4813
+ {
4814
+ hint: "Optional — will be shown to the user",
4815
+ style: { width: "100%" },
4816
+ children: [
4817
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
4818
+ /* @__PURE__ */ jsxRuntime.jsx(
4819
+ designSystem.TextInput,
4820
+ {
4821
+ value: banReason,
4822
+ onChange: (e) => setBanReason(e.target.value),
4823
+ placeholder: "e.g. Violated terms of service"
4824
+ }
4825
+ ),
4826
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
4827
+ ]
4828
+ }
4829
+ ),
4753
4830
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
4754
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(
4755
- designSystem.Field.Root,
4756
- {
4757
- hint: "Optional — will be shown to the user",
4758
- style: { width: "100%" },
4759
- children: [
4760
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
4761
- /* @__PURE__ */ jsxRuntime.jsx(
4762
- designSystem.TextInput,
4763
- {
4764
- value: banReason,
4765
- onChange: (e) => setBanReason(e.target.value),
4766
- placeholder: "e.g. Violated terms of service"
4767
- }
4768
- ),
4769
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
4770
- ]
4771
- }
4772
- ) }),
4773
4831
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(
4774
4832
  designSystem.Field.Root,
4775
4833
  {
@@ -4790,24 +4848,13 @@ function UserDetailDrawer({
4790
4848
  ]
4791
4849
  }
4792
4850
  ) }),
4793
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: banExpiryPreview ? /* @__PURE__ */ jsxRuntime.jsx(
4794
- designSystem.Flex,
4795
- {
4796
- direction: "column",
4797
- justifyContent: "flex-end",
4798
- style: { height: "100%", paddingBottom: 4 },
4799
- children: /* @__PURE__ */ jsxRuntime.jsxs(PreviewPill, { children: [
4800
- "⏱ Expires ",
4801
- banExpiryPreview
4802
- ] })
4803
- }
4804
- ) : /* @__PURE__ */ jsxRuntime.jsx(
4851
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
4805
4852
  designSystem.Flex,
4806
4853
  {
4807
4854
  direction: "column",
4808
4855
  justifyContent: "flex-end",
4809
4856
  style: { height: "100%", paddingBottom: 4 },
4810
- children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: "♾ Permanent ban" })
4857
+ children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
4811
4858
  }
4812
4859
  ) })
4813
4860
  ] }),
@@ -4815,8 +4862,7 @@ function UserDetailDrawer({
4815
4862
  designSystem.Button,
4816
4863
  {
4817
4864
  variant: "danger",
4818
- loading: banMutation.isLoading,
4819
- onClick: () => banMutation.mutate(),
4865
+ onClick: () => setConfirmBan(true),
4820
4866
  children: "Ban user"
4821
4867
  }
4822
4868
  ) })
@@ -4843,19 +4889,27 @@ function UserDetailDrawer({
4843
4889
  ] }),
4844
4890
  /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4845
4891
  /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Active sessions" }),
4846
- sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 0, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
4892
+ sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
4847
4893
  /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
4848
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", style: { flexWrap: "wrap" }, children: [
4849
- session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
4850
- /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
4851
- "Created",
4852
- " ",
4853
- new Date(session.createdAt).toLocaleString(),
4854
- " · Expires",
4855
- " ",
4856
- new Date(session.expiresAt).toLocaleString()
4857
- ] })
4858
- ] }),
4894
+ /* @__PURE__ */ jsxRuntime.jsxs(
4895
+ designSystem.Flex,
4896
+ {
4897
+ gap: 2,
4898
+ alignItems: "center",
4899
+ style: { flexWrap: "wrap" },
4900
+ children: [
4901
+ session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
4902
+ /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
4903
+ "Created",
4904
+ " ",
4905
+ new Date(session.createdAt).toLocaleString(),
4906
+ " · Expires",
4907
+ " ",
4908
+ new Date(session.expiresAt).toLocaleString()
4909
+ ] })
4910
+ ]
4911
+ }
4912
+ ),
4859
4913
  session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
4860
4914
  ] }),
4861
4915
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4917,6 +4971,18 @@ function UserDetailDrawer({
4917
4971
  onCancel: () => setConfirmRevokeSessionId(null)
4918
4972
  }
4919
4973
  ),
4974
+ confirmBan && /* @__PURE__ */ jsxRuntime.jsx(
4975
+ ConfirmDialog,
4976
+ {
4977
+ title: "Ban user",
4978
+ message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
4979
+ confirmLabel: "Ban user",
4980
+ variant: "danger",
4981
+ loading: banMutation.isLoading,
4982
+ onConfirm: () => banMutation.mutate(),
4983
+ onCancel: () => setConfirmBan(false)
4984
+ }
4985
+ ),
4920
4986
  confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
4921
4987
  ConfirmDialog,
4922
4988
  {
@@ -5147,6 +5213,7 @@ const Toolbar = styled__default.default.div`
5147
5213
  `;
5148
5214
  function UsersPage({ config }) {
5149
5215
  const qc = reactQuery.useQueryClient();
5216
+ const { toggleNotification } = admin.useNotification();
5150
5217
  const banEnabled = hasPlugin(config, "admin");
5151
5218
  const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
5152
5219
  const twoFactorEnabled = hasPlugin(config, "two-factor");
@@ -5178,6 +5245,13 @@ function UsersPage({ config }) {
5178
5245
  setConfirmDelete(null);
5179
5246
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5180
5247
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5248
+ toggleNotification({ type: "success", message: "User deleted" });
5249
+ },
5250
+ onError: (err) => {
5251
+ toggleNotification({
5252
+ type: "danger",
5253
+ message: err.message ?? "Failed to delete user"
5254
+ });
5181
5255
  }
5182
5256
  });
5183
5257
  const deleteManyMutation = reactQuery.useMutation({
@@ -5190,11 +5264,21 @@ function UsersPage({ config }) {
5190
5264
  throw new Error(result.error.message ?? "Delete failed");
5191
5265
  return result.data;
5192
5266
  },
5193
- onSuccess: () => {
5267
+ onSuccess: (_data, userIds) => {
5194
5268
  setConfirmDeleteMany(false);
5195
5269
  setSelected(/* @__PURE__ */ new Set());
5196
5270
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5197
5271
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5272
+ toggleNotification({
5273
+ type: "success",
5274
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
5275
+ });
5276
+ },
5277
+ onError: (err) => {
5278
+ toggleNotification({
5279
+ type: "danger",
5280
+ message: err.message ?? "Failed to delete users"
5281
+ });
5198
5282
  }
5199
5283
  });
5200
5284
  const banManyMutation = reactQuery.useMutation({
@@ -5206,10 +5290,20 @@ function UsersPage({ config }) {
5206
5290
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
5207
5291
  return result.data;
5208
5292
  },
5209
- onSuccess: () => {
5293
+ onSuccess: (_data, userIds) => {
5210
5294
  setConfirmBanMany(false);
5211
5295
  setSelected(/* @__PURE__ */ new Set());
5212
5296
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5297
+ toggleNotification({
5298
+ type: "success",
5299
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
5300
+ });
5301
+ },
5302
+ onError: (err) => {
5303
+ toggleNotification({
5304
+ type: "danger",
5305
+ message: err.message ?? "Failed to ban users"
5306
+ });
5213
5307
  }
5214
5308
  });
5215
5309
  const toggleSelect = (id) => {
@@ -5388,14 +5482,28 @@ function UsersPage({ config }) {
5388
5482
  user.id
5389
5483
  )) })
5390
5484
  ] }) }),
5391
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
5392
- designSystem.Pagination,
5393
- {
5394
- activePage: page,
5395
- pageCount,
5396
- onChangePage: setPage
5397
- }
5398
- ) }),
5485
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
5486
+ /* @__PURE__ */ jsxRuntime.jsx(
5487
+ designSystem.Button,
5488
+ {
5489
+ variant: "tertiary",
5490
+ size: "S",
5491
+ disabled: page === 1,
5492
+ onClick: () => setPage((p) => p - 1),
5493
+ children: "Previous"
5494
+ }
5495
+ ),
5496
+ /* @__PURE__ */ jsxRuntime.jsx(
5497
+ designSystem.Button,
5498
+ {
5499
+ variant: "tertiary",
5500
+ size: "S",
5501
+ disabled: page >= pageCount,
5502
+ onClick: () => setPage((p) => p + 1),
5503
+ children: "Next"
5504
+ }
5505
+ )
5506
+ ] }) }),
5399
5507
  showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
5400
5508
  detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
5401
5509
  UserDetailDrawer,