From d4f59aed8fca50508d87e2b4e6a907bcdf520f49 Mon Sep 17 00:00:00 2001 From: Clement James Date: Fri, 19 Jun 2026 09:48:27 +0100 Subject: [PATCH] feat: add customizable title and description to SignInForm component; update password reset event handling and include app URL in notification data --- plugins/notification/plugin.go | 6 ++++++ service.go | 7 ++++++- .../src/components/sign-in-form.tsx | 20 +++++++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/plugins/notification/plugin.go b/plugins/notification/plugin.go index 4cc9ba9f..b0e9fade 100644 --- a/plugins/notification/plugin.go +++ b/plugins/notification/plugin.go @@ -357,6 +357,12 @@ func (p *Plugin) handleHookEvent(ctx context.Context, event *hook.Event) error { data[k] = v } data["app_name"] = p.config.AppName + // app_url is the application root, available to every template for generic + // "open the app" call-to-action buttons (templates whose own variables don't + // include a specific link, e.g. email-verified, welcome, password-changed). + if p.config.BaseURL != "" { + data["app_url"] = strings.TrimRight(p.config.BaseURL, "/") + } // The default Herald auth.email-verification template marks verify_url as a // required variable — it predates the OTP flow, which delivers a 6-digit diff --git a/service.go b/service.go index 19768d20..89fbfccb 100644 --- a/service.go +++ b/service.go @@ -1077,8 +1077,13 @@ func (e *Engine) ResetPassword(ctx context.Context, token, newPassword string) e e.audit(ctx, bridge.SeverityInfo, bridge.OutcomeSuccess, "reset_password", "user", u.ID.String(), u.ID.String(), pr.AppID.String(), "auth", nil) + // A completed reset means the password changed — emit ActionPasswordChange + // (the "your password was changed" confirmation), NOT ActionPasswordReset. + // ActionPasswordReset is the reset-*requested* event (it sends the "Reset + // your password" link email); emitting it here re-sent that link email + // after the user had already reset their password. e.hooks.Emit(ctx, &hook.Event{ - Action: hook.ActionPasswordReset, + Action: hook.ActionPasswordChange, Resource: hook.ResourceUser, ResourceID: u.ID.String(), ActorID: u.ID.String(), diff --git a/ui/packages/components/src/components/sign-in-form.tsx b/ui/packages/components/src/components/sign-in-form.tsx index 7cdde61f..fb609236 100644 --- a/ui/packages/components/src/components/sign-in-form.tsx +++ b/ui/packages/components/src/components/sign-in-form.tsx @@ -56,6 +56,16 @@ export interface SignInFormComponentProps { variant?: AuthCardVariant; /** Additional CSS class names. */ className?: string; + /** + * Heading shown on the initial sign-in view (email / passwordless steps). + * Defaults to "Sign in". Use it to brand the form, e.g. "Sign in to Acme". + */ + title?: string; + /** + * Subtitle shown under the heading on the initial sign-in view. + * Defaults to "Welcome back. Sign in to your account." + */ + description?: string; } /** @@ -81,6 +91,8 @@ export function SignInForm({ align, variant, className, + title = "Sign in", + description = "Welcome back. Sign in to your account.", }: SignInFormComponentProps) { const { signIn, client, resendVerification } = useAuth(); const { config } = useClientConfig(); @@ -221,8 +233,8 @@ export function SignInForm({ const hasAnyMethod = hasSocial || showPasskey; return (