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

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-C4--Z2Qg.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);
@@ -1108,7 +1119,7 @@ function OrganizationDetail({
1108
1119
  };
1109
1120
  const org = orgQuery.data;
1110
1121
  const extraData = {
1111
- ...org,
1122
+ ...strapiOrgQuery.data ?? {},
1112
1123
  ...editExtra
1113
1124
  };
1114
1125
  const updateOrgMutation = reactQuery.useMutation({
@@ -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 : [];
@@ -3257,7 +3286,7 @@ function OverviewPage() {
3257
3286
  queryKey: ["dash-recent-sessions"],
3258
3287
  queryFn: async () => {
3259
3288
  const { data } = await get(
3260
- "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
3289
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=updatedAt:desc"
3261
3290
  );
3262
3291
  return data.results ?? [];
3263
3292
  },
@@ -3273,6 +3302,43 @@ 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
+ const uid = String(s.userId);
3310
+ if (!lastActiveByUserId.has(uid)) {
3311
+ lastActiveByUserId.set(uid, s.updatedAt ?? s.createdAt);
3312
+ activeUserIds.push(uid);
3313
+ }
3314
+ }
3315
+ const activeUsersQuery = reactQuery.useQuery({
3316
+ queryKey: ["dash-active-users", activeUserIds],
3317
+ queryFn: async () => {
3318
+ if (activeUserIds.length === 0) return [];
3319
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
3320
+ for (let i = 0; i < activeUserIds.length; i++) {
3321
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
3322
+ }
3323
+ params.set("pagination[pageSize]", "12");
3324
+ const { data } = await get(
3325
+ `/better-auth-dashboard/db?${params}`
3326
+ );
3327
+ return data.results ?? [];
3328
+ },
3329
+ enabled: feedMode === "active" && activeUserIds.length > 0
3330
+ });
3331
+ const chartRef = react.useRef(null);
3332
+ const [chartHeight, setChartHeight] = react.useState(0);
3333
+ react.useEffect(() => {
3334
+ const el = chartRef.current;
3335
+ if (!el) return;
3336
+ const ro = new ResizeObserver((entries) => {
3337
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
3338
+ });
3339
+ ro.observe(el);
3340
+ return () => ro.disconnect();
3341
+ }, []);
3276
3342
  if (statsQuery.isLoading) {
3277
3343
  return /* @__PURE__ */ jsxRuntime.jsx(
3278
3344
  designSystem.Flex,
@@ -3290,8 +3356,12 @@ function OverviewPage() {
3290
3356
  const graphData = graphQuery.data?.data ?? [];
3291
3357
  const rtnData = retentionQuery.data?.data ?? [];
3292
3358
  const users = usersQuery.data?.users ?? [];
3293
- const sessions = sessionsQuery.data ?? [];
3294
3359
  const orgCount = orgsQuery.data?.total ?? 0;
3360
+ const activeUserMap = /* @__PURE__ */ new Map();
3361
+ for (const u of activeUsersQuery.data ?? []) {
3362
+ activeUserMap.set(String(u.id), u);
3363
+ }
3364
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
3295
3365
  const totalSpark = graphData.map((d) => d.totalUsers);
3296
3366
  const newSpark = graphData.map((d) => d.newUsers);
3297
3367
  const activeSpark = graphData.map((d) => d.activeUsers);
@@ -3395,7 +3465,7 @@ function OverviewPage() {
3395
3465
  /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3396
3466
  ] }),
3397
3467
  /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3398
- /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { $delay: 5, children: [
3468
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
3399
3469
  /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
3400
3470
  /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
3401
3471
  /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3431,57 +3501,57 @@ function OverviewPage() {
3431
3501
  }
3432
3502
  )
3433
3503
  ] }),
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 })
3504
+ /* @__PURE__ */ jsxRuntime.jsxs(
3505
+ FeedCard,
3506
+ {
3507
+ $delay: 6,
3508
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
3509
+ children: [
3510
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
3511
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
3512
+ /* @__PURE__ */ jsxRuntime.jsxs(
3513
+ FeedSelect,
3514
+ {
3515
+ value: feedMode,
3516
+ onChange: (e) => setFeedMode(e.target.value),
3517
+ children: [
3518
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3519
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
3520
+ ]
3521
+ }
3522
+ )
3454
3523
  ] }),
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
- ] })
3524
+ /* @__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: [
3525
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3526
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3527
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3528
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3529
+ ] }),
3530
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
3531
+ ] }),
3532
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3533
+ ] }, 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: [
3534
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3535
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3536
+ /* @__PURE__ */ jsxRuntime.jsx(
3537
+ Avatar,
3538
+ {
3539
+ name: u.name,
3540
+ src: u.image ?? void 0,
3541
+ size: 20
3542
+ }
3543
+ ),
3544
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3545
+ ] }),
3546
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
3547
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
3548
+ ) })
3549
+ ] }),
3550
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3551
+ ] }, u.documentId)) })
3552
+ ]
3553
+ }
3554
+ )
3485
3555
  ] }),
3486
3556
  /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3487
3557
  /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
@@ -3857,9 +3927,10 @@ const SessionCard = styled__default.default.div`
3857
3927
  align-items: flex-start;
3858
3928
  justify-content: space-between;
3859
3929
  gap: 12px;
3860
- padding: 10px 0;
3861
- border-bottom: 1px solid #f5f5f9;
3862
- &:last-child { border-bottom: none; }
3930
+ padding: 10px 14px;
3931
+ background: white;
3932
+ border: 1px solid #eaeaef;
3933
+ border-radius: 8px;
3863
3934
  `;
3864
3935
  const SessionMeta = styled__default.default.div`
3865
3936
  display: flex;
@@ -3965,6 +4036,7 @@ function UserDetailDrawer({
3965
4036
  const [banExpiresDays, setBanExpiresDays] = react.useState("");
3966
4037
  const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
3967
4038
  const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
4039
+ const [confirmBan, setConfirmBan] = react.useState(false);
3968
4040
  const [confirmUnban, setConfirmUnban] = react.useState(false);
3969
4041
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
3970
4042
  const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
@@ -3988,33 +4060,18 @@ function UserDetailDrawer({
3988
4060
  };
3989
4061
  const updateMutation = reactQuery.useMutation({
3990
4062
  mutationFn: async () => {
3991
- const baBody = {};
3992
- if (editName !== void 0) baBody.name = editName;
3993
- if (editEmail !== void 0) baBody.email = editEmail;
4063
+ const body = { ...editExtra };
4064
+ if (editName !== void 0) body.name = editName;
4065
+ if (editEmail !== void 0) body.email = editEmail;
3994
4066
  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);
4067
+ body.emailVerified = editEmailVerified;
4068
+ if (editImage !== void 0) body.image = editImage;
4069
+ const documentId = strapiUserQuery.data?.documentId;
4070
+ if (!documentId) throw new Error("Could not resolve documentId for user");
4071
+ await put(
4072
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4073
+ body
4074
+ );
4018
4075
  },
4019
4076
  onSuccess: () => {
4020
4077
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
@@ -4042,7 +4099,7 @@ function UserDetailDrawer({
4042
4099
  mutationFn: async (sessionId) => {
4043
4100
  const result = await client.dash.sessions.revoke(
4044
4101
  {},
4045
- withContext({ sessionId })
4102
+ withContext({ sessionId, userId })
4046
4103
  );
4047
4104
  if (result.error)
4048
4105
  throw new Error(result.error.message ?? "Revoke failed");
@@ -4096,6 +4153,7 @@ function UserDetailDrawer({
4096
4153
  return result.data;
4097
4154
  },
4098
4155
  onSuccess: () => {
4156
+ setConfirmBan(false);
4099
4157
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
4100
4158
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4101
4159
  setBanReason("");
@@ -4711,7 +4769,8 @@ function UserDetailDrawer({
4711
4769
  ] })
4712
4770
  ] })
4713
4771
  ] }) }),
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: [
4772
+ 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: [
4773
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Ban status" }),
4715
4774
  /* @__PURE__ */ jsxRuntime.jsxs(WarnCard, { children: [
4716
4775
  /* @__PURE__ */ jsxRuntime.jsx(
4717
4776
  designSystem.Typography,
@@ -4726,7 +4785,7 @@ function UserDetailDrawer({
4726
4785
  "Reason: ",
4727
4786
  user.banReason
4728
4787
  ] }),
4729
- user.banExpires && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4788
+ user.banExpires ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4730
4789
  "Expires:",
4731
4790
  " ",
4732
4791
  new Date(user.banExpires).toLocaleDateString(
@@ -4738,8 +4797,7 @@ function UserDetailDrawer({
4738
4797
  day: "numeric"
4739
4798
  }
4740
4799
  )
4741
- ] }),
4742
- !user.banExpires && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4800
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4743
4801
  ] }),
4744
4802
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
4745
4803
  designSystem.Button,
@@ -4749,27 +4807,28 @@ function UserDetailDrawer({
4749
4807
  children: "Lift ban"
4750
4808
  }
4751
4809
  ) })
4752
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4810
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4811
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Apply ban" }),
4812
+ /* @__PURE__ */ jsxRuntime.jsxs(
4813
+ designSystem.Field.Root,
4814
+ {
4815
+ hint: "Optional — will be shown to the user",
4816
+ style: { width: "100%" },
4817
+ children: [
4818
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
4819
+ /* @__PURE__ */ jsxRuntime.jsx(
4820
+ designSystem.TextInput,
4821
+ {
4822
+ value: banReason,
4823
+ onChange: (e) => setBanReason(e.target.value),
4824
+ placeholder: "e.g. Violated terms of service"
4825
+ }
4826
+ ),
4827
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
4828
+ ]
4829
+ }
4830
+ ),
4753
4831
  /* @__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
4832
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(
4774
4833
  designSystem.Field.Root,
4775
4834
  {
@@ -4790,24 +4849,13 @@ function UserDetailDrawer({
4790
4849
  ]
4791
4850
  }
4792
4851
  ) }),
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(
4852
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
4805
4853
  designSystem.Flex,
4806
4854
  {
4807
4855
  direction: "column",
4808
4856
  justifyContent: "flex-end",
4809
4857
  style: { height: "100%", paddingBottom: 4 },
4810
- children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: "♾ Permanent ban" })
4858
+ children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
4811
4859
  }
4812
4860
  ) })
4813
4861
  ] }),
@@ -4815,8 +4863,7 @@ function UserDetailDrawer({
4815
4863
  designSystem.Button,
4816
4864
  {
4817
4865
  variant: "danger",
4818
- loading: banMutation.isLoading,
4819
- onClick: () => banMutation.mutate(),
4866
+ onClick: () => setConfirmBan(true),
4820
4867
  children: "Ban user"
4821
4868
  }
4822
4869
  ) })
@@ -4843,19 +4890,27 @@ function UserDetailDrawer({
4843
4890
  ] }),
4844
4891
  /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4845
4892
  /* @__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: [
4893
+ 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
4894
  /* @__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
- ] }),
4895
+ /* @__PURE__ */ jsxRuntime.jsxs(
4896
+ designSystem.Flex,
4897
+ {
4898
+ gap: 2,
4899
+ alignItems: "center",
4900
+ style: { flexWrap: "wrap" },
4901
+ children: [
4902
+ session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
4903
+ /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
4904
+ "Created",
4905
+ " ",
4906
+ new Date(session.createdAt).toLocaleString(),
4907
+ " · Expires",
4908
+ " ",
4909
+ new Date(session.expiresAt).toLocaleString()
4910
+ ] })
4911
+ ]
4912
+ }
4913
+ ),
4859
4914
  session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
4860
4915
  ] }),
4861
4916
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4917,6 +4972,18 @@ function UserDetailDrawer({
4917
4972
  onCancel: () => setConfirmRevokeSessionId(null)
4918
4973
  }
4919
4974
  ),
4975
+ confirmBan && /* @__PURE__ */ jsxRuntime.jsx(
4976
+ ConfirmDialog,
4977
+ {
4978
+ title: "Ban user",
4979
+ 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.",
4980
+ confirmLabel: "Ban user",
4981
+ variant: "danger",
4982
+ loading: banMutation.isLoading,
4983
+ onConfirm: () => banMutation.mutate(),
4984
+ onCancel: () => setConfirmBan(false)
4985
+ }
4986
+ ),
4920
4987
  confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
4921
4988
  ConfirmDialog,
4922
4989
  {
@@ -5147,6 +5214,7 @@ const Toolbar = styled__default.default.div`
5147
5214
  `;
5148
5215
  function UsersPage({ config }) {
5149
5216
  const qc = reactQuery.useQueryClient();
5217
+ const { toggleNotification } = admin.useNotification();
5150
5218
  const banEnabled = hasPlugin(config, "admin");
5151
5219
  const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
5152
5220
  const twoFactorEnabled = hasPlugin(config, "two-factor");
@@ -5178,6 +5246,13 @@ function UsersPage({ config }) {
5178
5246
  setConfirmDelete(null);
5179
5247
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5180
5248
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5249
+ toggleNotification({ type: "success", message: "User deleted" });
5250
+ },
5251
+ onError: (err) => {
5252
+ toggleNotification({
5253
+ type: "danger",
5254
+ message: err.message ?? "Failed to delete user"
5255
+ });
5181
5256
  }
5182
5257
  });
5183
5258
  const deleteManyMutation = reactQuery.useMutation({
@@ -5190,11 +5265,21 @@ function UsersPage({ config }) {
5190
5265
  throw new Error(result.error.message ?? "Delete failed");
5191
5266
  return result.data;
5192
5267
  },
5193
- onSuccess: () => {
5268
+ onSuccess: (_data, userIds) => {
5194
5269
  setConfirmDeleteMany(false);
5195
5270
  setSelected(/* @__PURE__ */ new Set());
5196
5271
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5197
5272
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5273
+ toggleNotification({
5274
+ type: "success",
5275
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
5276
+ });
5277
+ },
5278
+ onError: (err) => {
5279
+ toggleNotification({
5280
+ type: "danger",
5281
+ message: err.message ?? "Failed to delete users"
5282
+ });
5198
5283
  }
5199
5284
  });
5200
5285
  const banManyMutation = reactQuery.useMutation({
@@ -5206,10 +5291,20 @@ function UsersPage({ config }) {
5206
5291
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
5207
5292
  return result.data;
5208
5293
  },
5209
- onSuccess: () => {
5294
+ onSuccess: (_data, userIds) => {
5210
5295
  setConfirmBanMany(false);
5211
5296
  setSelected(/* @__PURE__ */ new Set());
5212
5297
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5298
+ toggleNotification({
5299
+ type: "success",
5300
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
5301
+ });
5302
+ },
5303
+ onError: (err) => {
5304
+ toggleNotification({
5305
+ type: "danger",
5306
+ message: err.message ?? "Failed to ban users"
5307
+ });
5213
5308
  }
5214
5309
  });
5215
5310
  const toggleSelect = (id) => {
@@ -5388,14 +5483,28 @@ function UsersPage({ config }) {
5388
5483
  user.id
5389
5484
  )) })
5390
5485
  ] }) }),
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
- ) }),
5486
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
5487
+ /* @__PURE__ */ jsxRuntime.jsx(
5488
+ designSystem.Button,
5489
+ {
5490
+ variant: "tertiary",
5491
+ size: "S",
5492
+ disabled: page === 1,
5493
+ onClick: () => setPage((p) => p - 1),
5494
+ children: "Previous"
5495
+ }
5496
+ ),
5497
+ /* @__PURE__ */ jsxRuntime.jsx(
5498
+ designSystem.Button,
5499
+ {
5500
+ variant: "tertiary",
5501
+ size: "S",
5502
+ disabled: page >= pageCount,
5503
+ onClick: () => setPage((p) => p + 1),
5504
+ children: "Next"
5505
+ }
5506
+ )
5507
+ ] }) }),
5399
5508
  showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
5400
5509
  detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
5401
5510
  UserDetailDrawer,