diff --git a/apps/docs/docs/better-auth/dashboard.md b/apps/docs/docs/better-auth/dashboard.md index ba16c83..13760cb 100644 --- a/apps/docs/docs/better-auth/dashboard.md +++ b/apps/docs/docs/better-auth/dashboard.md @@ -59,6 +59,29 @@ export const auth = betterAuth({ }); ``` +To enable the "Send verification email" and "Send password reset" actions in the user detail drawer, configure the dashboard plugin itself in `config/plugins.js`/`.ts`: + +```typescript title="config/plugins.ts" +export default { + "better-auth-dashboard": { + config: { + // Absolute URL of your public-facing client app. Used as the + // callback destination for email verification / password reset + // links sent from the dashboard. Unset by default — the two + // actions above stay disabled until this is configured, rather + // than guessing at a URL (e.g. the admin panel's own origin). + email_callback_url: process.env.BETTER_AUTH_DASHBOARD_CALLBACK_URL, + }, + }, +}; +``` + +#### Config reference + +| Property | Type | Required | Description | +|---|---|---|---| +| `email_callback_url` | `string` | — | Absolute URL of your public-facing client app. Used as the callback destination for email verification and password reset links sent from the user detail drawer. Not set by default, which keeps those two actions disabled rather than guessing at a URL. | + ### Start Strapi ```bash diff --git a/plugins/plugin-better-auth-dashboard/README.md b/plugins/plugin-better-auth-dashboard/README.md index 812c093..34bd4bf 100644 --- a/plugins/plugin-better-auth-dashboard/README.md +++ b/plugins/plugin-better-auth-dashboard/README.md @@ -64,6 +64,23 @@ export const auth = betterAuth({ }); ``` +To enable the "Send verification email" and "Send password reset" actions in the user detail drawer, configure the dashboard plugin itself in `config/plugins.js`/`.ts`: + +```typescript title="config/plugins.ts" +export default { + "better-auth-dashboard": { + config: { + // Absolute URL of your public-facing client app. Used as the + // callback destination for email verification / password reset + // links sent from the dashboard. Unset by default — the two + // actions above stay disabled until this is configured, rather + // than guessing at a URL (e.g. the admin panel's own origin). + email_callback_url: process.env.BETTER_AUTH_DASHBOARD_CALLBACK_URL, + }, + }, +}; +``` + ### Start Strapi ```bash diff --git a/plugins/plugin-better-auth-dashboard/admin/src/hooks/usePluginSettings.ts b/plugins/plugin-better-auth-dashboard/admin/src/hooks/usePluginSettings.ts new file mode 100644 index 0000000..38f98b3 --- /dev/null +++ b/plugins/plugin-better-auth-dashboard/admin/src/hooks/usePluginSettings.ts @@ -0,0 +1,26 @@ +import { useFetchClient } from "@strapi/strapi/admin"; +import { useQuery } from "react-query"; + +export interface PluginSettings { + /** + * Absolute URL of the public-facing client app. Used as the callback + * destination for email verification / password reset links sent from + * the dashboard, instead of falling back to the admin panel URL. + */ + email_callback_url?: string; +} + +export function usePluginSettings() { + const { get } = useFetchClient(); + + return useQuery({ + queryKey: ["dash-plugin-settings"], + queryFn: async () => { + const { data } = await get( + "/better-auth-dashboard/settings", + ); + return data; + }, + staleTime: 5 * 60 * 1000, + }); +} diff --git a/plugins/plugin-better-auth-dashboard/admin/src/pages/Users/UserDetailDrawer.tsx b/plugins/plugin-better-auth-dashboard/admin/src/pages/Users/UserDetailDrawer.tsx index eb4e433..3945872 100644 --- a/plugins/plugin-better-auth-dashboard/admin/src/pages/Users/UserDetailDrawer.tsx +++ b/plugins/plugin-better-auth-dashboard/admin/src/pages/Users/UserDetailDrawer.tsx @@ -44,6 +44,7 @@ import { } from "../../components/FormPrimitives"; import { MediaPickerField } from "../../components/MediaPickerField"; import { useModelSchema } from "../../hooks/useModelSchema"; +import { usePluginSettings } from "../../hooks/usePluginSettings"; import { withContext } from "../../utils/dashContext"; // ─── 2FA styled components ──────────────────────────────────────────────────── @@ -182,6 +183,14 @@ export function UserDetailDrawer({ const { toggleNotification } = useNotification(); const { get, put } = useFetchClient(); const schemaQuery = useModelSchema("user"); + const settingsQuery = usePluginSettings(); + + // Where email verification / password reset links should redirect back to + // once the user completes the flow. Only the configured + // `email_callback_url` server setting is used — deliberately no fallback + // to the current (admin panel) origin, since guessing a client URL is + // exactly what caused these links to point at the admin panel before. + const emailCallbackUrl = settingsQuery.data?.email_callback_url ?? null; const userQuery = useQuery({ queryKey: ["dash-user", userId], @@ -498,9 +507,13 @@ export function UserDetailDrawer({ const sendVerificationMutation = useMutation({ mutationFn: async () => { - const callbackUrl = new URL(window.location.href, window.location.origin); + if (!emailCallbackUrl) { + throw new Error( + "Set `email_callback_url` in the better-auth-dashboard plugin config to enable this action.", + ); + } const result = await client.dash.sendVerificationEmail( - { callbackUrl: callbackUrl.toString() }, + { callbackUrl: emailCallbackUrl }, withContext({ userId }, getAuthHeaders()), ); if (result.error) throw new Error(result.error.message ?? "Failed"); @@ -522,9 +535,13 @@ export function UserDetailDrawer({ const sendResetPasswordMutation = useMutation({ mutationFn: async () => { - const callbackUrl = new URL(window.location.href, window.location.origin); + if (!emailCallbackUrl) { + throw new Error( + "Set `email_callback_url` in the better-auth-dashboard plugin config to enable this action.", + ); + } const result = await client.dash.sendResetPasswordEmail( - { callbackUrl: callbackUrl.toString() }, + { callbackUrl: emailCallbackUrl }, withContext({ userId }, getAuthHeaders()), ); if (result.error) throw new Error(result.error.message ?? "Failed"); @@ -874,7 +891,7 @@ export function UserDetailDrawer({ variant="secondary" size="S" loading={sendVerificationMutation.isLoading} - disabled={user?.emailVerified} + disabled={!emailCallbackUrl || user?.emailVerified} onClick={() => sendVerificationMutation.mutate()} style={{ width: "100%" }} > @@ -884,11 +901,18 @@ export function UserDetailDrawer({ variant="secondary" size="S" loading={sendResetPasswordMutation.isLoading} + disabled={!emailCallbackUrl} onClick={() => sendResetPasswordMutation.mutate()} style={{ width: "100%" }} > Send password reset + {!settingsQuery.isLoading && !emailCallbackUrl && ( + + Set `email_callback_url` in the better-auth-dashboard + plugin config to enable these actions. + + )} )} diff --git a/plugins/plugin-better-auth-dashboard/server/src/config.ts b/plugins/plugin-better-auth-dashboard/server/src/config.ts new file mode 100644 index 0000000..2c476e6 --- /dev/null +++ b/plugins/plugin-better-auth-dashboard/server/src/config.ts @@ -0,0 +1,39 @@ +export interface Config { + /** + * Absolute URL of the public-facing client app that email links (email + * verification, password reset) generated from the dashboard should + * redirect back to once the user completes the flow. + * + * The Better Auth server has no knowledge of the client URL on its own + * (aside from `trustedOrigins`), so without this the dashboard would have + * to guess — previously it fell back to the current admin panel URL, + * which is wrong for end users. Set this to your frontend's URL, e.g. + * `https://app.example.com/login`. + */ + email_callback_url?: string; +} + +const config: { + default: Config; + validator: (config: Config) => void; +} = { + default: {}, + validator(config) { + if (config.email_callback_url === undefined) return; + + if ( + typeof config.email_callback_url !== "string" || + config.email_callback_url.length === 0 + ) { + throw new Error("email_callback_url must be a non-empty string"); + } + + try { + new URL(config.email_callback_url); + } catch { + throw new Error("email_callback_url must be a valid absolute URL"); + } + }, +}; + +export default config; diff --git a/plugins/plugin-better-auth-dashboard/server/src/controllers/index.ts b/plugins/plugin-better-auth-dashboard/server/src/controllers/index.ts index 5991099..387851e 100644 --- a/plugins/plugin-better-auth-dashboard/server/src/controllers/index.ts +++ b/plugins/plugin-better-auth-dashboard/server/src/controllers/index.ts @@ -1,7 +1,9 @@ import authController from "./auth-controller"; import dbController from "./db-controller"; +import settingsController from "./settings-controller"; export default { "auth-controller": authController, "db-controller": dbController, + "settings-controller": settingsController, }; diff --git a/plugins/plugin-better-auth-dashboard/server/src/controllers/settings-controller.ts b/plugins/plugin-better-auth-dashboard/server/src/controllers/settings-controller.ts new file mode 100644 index 0000000..078116b --- /dev/null +++ b/plugins/plugin-better-auth-dashboard/server/src/controllers/settings-controller.ts @@ -0,0 +1,18 @@ +import type { Context } from "koa"; +import type { Config } from "../config"; +import { PLUGIN_ID } from "../utils"; + +/** + * Serves this plugin's own server config to the admin panel. + * + * Kept separate from the Better Auth `dash()` config (proxied at + * `/auth/dash/config`) since these are settings for the dashboard plugin + * itself, not something the Better Auth server knows about. + */ +const settingsController = () => ({ + async get(ctx: Context) { + ctx.body = strapi.config.get(`plugin::${PLUGIN_ID}`, {}); + }, +}); + +export default settingsController; diff --git a/plugins/plugin-better-auth-dashboard/server/src/index.ts b/plugins/plugin-better-auth-dashboard/server/src/index.ts index a35a3b1..4290156 100644 --- a/plugins/plugin-better-auth-dashboard/server/src/index.ts +++ b/plugins/plugin-better-auth-dashboard/server/src/index.ts @@ -1,3 +1,4 @@ +import config from "./config"; import bootstrap from "./bootstrap"; import controllers from "./controllers"; import policies from "./policies"; @@ -5,6 +6,7 @@ import { register } from "./register"; import routes from "./routes"; export default { + config, controllers, routes, policies, diff --git a/plugins/plugin-better-auth-dashboard/server/src/routes/admin/index.ts b/plugins/plugin-better-auth-dashboard/server/src/routes/admin/index.ts index 5b0433b..e06bb3c 100644 --- a/plugins/plugin-better-auth-dashboard/server/src/routes/admin/index.ts +++ b/plugins/plugin-better-auth-dashboard/server/src/routes/admin/index.ts @@ -64,5 +64,14 @@ export default () => ({ prefix: "", }, }, + { + method: "GET", + path: "/better-auth-dashboard/settings", + handler: "settings-controller.get", + config: { + policies: ["has-permission"], + prefix: "", + }, + }, ], });