Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/docs/docs/better-auth/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions plugins/plugin-better-auth-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PluginSettings, Error>({
queryKey: ["dash-plugin-settings"],
queryFn: async () => {
const { data } = await get<PluginSettings>(
"/better-auth-dashboard/settings",
);
return data;
},
staleTime: 5 * 60 * 1000,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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%" }}
>
Expand All @@ -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
</Button>
{!settingsQuery.isLoading && !emailCallbackUrl && (
<Typography variant="pi" textColor="neutral500">
Set `email_callback_url` in the better-auth-dashboard
plugin config to enable these actions.
</Typography>
)}
</Flex>
</Box>
)}
Expand Down
39 changes: 39 additions & 0 deletions plugins/plugin-better-auth-dashboard/server/src/config.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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<Config>(`plugin::${PLUGIN_ID}`, {});
},
});

export default settingsController;
2 changes: 2 additions & 0 deletions plugins/plugin-better-auth-dashboard/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import config from "./config";
import bootstrap from "./bootstrap";
import controllers from "./controllers";
import policies from "./policies";
import { register } from "./register";
import routes from "./routes";

export default {
config,
controllers,
routes,
policies,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,14 @@ export default () => ({
prefix: "",
},
},
{
method: "GET",
path: "/better-auth-dashboard/settings",
handler: "settings-controller.get",
config: {
policies: ["has-permission"],
prefix: "",
},
},
],
});
Loading