@strapi-community/plugin-better-auth-dashboard 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -86,6 +86,125 @@ pnpm develop
86
86
 
87
87
  The dashboard is now available in the Strapi admin panel.
88
88
 
89
+ ## MCP server integration
90
+
91
+ This plugin can expose Better Auth **admin and analytics tools** on Strapi's
92
+ native [MCP server](https://docs.strapi.io/cms/features/strapi-mcp-server), so AI
93
+ clients can list, inspect, update and ban users and read sign-up analytics
94
+ through `POST /mcp`. It requires Strapi `>= 5.47`.
95
+
96
+ Every tool calls the Better Auth `dash()` plugin endpoints — the **same calls the
97
+ dashboard UI makes** — so Better Auth's own business logic runs (validation, ban
98
+ side-effects, stats aggregation). It therefore relies on the same setup the
99
+ dashboard already requires: `dash()` + `jwt()` configured and the default
100
+ `basePath` (`/api/auth`).
101
+
102
+ The integration is **opt-in** and gated by two switches:
103
+
104
+ ```typescript title="config/server.ts"
105
+ export default ({ env }) => ({
106
+ // ...existing config
107
+ mcp: { enabled: true }, // Strapi's own MCP server (serves POST /mcp)
108
+ });
109
+ ```
110
+
111
+ ```typescript title="config/plugins.ts"
112
+ export default {
113
+ "better-auth-dashboard": {
114
+ config: {
115
+ mcp: { enabled: true }, // register this plugin's Better Auth tools
116
+ },
117
+ },
118
+ };
119
+ ```
120
+
121
+ > [!NOTE]
122
+ > On Strapi versions below 5.47 (no MCP server) the plugin simply skips tool
123
+ > registration — enabling the flag is harmless.
124
+
125
+ ### Authentication & permissions
126
+
127
+ MCP clients authenticate with an **admin API token** (created in
128
+ _Settings → API Tokens_) sent as `Authorization: Bearer <token>`. Every tool is
129
+ gated by the **same permissions as the dashboard UI** — the token's admin role
130
+ must hold the relevant `Better Auth Dashboard` permissions:
131
+
132
+ | Permission | Tools it unlocks |
133
+ | ---------- | ---------------- |
134
+ | `user.read` | `better_auth_list_users`, `better_auth_get_user` |
135
+ | `user.update` | `better_auth_update_user`, `better_auth_ban_user`, `better_auth_unban_user` |
136
+ | `overview.read` | `better_auth_user_stats`, `better_auth_signups_graph` |
137
+
138
+ ### Available tools
139
+
140
+ User tools identify the target by its **Better Auth user id** (as returned by
141
+ `better_auth_list_users`), exactly like the dashboard.
142
+
143
+ | Tool | Description |
144
+ | ---- | ----------- |
145
+ | `better_auth_list_users` | List and search users (`search` matches name/email); returns users + total |
146
+ | `better_auth_get_user` | Fetch a single user by Better Auth `userId`, with session/account counts |
147
+ | `better_auth_update_user` | Update a user's `name` / `email` / `image` / `emailVerified` |
148
+ | `better_auth_ban_user` | Ban a user (optional reason + expiry in days); Better Auth revokes sessions¹ |
149
+ | `better_auth_unban_user` | Lift a user's ban¹ |
150
+ | `better_auth_user_stats` | Totals + day/week/month sign-ups (with % deltas) + active users |
151
+ | `better_auth_signups_graph` | Sign-up time series (new/active/total users) by `day`/`week`/`month` |
152
+
153
+ ¹ Registered only when the Better Auth [`admin`](https://www.better-auth.com/docs/plugins/admin)
154
+ plugin is configured (the `banned` field it adds must exist on the `user` schema).
155
+
156
+ ## Third-party dash API gateway
157
+
158
+ If you want to call the Better Auth `dash()` endpoints from a third-party service
159
+ (not the admin panel, not MCP), enable the **opt-in gateway**. It exposes the full
160
+ dash surface under `/api/better-auth-dashboard/dash/*`, authenticated with a
161
+ **Strapi API token**, and forwards each request through the same signed-JWT
162
+ mechanism the dashboard uses — so Better Auth's own logic runs.
163
+
164
+ ```typescript title="config/plugins.ts"
165
+ export default {
166
+ "better-auth-dashboard": {
167
+ config: {
168
+ dashApi: { enabled: true }, // default: false
169
+ },
170
+ },
171
+ };
172
+ ```
173
+
174
+ > [!NOTE]
175
+ > Like the MCP integration, this is opt-in at registration: when `dashApi.enabled`
176
+ > is false (the default) the routes are never mounted, so requests to them 404.
177
+
178
+ Authentication is Strapi's standard content-api token auth: send a Strapi API
179
+ token (Settings → API Tokens) as a Bearer token. Use a **Full access** token, or a
180
+ **Custom** token granted this gateway route's permission. The endpoint path after
181
+ `/dash/` is the dash endpoint; per-user/org context goes in a base64(JSON)
182
+ `X-Dash-Context` header, exactly like the dashboard client:
183
+
184
+ ```bash
185
+ # Analytics
186
+ curl "$STRAPI/api/better-auth-dashboard/dash/user-stats" \
187
+ -H "Authorization: Bearer $API_TOKEN"
188
+
189
+ # List users
190
+ curl "$STRAPI/api/better-auth-dashboard/dash/list-users?limit=50" \
191
+ -H "Authorization: Bearer $API_TOKEN"
192
+
193
+ # Ban a user (userId passed via X-Dash-Context)
194
+ curl -X POST "$STRAPI/api/better-auth-dashboard/dash/ban-user" \
195
+ -H "Authorization: Bearer $API_TOKEN" \
196
+ -H "X-Dash-Context: $(echo -n '{"userId":"42"}' | base64)" \
197
+ -H "Content-Type: application/json" \
198
+ -d '{"banReason":"abuse"}'
199
+ ```
200
+
201
+ > [!WARNING]
202
+ > This is a **generic passthrough to the whole dash surface** (including user
203
+ > deletion and organization management) exposed as a single route, so granting it
204
+ > grants the entire dash API — the gateway does not scope per dash endpoint. A
205
+ > Full access token reaches everything. Create a **dedicated** token for the
206
+ > integration and treat it as privileged. Leave `dashApi` disabled if unused.
207
+
89
208
  ## Pages
90
209
 
91
210
  | Tab | Description |
@@ -140,7 +259,7 @@ Available APIs:
140
259
 
141
260
  `isAvailable` defaults to `true`. Its result is cached for five minutes, combined with permission checks, and applied to both navigation and direct route access. Errors are treated as unavailable.
142
261
 
143
- See the [Dashboard documentation](https://strapi-community.github.io/plugin-better-auth/docs/better-auth/dashboard#dashboard-screen-api) for the complete API reference and extension guidance.
262
+ See the [Dashboard screen API documentation](https://strapi-community.github.io/plugin-better-auth/docs/dashboard/screen-api) for the complete API reference and extension guidance.
144
263
 
145
264
  ## Requirements
146
265
 
@@ -150,7 +269,7 @@ See the [Dashboard documentation](https://strapi-community.github.io/plugin-bett
150
269
 
151
270
  ## Resources
152
271
 
153
- - [Documentation](https://strapi-community.github.io/plugin-better-auth/docs/better-auth/dashboard)
272
+ - [Documentation](https://strapi-community.github.io/plugin-better-auth/docs/dashboard)
154
273
  - [Better Auth Documentation](https://better-auth.com)
155
274
  - [Strapi Documentation](https://docs.strapi.io)
156
275
 
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useId, useState, useEffect, useMemo } from "react";
3
3
  import { useQueryClient, QueryClient, QueryClientProvider } from "react-query";
4
4
  import { NavLink, Outlet, useMatch, Navigate } from "react-router-dom";
5
- import { g as getTranslation, a as getDashLinks, P as PLUGIN_ID, b as getDashLinkAvailabilityQueryKey, r as resolveDashLinkAvailability, D as DASH_LINK_AVAILABILITY_STALE_TIME } from "./index-BI2WIsds.mjs";
5
+ import { g as getTranslation, a as getDashLinks, P as PLUGIN_ID, b as getDashLinkAvailabilityQueryKey, r as resolveDashLinkAvailability, D as DASH_LINK_AVAILABILITY_STALE_TIME } from "./index-DAkkUU0o.mjs";
6
6
  import { Typography, ScrollArea, Box, Flex, Badge, SubNav as SubNav$1, Divider } from "@strapi/design-system";
7
7
  import { useAuth, Layouts, Page, BackButton } from "@strapi/strapi/admin";
8
8
  import { useIntl } from "react-intl";
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
5
  const reactQuery = require("react-query");
6
6
  const reactRouterDom = require("react-router-dom");
7
- const index = require("./index-BCBu-0nr.js");
7
+ const index = require("./index-Bko3vlk0.js");
8
8
  const designSystem = require("@strapi/design-system");
9
9
  const admin = require("@strapi/strapi/admin");
10
10
  const reactIntl = require("react-intl");
@@ -5,7 +5,7 @@ import { createContext, useContext, useState, useRef, useEffect } from "react";
5
5
  import { useIntl } from "react-intl";
6
6
  import { useQuery } from "react-query";
7
7
  import styled, { useTheme } from "styled-components";
8
- import { c as PERMISSIONS, P as PLUGIN_ID, g as getTranslation, d as client } from "./index-BI2WIsds.mjs";
8
+ import { c as PERMISSIONS, P as PLUGIN_ID, g as getTranslation, d as client } from "./index-DAkkUU0o.mjs";
9
9
  import { A as Avatar } from "./Avatar-zlMTAZh3.mjs";
10
10
  const SegmentedControlContext = createContext(null);
11
11
  const Root = ({ value, onChange, label, children }) => /* @__PURE__ */ jsx(SegmentedControlContext.Provider, { value: { value, onChange }, children: /* @__PURE__ */ jsx(
@@ -378,7 +378,7 @@ const plugin = {
378
378
  id: `${PLUGIN_ID}.plugin.name`,
379
379
  defaultMessage: "Auth Dashboard"
380
380
  },
381
- Component: async () => Promise.resolve().then(() => require("./Root-CsiOnzlB.js")),
381
+ Component: async () => Promise.resolve().then(() => require("./Root-CMQ7swi6.js")),
382
382
  permissions: [
383
383
  ...PERMISSIONS.overview,
384
384
  ...PERMISSIONS.user,
@@ -402,7 +402,7 @@ const plugin = {
402
402
  defaultMessage: "Overview"
403
403
  },
404
404
  to: "/overview",
405
- Component: () => Promise.resolve().then(() => require("./index-DQdn5s8q.js")),
405
+ Component: () => Promise.resolve().then(() => require("./index-hbz3gTsi.js")),
406
406
  permissions: PERMISSIONS.overview
407
407
  });
408
408
  dash.addDashLink("general", {
@@ -412,7 +412,7 @@ const plugin = {
412
412
  defaultMessage: "User Management"
413
413
  },
414
414
  to: "/users",
415
- Component: () => Promise.resolve().then(() => require("./index-D9ttdrT3.js")),
415
+ Component: () => Promise.resolve().then(() => require("./index-D1nrIz0Y.js")),
416
416
  permissions: PERMISSIONS.user
417
417
  });
418
418
  dash.addDashLink("general", {
@@ -422,7 +422,7 @@ const plugin = {
422
422
  defaultMessage: "Organization Management"
423
423
  },
424
424
  to: "/organizations",
425
- Component: () => Promise.resolve().then(() => require("./index-CPSoAbZt.js")),
425
+ Component: () => Promise.resolve().then(() => require("./index-CSyPvFnN.js")),
426
426
  permissions: PERMISSIONS.organization,
427
427
  isAvailable: async () => hasPlugin(await getDashConfig(), "organization")
428
428
  });
@@ -7,9 +7,9 @@ const admin = require("@strapi/strapi/admin");
7
7
  const react = require("react");
8
8
  const reactIntl = require("react-intl");
9
9
  const reactQuery = require("react-query");
10
- const index = require("./index-BCBu-0nr.js");
10
+ const index = require("./index-Bko3vlk0.js");
11
11
  const Avatar = require("./Avatar-CdT7eBCh.js");
12
- const useModelSchema = require("./useModelSchema-BogQah8f.js");
12
+ const useModelSchema = require("./useModelSchema-DON5XCju.js");
13
13
  function UserCombobox({
14
14
  label,
15
15
  hint,
@@ -467,10 +467,12 @@ function OrganizationDetail({
467
467
  });
468
468
  const strapiOrgQuery = reactQuery.useQuery({
469
469
  queryKey: ["dash-strapi-org", organizationId],
470
- enabled: !!orgQuery.data,
470
+ enabled: !!schemaQuery.data,
471
471
  queryFn: async () => {
472
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
473
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
472
474
  const { data } = await get(
473
- `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
475
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1${populateParam}`
474
476
  );
475
477
  return data.results?.[0] ?? null;
476
478
  }
@@ -5,9 +5,9 @@ import { useNotification, useFetchClient, Page, useRBAC, Layouts } from "@strapi
5
5
  import { useState } from "react";
6
6
  import { useIntl } from "react-intl";
7
7
  import { useQuery, useQueryClient, useMutation } from "react-query";
8
- import { d as client, g as getTranslation, e as getAuthHeaders, u as useDashConfig, c as PERMISSIONS, P as PLUGIN_ID, h as hasPlugin } from "./index-BI2WIsds.mjs";
8
+ import { d as client, g as getTranslation, e as getAuthHeaders, u as useDashConfig, c as PERMISSIONS, P as PLUGIN_ID, h as hasPlugin } from "./index-DAkkUU0o.mjs";
9
9
  import { A as Avatar } from "./Avatar-zlMTAZh3.mjs";
10
- import { S as SheetDialog, F as FormSection, a as SectionLabel, M as MediaPickerField, u as useModelSchema, w as withContext, E as EditLayout, C as CustomFieldsSection, b as EditSidebar, c as MetaItem, d as MetaKey, e as MonoChip, f as MetaVal, g as EditViewSidePanels, A as AccountRow, P as ProviderBadge, D as DangerRow, W as WarnCard, h as PreviewPill, R as RoleBadge, i as ConfirmDialog, j as Code } from "./useModelSchema-C6bxNjW4.mjs";
10
+ import { S as SheetDialog, F as FormSection, a as SectionLabel, M as MediaPickerField, u as useModelSchema, w as withContext, E as EditLayout, C as CustomFieldsSection, b as EditSidebar, c as MetaItem, d as MetaKey, e as MonoChip, f as MetaVal, g as EditViewSidePanels, A as AccountRow, P as ProviderBadge, D as DangerRow, W as WarnCard, h as PreviewPill, R as RoleBadge, i as ConfirmDialog, j as Code } from "./useModelSchema-Bo8WEBwb.mjs";
11
11
  function useUsers(options = {}) {
12
12
  const { limit = 25, offset = 0, search } = options;
13
13
  const { formatMessage } = useIntl();
@@ -7,9 +7,9 @@ const admin = require("@strapi/strapi/admin");
7
7
  const react = require("react");
8
8
  const reactIntl = require("react-intl");
9
9
  const reactQuery = require("react-query");
10
- const index = require("./index-BCBu-0nr.js");
10
+ const index = require("./index-Bko3vlk0.js");
11
11
  const Avatar = require("./Avatar-CdT7eBCh.js");
12
- const useModelSchema = require("./useModelSchema-BogQah8f.js");
12
+ const useModelSchema = require("./useModelSchema-DON5XCju.js");
13
13
  function useUsers(options = {}) {
14
14
  const { limit = 25, offset = 0, search } = options;
15
15
  const { formatMessage } = reactIntl.useIntl();
@@ -377,7 +377,7 @@ const plugin = {
377
377
  id: `${PLUGIN_ID}.plugin.name`,
378
378
  defaultMessage: "Auth Dashboard"
379
379
  },
380
- Component: async () => import("./Root-CgVBJuoM.mjs"),
380
+ Component: async () => import("./Root-C-dQhq8o.mjs"),
381
381
  permissions: [
382
382
  ...PERMISSIONS.overview,
383
383
  ...PERMISSIONS.user,
@@ -401,7 +401,7 @@ const plugin = {
401
401
  defaultMessage: "Overview"
402
402
  },
403
403
  to: "/overview",
404
- Component: () => import("./index-DvfwvCj0.mjs"),
404
+ Component: () => import("./index-B5jiojjV.mjs"),
405
405
  permissions: PERMISSIONS.overview
406
406
  });
407
407
  dash.addDashLink("general", {
@@ -411,7 +411,7 @@ const plugin = {
411
411
  defaultMessage: "User Management"
412
412
  },
413
413
  to: "/users",
414
- Component: () => import("./index-gFqHlMLP.mjs"),
414
+ Component: () => import("./index-CvNwHg1k.mjs"),
415
415
  permissions: PERMISSIONS.user
416
416
  });
417
417
  dash.addDashLink("general", {
@@ -421,7 +421,7 @@ const plugin = {
421
421
  defaultMessage: "Organization Management"
422
422
  },
423
423
  to: "/organizations",
424
- Component: () => import("./index-VyHXoQ6p.mjs"),
424
+ Component: () => import("./index-NF35nlqQ.mjs"),
425
425
  permissions: PERMISSIONS.organization,
426
426
  isAvailable: async () => hasPlugin(await getDashConfig(), "organization")
427
427
  });
@@ -5,9 +5,9 @@ import { useNotification, useFetchClient, Page, useRBAC, Layouts } from "@strapi
5
5
  import { useState } from "react";
6
6
  import { useIntl } from "react-intl";
7
7
  import { useQuery, useQueryClient, useMutation } from "react-query";
8
- import { d as client, g as getTranslation, e as getAuthHeaders, f as useOrganizationOptions, c as PERMISSIONS, P as PLUGIN_ID } from "./index-BI2WIsds.mjs";
8
+ import { d as client, g as getTranslation, e as getAuthHeaders, f as useOrganizationOptions, c as PERMISSIONS, P as PLUGIN_ID } from "./index-DAkkUU0o.mjs";
9
9
  import { A as Avatar } from "./Avatar-zlMTAZh3.mjs";
10
- import { w as withContext, S as SheetDialog, F as FormSection, a as SectionLabel, M as MediaPickerField, u as useModelSchema, E as EditLayout, C as CustomFieldsSection, b as EditSidebar, c as MetaItem, d as MetaKey, e as MonoChip, f as MetaVal, g as EditViewSidePanels, A as AccountRow, P as ProviderBadge, i as ConfirmDialog, j as Code } from "./useModelSchema-C6bxNjW4.mjs";
10
+ import { w as withContext, S as SheetDialog, F as FormSection, a as SectionLabel, M as MediaPickerField, u as useModelSchema, E as EditLayout, C as CustomFieldsSection, b as EditSidebar, c as MetaItem, d as MetaKey, e as MonoChip, f as MetaVal, g as EditViewSidePanels, A as AccountRow, P as ProviderBadge, i as ConfirmDialog, j as Code } from "./useModelSchema-Bo8WEBwb.mjs";
11
11
  function UserCombobox({
12
12
  label,
13
13
  hint,
@@ -465,10 +465,12 @@ function OrganizationDetail({
465
465
  });
466
466
  const strapiOrgQuery = useQuery({
467
467
  queryKey: ["dash-strapi-org", organizationId],
468
- enabled: !!orgQuery.data,
468
+ enabled: !!schemaQuery.data,
469
469
  queryFn: async () => {
470
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
471
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
470
472
  const { data } = await get(
471
- `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
473
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1${populateParam}`
472
474
  );
473
475
  return data.results?.[0] ?? null;
474
476
  }
@@ -7,7 +7,7 @@ const react = require("react");
7
7
  const reactIntl = require("react-intl");
8
8
  const reactQuery = require("react-query");
9
9
  const styled = require("styled-components");
10
- const index = require("./index-BCBu-0nr.js");
10
+ const index = require("./index-Bko3vlk0.js");
11
11
  const Avatar = require("./Avatar-CdT7eBCh.js");
12
12
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
13
13
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const index = require("./index-BCBu-0nr.js");
3
+ const index = require("./index-Bko3vlk0.js");
4
4
  exports.default = index.plugin;
@@ -1,4 +1,4 @@
1
- import { p } from "./index-BI2WIsds.mjs";
1
+ import { p } from "./index-DAkkUU0o.mjs";
2
2
  export {
3
3
  p as default
4
4
  };
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { Dialog, Button, Typography, Flex, Grid, Box, Badge, Field, TextInput, Modal, Combobox, ComboboxOption, IconButton, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea } from "@strapi/design-system";
3
3
  import { WarningCircle, Clock, Images, Trash } from "@strapi/icons";
4
4
  import { useIntl } from "react-intl";
5
- import { g as getTranslation, i as getMediaLibraryComponent, j as getEditViewSidePanels } from "./index-BI2WIsds.mjs";
5
+ import { g as getTranslation, i as getMediaLibraryComponent, j as getEditViewSidePanels } from "./index-DAkkUU0o.mjs";
6
6
  import { useState, useRef } from "react";
7
7
  import styled, { keyframes } from "styled-components";
8
8
  import { useFetchClient } from "@strapi/strapi/admin";
@@ -3,7 +3,7 @@ const jsxRuntime = require("react/jsx-runtime");
3
3
  const designSystem = require("@strapi/design-system");
4
4
  const icons = require("@strapi/icons");
5
5
  const reactIntl = require("react-intl");
6
- const index = require("./index-BCBu-0nr.js");
6
+ const index = require("./index-Bko3vlk0.js");
7
7
  const react = require("react");
8
8
  const styled = require("styled-components");
9
9
  const admin = require("@strapi/strapi/admin");