@forjio/auth-ui 0.4.1 → 0.5.0

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/dist/AuthForm.cjs CHANGED
@@ -37,7 +37,9 @@ var import_react = require("react");
37
37
  var import_navigation = require("next/navigation");
38
38
  var import_link = __toESM(require("next/link"), 1);
39
39
  var import_lucide_react = require("lucide-react");
40
+ var import_react_turnstile = require("@marsidev/react-turnstile");
40
41
  var import_types = require("./types");
42
+ const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
41
43
  function AuthForm({
42
44
  mode,
43
45
  brand,
@@ -61,6 +63,7 @@ function AuthForm({
61
63
  const [name, setName] = (0, import_react.useState)("");
62
64
  const [submitting, setSubmitting] = (0, import_react.useState)(false);
63
65
  const [redirecting, setRedirecting] = (0, import_react.useState)(false);
66
+ const [turnstileToken, setTurnstileToken] = (0, import_react.useState)("");
64
67
  const [error, setError] = (0, import_react.useState)(
65
68
  ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null
66
69
  );
@@ -72,6 +75,7 @@ function AuthForm({
72
75
  const path = mode === "signup" ? ep.signup : ep.login;
73
76
  const body = { email, password, ...extraBody };
74
77
  if (mode === "signup" && name.trim()) body.name = name.trim();
78
+ if (TURNSTILE_SITE_KEY) body["cf-turnstile-response"] = turnstileToken;
75
79
  const res = await fetch(path, {
76
80
  method: "POST",
77
81
  headers: { "Content-Type": "application/json" },
@@ -82,7 +86,8 @@ function AuthForm({
82
86
  const payload = await res.json().catch(() => null);
83
87
  if (payload?.error?.code === "MFA_REQUIRED") {
84
88
  setRedirecting(true);
85
- window.location.href = `${ep.socialStart}?return_to=${encodeURIComponent(returnTo)}`;
89
+ const qs = new URLSearchParams({ return_to: returnTo, ...socialParams });
90
+ window.location.href = `${ep.socialStart}?${qs.toString()}`;
86
91
  return;
87
92
  }
88
93
  throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);
@@ -203,6 +208,15 @@ function AuthForm({
203
208
  }
204
209
  )
205
210
  ] }),
211
+ TURNSTILE_SITE_KEY && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-center py-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
212
+ import_react_turnstile.Turnstile,
213
+ {
214
+ siteKey: TURNSTILE_SITE_KEY,
215
+ onSuccess: setTurnstileToken,
216
+ onError: () => setError("Security check failed."),
217
+ options: { theme: "auto" }
218
+ }
219
+ ) }),
206
220
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
207
221
  "button",
208
222
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/AuthForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle } from 'lucide-react';\nimport { defaultEndpoints, type AuthEndpoints, type SocialProviders } from './types';\n\nexport interface AuthFormProps {\n mode: 'login' | 'signup';\n /** Display name shown in copy (\"New to Plugipay?\", \"Welcome back\"). */\n brand: string;\n /** Override the auth endpoint paths. Default matches Forjio family\n * `@forjio/sdk/auth-handlers` mounts. */\n endpoints?: Partial<AuthEndpoints>;\n /** Which social providers to render. Host fetches the provider\n * status; pass undefined to show all (fail-open). */\n providers?: SocialProviders | null;\n /** Default redirect target after a successful auth. Default\n * `/dashboard`. Search-param `?return_to=` overrides at runtime. */\n defaultReturnTo?: string;\n /** Mode-switch link targets. Default `/login` / `/signup`. Override\n * for products with non-default auth routes (e.g. role-scoped\n * `/creators/login` + `/creators/onboarding`). `?return_to=` is\n * appended automatically. */\n loginHref?: string;\n signupHref?: string;\n /** \"Forgot password?\" link target. Default `/forgot-password`. */\n forgotPasswordHref?: string;\n /** Extra fields merged into the login/signup request body — e.g. a\n * `role` discriminator for multi-role products. */\n extraBody?: Record<string, unknown>;\n /** Extra query params appended to the social-start URL — e.g.\n * `{ role }`, which the Huudis OIDC start needs to mint the correct\n * per-role session on callback. */\n socialParams?: Record<string, string>;\n}\n\nexport function AuthForm({\n mode,\n brand,\n endpoints,\n providers,\n defaultReturnTo = '/dashboard',\n loginHref = '/login',\n signupHref = '/signup',\n forgotPasswordHref = '/forgot-password',\n extraBody,\n socialParams,\n}: AuthFormProps) {\n const router = useRouter();\n const params = useSearchParams();\n const returnTo = params?.get('return_to') || defaultReturnTo;\n const ssoError = params?.get('sso_error');\n const ssoDetail = params?.get('sso_detail');\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [name, setName] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [redirecting, setRedirecting] = useState(false);\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const path = mode === 'signup' ? ep.signup : ep.login;\n const body: Record<string, unknown> = { email, password, ...extraBody };\n if (mode === 'signup' && name.trim()) body.name = name.trim();\n const res = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as {\n error?: { code?: string; message?: string };\n } | null;\n // The product BFF does not do inline MFA. When a user has MFA\n // enabled, Huudis ROPC fails and the SDK returns 401 with\n // `code: 'MFA_REQUIRED'`. Hand off to the Huudis hosted-login\n // flow (no `provider=` param) — Huudis performs the challenge.\n if (payload?.error?.code === 'MFA_REQUIRED') {\n setRedirecting(true);\n window.location.href = `${ep.socialStart}?return_to=${encodeURIComponent(returnTo)}`;\n return;\n }\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n router.push(returnTo);\n router.refresh();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n const otherMode = mode === 'login' ? 'signup' : 'login';\n const otherHref = `${otherMode === 'signup' ? signupHref : loginHref}?return_to=${encodeURIComponent(returnTo)}`;\n const socialUrl = (provider: 'google' | 'apple' | 'facebook') => {\n const qs = new URLSearchParams({ provider, return_to: returnTo, ...socialParams });\n return `${ep.socialStart}?${qs.toString()}`;\n };\n\n const showGoogle = providers?.google !== false;\n const showApple = providers?.apple !== false;\n const showFacebook = providers?.facebook !== false;\n const hasAnySocial = showGoogle || showApple || showFacebook;\n\n return (\n <div className=\"space-y-4\">\n {error && !redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n\n {redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-border bg-accent px-3 py-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 shrink-0 mt-0.5 animate-spin\" />\n <span>Redirecting you to complete two-factor sign-in…</span>\n </div>\n )}\n\n {hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <a\n href={socialUrl('google')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n )}\n {showApple && (\n <a\n href={socialUrl('apple')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n )}\n {showFacebook && (\n <a\n href={socialUrl('facebook')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n )}\n </div>\n\n <div className=\"my-4 flex items-center gap-3 text-[11px] text-muted-foreground\">\n <div className=\"flex-1 border-t border-border\" />\n OR\n <div className=\"flex-1 border-t border-border\" />\n </div>\n </>\n )}\n\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Password</label>\n <input\n type=\"password\"\n required\n minLength={mode === 'signup' ? 10 : undefined}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {mode === 'signup' && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">At least 10 characters, with a letter and a number.</p>\n )}\n </div>\n {mode === 'signup' && (\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n Your name <span className=\"text-muted-foreground/60\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n autoComplete=\"name\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {(submitting || redirecting) && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {redirecting\n ? 'Redirecting…'\n : submitting\n ? mode === 'signup'\n ? 'Creating…'\n : 'Signing in…'\n : mode === 'signup'\n ? 'Create account'\n : 'Sign in'}\n </button>\n </form>\n\n <div className=\"flex items-center justify-between pt-2 text-xs text-muted-foreground\">\n {mode === 'login' && (\n <Link href={forgotPasswordHref} className=\"hover:text-foreground\">\n Forgot password?\n </Link>\n )}\n <span className={mode === 'login' ? '' : 'ml-auto'}>\n {mode === 'login' ? `New to ${brand}?` : 'Already have an account?'}{' '}\n <Link href={otherHref} className=\"font-medium text-foreground hover:underline\">\n {mode === 'login' ? 'Sign up' : 'Sign in'}\n </Link>\n </span>\n </div>\n <p className=\"pt-2 text-[11px] leading-relaxed text-muted-foreground/80\">\n Identity is powered by{' '}\n <a href=\"https://huudis.com\" className=\"underline hover:text-foreground\">\n Huudis\n </a>\n . One account for every Forjio product.\n </p>\n </div>\n );\n}\n\nfunction GoogleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z\" fill=\"#4285F4\" />\n <path d=\"M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z\" fill=\"#34A853\" />\n <path d=\"M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z\" fill=\"#FBBC05\" />\n <path d=\"M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z\" fill=\"#EA4335\" />\n </svg>\n );\n}\n\nfunction FacebookMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#1877F2\" aria-hidden=\"true\">\n <path d=\"M24 12.073C24 5.404 18.627 0 12 0S0 5.404 0 12.073c0 6.026 4.388 11.022 10.125 11.927v-8.437H7.078v-3.49h3.047V9.41c0-3.026 1.792-4.697 4.533-4.697 1.313 0 2.686.236 2.686.236v2.971H15.83c-1.49 0-1.955.93-1.955 1.886v2.266h3.328l-.532 3.49h-2.796v8.437C19.612 23.095 24 18.099 24 12.073Z\" />\n </svg>\n );\n}\n\nfunction AppleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z\" />\n </svg>\n );\n}\n"],"mappings":"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","names":["Link"]}
1
+ {"version":3,"sources":["../src/AuthForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle } from 'lucide-react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport { defaultEndpoints, type AuthEndpoints, type SocialProviders } from './types';\n\n// Cloudflare Turnstile is enabled family-wide by setting\n// NEXT_PUBLIC_TURNSTILE_SITE_KEY at build time. Next inlines NEXT_PUBLIC_*\n// referenced here (in node_modules) into each product's bundle, so no\n// per-product page edit is needed — set the env var + the widget appears,\n// and the token rides the login/signup request as `cf-turnstile-response`.\n// The backend (@forjio/sdk/auth-server) verifies it when\n// TURNSTILE_SECRET_KEY is set; both sides bypass gracefully when unset.\nconst TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;\n\nexport interface AuthFormProps {\n mode: 'login' | 'signup';\n /** Display name shown in copy (\"New to Plugipay?\", \"Welcome back\"). */\n brand: string;\n /** Override the auth endpoint paths. Default matches Forjio family\n * `@forjio/sdk/auth-handlers` mounts. */\n endpoints?: Partial<AuthEndpoints>;\n /** Which social providers to render. Host fetches the provider\n * status; pass undefined to show all (fail-open). */\n providers?: SocialProviders | null;\n /** Default redirect target after a successful auth. Default\n * `/dashboard`. Search-param `?return_to=` overrides at runtime. */\n defaultReturnTo?: string;\n /** Mode-switch link targets. Default `/login` / `/signup`. Override\n * for products with non-default auth routes (e.g. role-scoped\n * `/creators/login` + `/creators/onboarding`). `?return_to=` is\n * appended automatically. */\n loginHref?: string;\n signupHref?: string;\n /** \"Forgot password?\" link target. Default `/forgot-password`. */\n forgotPasswordHref?: string;\n /** Extra fields merged into the login/signup request body — e.g. a\n * `role` discriminator for multi-role products. */\n extraBody?: Record<string, unknown>;\n /** Extra query params appended to the social-start URL — e.g.\n * `{ role }`, which the Huudis OIDC start needs to mint the correct\n * per-role session on callback. */\n socialParams?: Record<string, string>;\n}\n\nexport function AuthForm({\n mode,\n brand,\n endpoints,\n providers,\n defaultReturnTo = '/dashboard',\n loginHref = '/login',\n signupHref = '/signup',\n forgotPasswordHref = '/forgot-password',\n extraBody,\n socialParams,\n}: AuthFormProps) {\n const router = useRouter();\n const params = useSearchParams();\n const returnTo = params?.get('return_to') || defaultReturnTo;\n const ssoError = params?.get('sso_error');\n const ssoDetail = params?.get('sso_detail');\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [name, setName] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [redirecting, setRedirecting] = useState(false);\n const [turnstileToken, setTurnstileToken] = useState('');\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const path = mode === 'signup' ? ep.signup : ep.login;\n const body: Record<string, unknown> = { email, password, ...extraBody };\n if (mode === 'signup' && name.trim()) body.name = name.trim();\n if (TURNSTILE_SITE_KEY) body['cf-turnstile-response'] = turnstileToken;\n const res = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as {\n error?: { code?: string; message?: string };\n } | null;\n // The product BFF does not do inline MFA. When a user has MFA\n // enabled, Huudis ROPC fails and the SDK returns 401 with\n // `code: 'MFA_REQUIRED'`. Hand off to the Huudis hosted-login\n // flow (no `provider=` param) — Huudis performs the challenge.\n // `socialParams` (e.g. `{ role }`) MUST ride along so a\n // multi-role product mints the correct per-role session on the\n // OIDC callback — otherwise the role is lost and the user is\n // gated on the wrong portal.\n if (payload?.error?.code === 'MFA_REQUIRED') {\n setRedirecting(true);\n const qs = new URLSearchParams({ return_to: returnTo, ...socialParams });\n window.location.href = `${ep.socialStart}?${qs.toString()}`;\n return;\n }\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n router.push(returnTo);\n router.refresh();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n const otherMode = mode === 'login' ? 'signup' : 'login';\n const otherHref = `${otherMode === 'signup' ? signupHref : loginHref}?return_to=${encodeURIComponent(returnTo)}`;\n const socialUrl = (provider: 'google' | 'apple' | 'facebook') => {\n const qs = new URLSearchParams({ provider, return_to: returnTo, ...socialParams });\n return `${ep.socialStart}?${qs.toString()}`;\n };\n\n const showGoogle = providers?.google !== false;\n const showApple = providers?.apple !== false;\n const showFacebook = providers?.facebook !== false;\n const hasAnySocial = showGoogle || showApple || showFacebook;\n\n return (\n <div className=\"space-y-4\">\n {error && !redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n\n {redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-border bg-accent px-3 py-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 shrink-0 mt-0.5 animate-spin\" />\n <span>Redirecting you to complete two-factor sign-in…</span>\n </div>\n )}\n\n {hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <a\n href={socialUrl('google')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n )}\n {showApple && (\n <a\n href={socialUrl('apple')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n )}\n {showFacebook && (\n <a\n href={socialUrl('facebook')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n )}\n </div>\n\n <div className=\"my-4 flex items-center gap-3 text-[11px] text-muted-foreground\">\n <div className=\"flex-1 border-t border-border\" />\n OR\n <div className=\"flex-1 border-t border-border\" />\n </div>\n </>\n )}\n\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Password</label>\n <input\n type=\"password\"\n required\n minLength={mode === 'signup' ? 10 : undefined}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {mode === 'signup' && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">At least 10 characters, with a letter and a number.</p>\n )}\n </div>\n {mode === 'signup' && (\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n Your name <span className=\"text-muted-foreground/60\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n autoComplete=\"name\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n )}\n {TURNSTILE_SITE_KEY && (\n <div className=\"flex justify-center py-1\">\n <Turnstile\n siteKey={TURNSTILE_SITE_KEY}\n onSuccess={setTurnstileToken}\n onError={() => setError('Security check failed.')}\n options={{ theme: 'auto' }}\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {(submitting || redirecting) && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {redirecting\n ? 'Redirecting…'\n : submitting\n ? mode === 'signup'\n ? 'Creating…'\n : 'Signing in…'\n : mode === 'signup'\n ? 'Create account'\n : 'Sign in'}\n </button>\n </form>\n\n <div className=\"flex items-center justify-between pt-2 text-xs text-muted-foreground\">\n {mode === 'login' && (\n <Link href={forgotPasswordHref} className=\"hover:text-foreground\">\n Forgot password?\n </Link>\n )}\n <span className={mode === 'login' ? '' : 'ml-auto'}>\n {mode === 'login' ? `New to ${brand}?` : 'Already have an account?'}{' '}\n <Link href={otherHref} className=\"font-medium text-foreground hover:underline\">\n {mode === 'login' ? 'Sign up' : 'Sign in'}\n </Link>\n </span>\n </div>\n <p className=\"pt-2 text-[11px] leading-relaxed text-muted-foreground/80\">\n Identity is powered by{' '}\n <a href=\"https://huudis.com\" className=\"underline hover:text-foreground\">\n Huudis\n </a>\n . One account for every Forjio product.\n </p>\n </div>\n );\n}\n\nfunction GoogleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z\" fill=\"#4285F4\" />\n <path d=\"M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z\" fill=\"#34A853\" />\n <path d=\"M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z\" fill=\"#FBBC05\" />\n <path d=\"M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z\" fill=\"#EA4335\" />\n </svg>\n );\n}\n\nfunction FacebookMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#1877F2\" aria-hidden=\"true\">\n <path d=\"M24 12.073C24 5.404 18.627 0 12 0S0 5.404 0 12.073c0 6.026 4.388 11.022 10.125 11.927v-8.437H7.078v-3.49h3.047V9.41c0-3.026 1.792-4.697 4.533-4.697 1.313 0 2.686.236 2.686.236v2.971H15.83c-1.49 0-1.955.93-1.955 1.886v2.266h3.328l-.532 3.49h-2.796v8.437C19.612 23.095 24 18.099 24 12.073Z\" />\n </svg>\n );\n}\n\nfunction AppleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z\" />\n </svg>\n );\n}\n"],"mappings":"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","names":["Link"]}
package/dist/AuthForm.js CHANGED
@@ -4,7 +4,9 @@ import { useState } from "react";
4
4
  import { useRouter, useSearchParams } from "next/navigation";
5
5
  import Link from "next/link";
6
6
  import { Loader2, AlertCircle } from "lucide-react";
7
+ import { Turnstile } from "@marsidev/react-turnstile";
7
8
  import { defaultEndpoints } from "./types";
9
+ const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
8
10
  function AuthForm({
9
11
  mode,
10
12
  brand,
@@ -28,6 +30,7 @@ function AuthForm({
28
30
  const [name, setName] = useState("");
29
31
  const [submitting, setSubmitting] = useState(false);
30
32
  const [redirecting, setRedirecting] = useState(false);
33
+ const [turnstileToken, setTurnstileToken] = useState("");
31
34
  const [error, setError] = useState(
32
35
  ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null
33
36
  );
@@ -39,6 +42,7 @@ function AuthForm({
39
42
  const path = mode === "signup" ? ep.signup : ep.login;
40
43
  const body = { email, password, ...extraBody };
41
44
  if (mode === "signup" && name.trim()) body.name = name.trim();
45
+ if (TURNSTILE_SITE_KEY) body["cf-turnstile-response"] = turnstileToken;
42
46
  const res = await fetch(path, {
43
47
  method: "POST",
44
48
  headers: { "Content-Type": "application/json" },
@@ -49,7 +53,8 @@ function AuthForm({
49
53
  const payload = await res.json().catch(() => null);
50
54
  if (payload?.error?.code === "MFA_REQUIRED") {
51
55
  setRedirecting(true);
52
- window.location.href = `${ep.socialStart}?return_to=${encodeURIComponent(returnTo)}`;
56
+ const qs = new URLSearchParams({ return_to: returnTo, ...socialParams });
57
+ window.location.href = `${ep.socialStart}?${qs.toString()}`;
53
58
  return;
54
59
  }
55
60
  throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);
@@ -170,6 +175,15 @@ function AuthForm({
170
175
  }
171
176
  )
172
177
  ] }),
178
+ TURNSTILE_SITE_KEY && /* @__PURE__ */ jsx("div", { className: "flex justify-center py-1", children: /* @__PURE__ */ jsx(
179
+ Turnstile,
180
+ {
181
+ siteKey: TURNSTILE_SITE_KEY,
182
+ onSuccess: setTurnstileToken,
183
+ onError: () => setError("Security check failed."),
184
+ options: { theme: "auto" }
185
+ }
186
+ ) }),
173
187
  /* @__PURE__ */ jsxs(
174
188
  "button",
175
189
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/AuthForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle } from 'lucide-react';\nimport { defaultEndpoints, type AuthEndpoints, type SocialProviders } from './types';\n\nexport interface AuthFormProps {\n mode: 'login' | 'signup';\n /** Display name shown in copy (\"New to Plugipay?\", \"Welcome back\"). */\n brand: string;\n /** Override the auth endpoint paths. Default matches Forjio family\n * `@forjio/sdk/auth-handlers` mounts. */\n endpoints?: Partial<AuthEndpoints>;\n /** Which social providers to render. Host fetches the provider\n * status; pass undefined to show all (fail-open). */\n providers?: SocialProviders | null;\n /** Default redirect target after a successful auth. Default\n * `/dashboard`. Search-param `?return_to=` overrides at runtime. */\n defaultReturnTo?: string;\n /** Mode-switch link targets. Default `/login` / `/signup`. Override\n * for products with non-default auth routes (e.g. role-scoped\n * `/creators/login` + `/creators/onboarding`). `?return_to=` is\n * appended automatically. */\n loginHref?: string;\n signupHref?: string;\n /** \"Forgot password?\" link target. Default `/forgot-password`. */\n forgotPasswordHref?: string;\n /** Extra fields merged into the login/signup request body — e.g. a\n * `role` discriminator for multi-role products. */\n extraBody?: Record<string, unknown>;\n /** Extra query params appended to the social-start URL — e.g.\n * `{ role }`, which the Huudis OIDC start needs to mint the correct\n * per-role session on callback. */\n socialParams?: Record<string, string>;\n}\n\nexport function AuthForm({\n mode,\n brand,\n endpoints,\n providers,\n defaultReturnTo = '/dashboard',\n loginHref = '/login',\n signupHref = '/signup',\n forgotPasswordHref = '/forgot-password',\n extraBody,\n socialParams,\n}: AuthFormProps) {\n const router = useRouter();\n const params = useSearchParams();\n const returnTo = params?.get('return_to') || defaultReturnTo;\n const ssoError = params?.get('sso_error');\n const ssoDetail = params?.get('sso_detail');\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [name, setName] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [redirecting, setRedirecting] = useState(false);\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const path = mode === 'signup' ? ep.signup : ep.login;\n const body: Record<string, unknown> = { email, password, ...extraBody };\n if (mode === 'signup' && name.trim()) body.name = name.trim();\n const res = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as {\n error?: { code?: string; message?: string };\n } | null;\n // The product BFF does not do inline MFA. When a user has MFA\n // enabled, Huudis ROPC fails and the SDK returns 401 with\n // `code: 'MFA_REQUIRED'`. Hand off to the Huudis hosted-login\n // flow (no `provider=` param) — Huudis performs the challenge.\n if (payload?.error?.code === 'MFA_REQUIRED') {\n setRedirecting(true);\n window.location.href = `${ep.socialStart}?return_to=${encodeURIComponent(returnTo)}`;\n return;\n }\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n router.push(returnTo);\n router.refresh();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n const otherMode = mode === 'login' ? 'signup' : 'login';\n const otherHref = `${otherMode === 'signup' ? signupHref : loginHref}?return_to=${encodeURIComponent(returnTo)}`;\n const socialUrl = (provider: 'google' | 'apple' | 'facebook') => {\n const qs = new URLSearchParams({ provider, return_to: returnTo, ...socialParams });\n return `${ep.socialStart}?${qs.toString()}`;\n };\n\n const showGoogle = providers?.google !== false;\n const showApple = providers?.apple !== false;\n const showFacebook = providers?.facebook !== false;\n const hasAnySocial = showGoogle || showApple || showFacebook;\n\n return (\n <div className=\"space-y-4\">\n {error && !redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n\n {redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-border bg-accent px-3 py-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 shrink-0 mt-0.5 animate-spin\" />\n <span>Redirecting you to complete two-factor sign-in…</span>\n </div>\n )}\n\n {hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <a\n href={socialUrl('google')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n )}\n {showApple && (\n <a\n href={socialUrl('apple')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n )}\n {showFacebook && (\n <a\n href={socialUrl('facebook')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n )}\n </div>\n\n <div className=\"my-4 flex items-center gap-3 text-[11px] text-muted-foreground\">\n <div className=\"flex-1 border-t border-border\" />\n OR\n <div className=\"flex-1 border-t border-border\" />\n </div>\n </>\n )}\n\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Password</label>\n <input\n type=\"password\"\n required\n minLength={mode === 'signup' ? 10 : undefined}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {mode === 'signup' && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">At least 10 characters, with a letter and a number.</p>\n )}\n </div>\n {mode === 'signup' && (\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n Your name <span className=\"text-muted-foreground/60\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n autoComplete=\"name\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {(submitting || redirecting) && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {redirecting\n ? 'Redirecting…'\n : submitting\n ? mode === 'signup'\n ? 'Creating…'\n : 'Signing in…'\n : mode === 'signup'\n ? 'Create account'\n : 'Sign in'}\n </button>\n </form>\n\n <div className=\"flex items-center justify-between pt-2 text-xs text-muted-foreground\">\n {mode === 'login' && (\n <Link href={forgotPasswordHref} className=\"hover:text-foreground\">\n Forgot password?\n </Link>\n )}\n <span className={mode === 'login' ? '' : 'ml-auto'}>\n {mode === 'login' ? `New to ${brand}?` : 'Already have an account?'}{' '}\n <Link href={otherHref} className=\"font-medium text-foreground hover:underline\">\n {mode === 'login' ? 'Sign up' : 'Sign in'}\n </Link>\n </span>\n </div>\n <p className=\"pt-2 text-[11px] leading-relaxed text-muted-foreground/80\">\n Identity is powered by{' '}\n <a href=\"https://huudis.com\" className=\"underline hover:text-foreground\">\n Huudis\n </a>\n . One account for every Forjio product.\n </p>\n </div>\n );\n}\n\nfunction GoogleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z\" fill=\"#4285F4\" />\n <path d=\"M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z\" fill=\"#34A853\" />\n <path d=\"M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z\" fill=\"#FBBC05\" />\n <path d=\"M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z\" fill=\"#EA4335\" />\n </svg>\n );\n}\n\nfunction FacebookMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#1877F2\" aria-hidden=\"true\">\n <path d=\"M24 12.073C24 5.404 18.627 0 12 0S0 5.404 0 12.073c0 6.026 4.388 11.022 10.125 11.927v-8.437H7.078v-3.49h3.047V9.41c0-3.026 1.792-4.697 4.533-4.697 1.313 0 2.686.236 2.686.236v2.971H15.83c-1.49 0-1.955.93-1.955 1.886v2.266h3.328l-.532 3.49h-2.796v8.437C19.612 23.095 24 18.099 24 12.073Z\" />\n </svg>\n );\n}\n\nfunction AppleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z\" />\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/AuthForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle } from 'lucide-react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport { defaultEndpoints, type AuthEndpoints, type SocialProviders } from './types';\n\n// Cloudflare Turnstile is enabled family-wide by setting\n// NEXT_PUBLIC_TURNSTILE_SITE_KEY at build time. Next inlines NEXT_PUBLIC_*\n// referenced here (in node_modules) into each product's bundle, so no\n// per-product page edit is needed — set the env var + the widget appears,\n// and the token rides the login/signup request as `cf-turnstile-response`.\n// The backend (@forjio/sdk/auth-server) verifies it when\n// TURNSTILE_SECRET_KEY is set; both sides bypass gracefully when unset.\nconst TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;\n\nexport interface AuthFormProps {\n mode: 'login' | 'signup';\n /** Display name shown in copy (\"New to Plugipay?\", \"Welcome back\"). */\n brand: string;\n /** Override the auth endpoint paths. Default matches Forjio family\n * `@forjio/sdk/auth-handlers` mounts. */\n endpoints?: Partial<AuthEndpoints>;\n /** Which social providers to render. Host fetches the provider\n * status; pass undefined to show all (fail-open). */\n providers?: SocialProviders | null;\n /** Default redirect target after a successful auth. Default\n * `/dashboard`. Search-param `?return_to=` overrides at runtime. */\n defaultReturnTo?: string;\n /** Mode-switch link targets. Default `/login` / `/signup`. Override\n * for products with non-default auth routes (e.g. role-scoped\n * `/creators/login` + `/creators/onboarding`). `?return_to=` is\n * appended automatically. */\n loginHref?: string;\n signupHref?: string;\n /** \"Forgot password?\" link target. Default `/forgot-password`. */\n forgotPasswordHref?: string;\n /** Extra fields merged into the login/signup request body — e.g. a\n * `role` discriminator for multi-role products. */\n extraBody?: Record<string, unknown>;\n /** Extra query params appended to the social-start URL — e.g.\n * `{ role }`, which the Huudis OIDC start needs to mint the correct\n * per-role session on callback. */\n socialParams?: Record<string, string>;\n}\n\nexport function AuthForm({\n mode,\n brand,\n endpoints,\n providers,\n defaultReturnTo = '/dashboard',\n loginHref = '/login',\n signupHref = '/signup',\n forgotPasswordHref = '/forgot-password',\n extraBody,\n socialParams,\n}: AuthFormProps) {\n const router = useRouter();\n const params = useSearchParams();\n const returnTo = params?.get('return_to') || defaultReturnTo;\n const ssoError = params?.get('sso_error');\n const ssoDetail = params?.get('sso_detail');\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [name, setName] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [redirecting, setRedirecting] = useState(false);\n const [turnstileToken, setTurnstileToken] = useState('');\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const path = mode === 'signup' ? ep.signup : ep.login;\n const body: Record<string, unknown> = { email, password, ...extraBody };\n if (mode === 'signup' && name.trim()) body.name = name.trim();\n if (TURNSTILE_SITE_KEY) body['cf-turnstile-response'] = turnstileToken;\n const res = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify(body),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as {\n error?: { code?: string; message?: string };\n } | null;\n // The product BFF does not do inline MFA. When a user has MFA\n // enabled, Huudis ROPC fails and the SDK returns 401 with\n // `code: 'MFA_REQUIRED'`. Hand off to the Huudis hosted-login\n // flow (no `provider=` param) — Huudis performs the challenge.\n // `socialParams` (e.g. `{ role }`) MUST ride along so a\n // multi-role product mints the correct per-role session on the\n // OIDC callback — otherwise the role is lost and the user is\n // gated on the wrong portal.\n if (payload?.error?.code === 'MFA_REQUIRED') {\n setRedirecting(true);\n const qs = new URLSearchParams({ return_to: returnTo, ...socialParams });\n window.location.href = `${ep.socialStart}?${qs.toString()}`;\n return;\n }\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n router.push(returnTo);\n router.refresh();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n const otherMode = mode === 'login' ? 'signup' : 'login';\n const otherHref = `${otherMode === 'signup' ? signupHref : loginHref}?return_to=${encodeURIComponent(returnTo)}`;\n const socialUrl = (provider: 'google' | 'apple' | 'facebook') => {\n const qs = new URLSearchParams({ provider, return_to: returnTo, ...socialParams });\n return `${ep.socialStart}?${qs.toString()}`;\n };\n\n const showGoogle = providers?.google !== false;\n const showApple = providers?.apple !== false;\n const showFacebook = providers?.facebook !== false;\n const hasAnySocial = showGoogle || showApple || showFacebook;\n\n return (\n <div className=\"space-y-4\">\n {error && !redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n\n {redirecting && (\n <div className=\"flex items-start gap-2 rounded-md border border-border bg-accent px-3 py-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 shrink-0 mt-0.5 animate-spin\" />\n <span>Redirecting you to complete two-factor sign-in…</span>\n </div>\n )}\n\n {hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <a\n href={socialUrl('google')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n )}\n {showApple && (\n <a\n href={socialUrl('apple')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n )}\n {showFacebook && (\n <a\n href={socialUrl('facebook')}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-border bg-background py-2 text-sm font-medium hover:bg-accent\"\n >\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n )}\n </div>\n\n <div className=\"my-4 flex items-center gap-3 text-[11px] text-muted-foreground\">\n <div className=\"flex-1 border-t border-border\" />\n OR\n <div className=\"flex-1 border-t border-border\" />\n </div>\n </>\n )}\n\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Password</label>\n <input\n type=\"password\"\n required\n minLength={mode === 'signup' ? 10 : undefined}\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {mode === 'signup' && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">At least 10 characters, with a letter and a number.</p>\n )}\n </div>\n {mode === 'signup' && (\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">\n Your name <span className=\"text-muted-foreground/60\">(optional)</span>\n </label>\n <input\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n autoComplete=\"name\"\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n )}\n {TURNSTILE_SITE_KEY && (\n <div className=\"flex justify-center py-1\">\n <Turnstile\n siteKey={TURNSTILE_SITE_KEY}\n onSuccess={setTurnstileToken}\n onError={() => setError('Security check failed.')}\n options={{ theme: 'auto' }}\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {(submitting || redirecting) && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {redirecting\n ? 'Redirecting…'\n : submitting\n ? mode === 'signup'\n ? 'Creating…'\n : 'Signing in…'\n : mode === 'signup'\n ? 'Create account'\n : 'Sign in'}\n </button>\n </form>\n\n <div className=\"flex items-center justify-between pt-2 text-xs text-muted-foreground\">\n {mode === 'login' && (\n <Link href={forgotPasswordHref} className=\"hover:text-foreground\">\n Forgot password?\n </Link>\n )}\n <span className={mode === 'login' ? '' : 'ml-auto'}>\n {mode === 'login' ? `New to ${brand}?` : 'Already have an account?'}{' '}\n <Link href={otherHref} className=\"font-medium text-foreground hover:underline\">\n {mode === 'login' ? 'Sign up' : 'Sign in'}\n </Link>\n </span>\n </div>\n <p className=\"pt-2 text-[11px] leading-relaxed text-muted-foreground/80\">\n Identity is powered by{' '}\n <a href=\"https://huudis.com\" className=\"underline hover:text-foreground\">\n Huudis\n </a>\n . One account for every Forjio product.\n </p>\n </div>\n );\n}\n\nfunction GoogleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M21.6 12.227c0-.708-.064-1.39-.182-2.045H12v3.868h5.384a4.603 4.603 0 0 1-1.997 3.018v2.51h3.232c1.891-1.742 2.98-4.307 2.98-7.35Z\" fill=\"#4285F4\" />\n <path d=\"M12 22c2.7 0 4.965-.895 6.62-2.422l-3.233-2.51c-.895.6-2.041.955-3.386.955-2.604 0-4.81-1.76-5.596-4.122H3.067v2.59A9.996 9.996 0 0 0 12 22Z\" fill=\"#34A853\" />\n <path d=\"M6.404 13.9a6.016 6.016 0 0 1 0-3.8V7.512H3.067a9.996 9.996 0 0 0 0 8.977L6.404 13.9Z\" fill=\"#FBBC05\" />\n <path d=\"M12 5.977c1.468 0 2.786.505 3.823 1.497l2.868-2.868C16.96 2.986 14.696 2 12 2 8.118 2 4.76 4.232 3.067 7.51l3.337 2.59C7.19 7.737 9.396 5.977 12 5.977Z\" fill=\"#EA4335\" />\n </svg>\n );\n}\n\nfunction FacebookMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#1877F2\" aria-hidden=\"true\">\n <path d=\"M24 12.073C24 5.404 18.627 0 12 0S0 5.404 0 12.073c0 6.026 4.388 11.022 10.125 11.927v-8.437H7.078v-3.49h3.047V9.41c0-3.026 1.792-4.697 4.533-4.697 1.313 0 2.686.236 2.686.236v2.971H15.83c-1.49 0-1.955.93-1.955 1.886v2.266h3.328l-.532 3.49h-2.796v8.437C19.612 23.095 24 18.099 24 12.073Z\" />\n </svg>\n );\n}\n\nfunction AppleMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.564 12.73c-.037-3.16 2.58-4.678 2.698-4.752-1.47-2.146-3.76-2.44-4.576-2.473-1.948-.2-3.8 1.148-4.788 1.148-.993 0-2.513-1.12-4.13-1.091-2.127.03-4.085 1.236-5.174 3.142-2.207 3.82-.562 9.463 1.58 12.56 1.052 1.514 2.306 3.216 3.952 3.155 1.586-.065 2.185-1.026 4.102-1.026 1.917 0 2.455 1.026 4.133.99 1.705-.03 2.785-1.546 3.83-3.066 1.207-1.757 1.702-3.462 1.731-3.55-.038-.018-3.325-1.274-3.358-5.037Zm-3.154-9.24c.878-1.06 1.467-2.542 1.306-4.014-1.26.051-2.79.838-3.695 1.898-.813.937-1.524 2.433-1.333 3.885 1.405.108 2.843-.712 3.722-1.77Z\" />\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
@@ -36,12 +36,15 @@ var import_jsx_runtime = require("react/jsx-runtime");
36
36
  var import_react = require("react");
37
37
  var import_link = __toESM(require("next/link"), 1);
38
38
  var import_lucide_react = require("lucide-react");
39
+ var import_react_turnstile = require("@marsidev/react-turnstile");
39
40
  var import_types = require("./types");
41
+ const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
40
42
  function ForgotPasswordForm({ endpoints } = {}) {
41
43
  const ep = { ...import_types.defaultEndpoints, ...endpoints };
42
44
  const [email, setEmail] = (0, import_react.useState)("");
43
45
  const [submitting, setSubmitting] = (0, import_react.useState)(false);
44
46
  const [sent, setSent] = (0, import_react.useState)(false);
47
+ const [turnstileToken, setTurnstileToken] = (0, import_react.useState)("");
45
48
  const [error, setError] = (0, import_react.useState)(null);
46
49
  async function submit(e) {
47
50
  e.preventDefault();
@@ -51,7 +54,10 @@ function ForgotPasswordForm({ endpoints } = {}) {
51
54
  const res = await fetch(ep.forgotPassword, {
52
55
  method: "POST",
53
56
  headers: { "Content-Type": "application/json" },
54
- body: JSON.stringify({ email })
57
+ body: JSON.stringify({
58
+ email,
59
+ ...TURNSTILE_SITE_KEY ? { "cf-turnstile-response": turnstileToken } : {}
60
+ })
55
61
  });
56
62
  if (!res.ok) {
57
63
  const payload = await res.json().catch(() => null);
@@ -97,6 +103,15 @@ function ForgotPasswordForm({ endpoints } = {}) {
97
103
  }
98
104
  )
99
105
  ] }),
106
+ TURNSTILE_SITE_KEY && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-center py-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
107
+ import_react_turnstile.Turnstile,
108
+ {
109
+ siteKey: TURNSTILE_SITE_KEY,
110
+ onSuccess: setTurnstileToken,
111
+ onError: () => setError("Security check failed."),
112
+ options: { theme: "auto" }
113
+ }
114
+ ) }),
100
115
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
101
116
  "button",
102
117
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ForgotPasswordForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';\nimport { defaultEndpoints, type AuthEndpoints } from './types';\n\nexport interface ForgotPasswordFormProps {\n endpoints?: Partial<AuthEndpoints>;\n}\n\nexport function ForgotPasswordForm({ endpoints }: ForgotPasswordFormProps = {}) {\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n const [email, setEmail] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [sent, setSent] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const res = await fetch(ep.forgotPassword, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email }),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as { error?: { message?: string } } | null;\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n setSent(true);\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n if (sent) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-start gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm text-foreground\">\n <CheckCircle2 className=\"h-4 w-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n If <strong>{email}</strong> has a Huudis account, we&rsquo;ve sent a reset link. It expires in 1 hour.\n </span>\n </div>\n <Link href=\"/login\" className=\"block text-center text-sm font-medium text-foreground hover:underline\">\n Back to sign in\n </Link>\n </div>\n );\n }\n\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n {error && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n autoFocus\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <button\n type=\"submit\"\n disabled={submitting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting ? 'Sending…' : 'Send reset link'}\n </button>\n <div className=\"text-center text-xs text-muted-foreground\">\n Remembered it?{' '}\n <Link href=\"/login\" className=\"font-medium text-foreground hover:underline\">\n Sign in\n </Link>\n </div>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4CU;AA1CV,mBAAyB;AACzB,kBAAiB;AACjB,0BAAmD;AACnD,mBAAqD;AAM9C,SAAS,mBAAmB,EAAE,UAAU,IAA6B,CAAC,GAAG;AAC9E,QAAM,KAAoB,EAAE,GAAG,+BAAkB,GAAG,UAAU;AAC9D,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAS,EAAE;AACrC,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAS,KAAK;AAClD,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB,IAAI;AAEtD,iBAAe,OAAO,GAAoB;AACxC,MAAE,eAAe;AACjB,aAAS,IAAI;AACb,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,gBAAgB;AAAA,QACzC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,UAAW,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAClD,cAAM,IAAI,MAAM,SAAS,OAAO,WAAW,mBAAmB,IAAI,MAAM,GAAG;AAAA,MAC7E;AACA,cAAQ,IAAI;AAAA,IACd,SAAS,KAAK;AACZ,eAAU,IAAc,OAAO;AAAA,IACjC,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,MAAM;AACR,WACE,6CAAC,SAAI,WAAU,aACb;AAAA,mDAAC,SAAI,WAAU,8GACb;AAAA,oDAAC,oCAAa,WAAU,wCAAuC;AAAA,QAC/D,6CAAC,UAAK;AAAA;AAAA,UACD,4CAAC,YAAQ,iBAAM;AAAA,UAAS;AAAA,WAC7B;AAAA,SACF;AAAA,MACA,4CAAC,YAAAA,SAAA,EAAK,MAAK,UAAS,WAAU,yEAAwE,6BAEtG;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,6CAAC,UAAK,UAAU,QAAQ,WAAU,aAC/B;AAAA,aACC,6CAAC,SAAI,WAAU,uHACb;AAAA,kDAAC,mCAAY,WAAU,2BAA0B;AAAA,MACjD,4CAAC,UAAM,iBAAM;AAAA,OACf;AAAA,IAEF,6CAAC,SACC;AAAA,kDAAC,WAAM,WAAU,wDAAuD,mBAAK;AAAA,MAC7E;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAQ;AAAA,UACR,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,cAAa;AAAA,UACb,WAAS;AAAA,UACT,WAAU;AAAA;AAAA,MACZ;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAU;AAAA,QACV,WAAU;AAAA,QAET;AAAA,wBAAc,4CAAC,+BAAQ,WAAU,wBAAuB;AAAA,UACxD,aAAa,kBAAa;AAAA;AAAA;AAAA,IAC7B;AAAA,IACA,6CAAC,SAAI,WAAU,6CAA4C;AAAA;AAAA,MAC1C;AAAA,MACf,4CAAC,YAAAA,SAAA,EAAK,MAAK,UAAS,WAAU,+CAA8C,qBAE5E;AAAA,OACF;AAAA,KACF;AAEJ;","names":["Link"]}
1
+ {"version":3,"sources":["../src/ForgotPasswordForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport { defaultEndpoints, type AuthEndpoints } from './types';\n\n// Enabled family-wide via NEXT_PUBLIC_TURNSTILE_SITE_KEY (see AuthForm).\nconst TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;\n\nexport interface ForgotPasswordFormProps {\n endpoints?: Partial<AuthEndpoints>;\n}\n\nexport function ForgotPasswordForm({ endpoints }: ForgotPasswordFormProps = {}) {\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n const [email, setEmail] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [sent, setSent] = useState(false);\n const [turnstileToken, setTurnstileToken] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const res = await fetch(ep.forgotPassword, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email,\n ...(TURNSTILE_SITE_KEY ? { 'cf-turnstile-response': turnstileToken } : {}),\n }),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as { error?: { message?: string } } | null;\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n setSent(true);\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n if (sent) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-start gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm text-foreground\">\n <CheckCircle2 className=\"h-4 w-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n If <strong>{email}</strong> has a Huudis account, we&rsquo;ve sent a reset link. It expires in 1 hour.\n </span>\n </div>\n <Link href=\"/login\" className=\"block text-center text-sm font-medium text-foreground hover:underline\">\n Back to sign in\n </Link>\n </div>\n );\n }\n\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n {error && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n autoFocus\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n {TURNSTILE_SITE_KEY && (\n <div className=\"flex justify-center py-1\">\n <Turnstile\n siteKey={TURNSTILE_SITE_KEY}\n onSuccess={setTurnstileToken}\n onError={() => setError('Security check failed.')}\n options={{ theme: 'auto' }}\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting ? 'Sending…' : 'Send reset link'}\n </button>\n <div className=\"text-center text-xs text-muted-foreground\">\n Remembered it?{' '}\n <Link href=\"/login\" className=\"font-medium text-foreground hover:underline\">\n Sign in\n </Link>\n </div>\n </form>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoDU;AAlDV,mBAAyB;AACzB,kBAAiB;AACjB,0BAAmD;AACnD,6BAA0B;AAC1B,mBAAqD;AAGrD,MAAM,qBAAqB,QAAQ,IAAI;AAMhC,SAAS,mBAAmB,EAAE,UAAU,IAA6B,CAAC,GAAG;AAC9E,QAAM,KAAoB,EAAE,GAAG,+BAAkB,GAAG,UAAU;AAC9D,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAS,EAAE;AACrC,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAS,KAAK;AAClD,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,KAAK;AACtC,QAAM,CAAC,gBAAgB,iBAAiB,QAAI,uBAAS,EAAE;AACvD,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB,IAAI;AAEtD,iBAAe,OAAO,GAAoB;AACxC,MAAE,eAAe;AACjB,aAAS,IAAI;AACb,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,gBAAgB;AAAA,QACzC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU;AAAA,UACnB;AAAA,UACA,GAAI,qBAAqB,EAAE,yBAAyB,eAAe,IAAI,CAAC;AAAA,QAC1E,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,UAAW,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAClD,cAAM,IAAI,MAAM,SAAS,OAAO,WAAW,mBAAmB,IAAI,MAAM,GAAG;AAAA,MAC7E;AACA,cAAQ,IAAI;AAAA,IACd,SAAS,KAAK;AACZ,eAAU,IAAc,OAAO;AAAA,IACjC,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,MAAM;AACR,WACE,6CAAC,SAAI,WAAU,aACb;AAAA,mDAAC,SAAI,WAAU,8GACb;AAAA,oDAAC,oCAAa,WAAU,wCAAuC;AAAA,QAC/D,6CAAC,UAAK;AAAA;AAAA,UACD,4CAAC,YAAQ,iBAAM;AAAA,UAAS;AAAA,WAC7B;AAAA,SACF;AAAA,MACA,4CAAC,YAAAA,SAAA,EAAK,MAAK,UAAS,WAAU,yEAAwE,6BAEtG;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,6CAAC,UAAK,UAAU,QAAQ,WAAU,aAC/B;AAAA,aACC,6CAAC,SAAI,WAAU,uHACb;AAAA,kDAAC,mCAAY,WAAU,2BAA0B;AAAA,MACjD,4CAAC,UAAM,iBAAM;AAAA,OACf;AAAA,IAEF,6CAAC,SACC;AAAA,kDAAC,WAAM,WAAU,wDAAuD,mBAAK;AAAA,MAC7E;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAQ;AAAA,UACR,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,cAAa;AAAA,UACb,WAAS;AAAA,UACT,WAAU;AAAA;AAAA,MACZ;AAAA,OACF;AAAA,IACC,sBACC,4CAAC,SAAI,WAAU,4BACb;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,WAAW;AAAA,QACX,SAAS,MAAM,SAAS,wBAAwB;AAAA,QAChD,SAAS,EAAE,OAAO,OAAO;AAAA;AAAA,IAC3B,GACF;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAU;AAAA,QACV,WAAU;AAAA,QAET;AAAA,wBAAc,4CAAC,+BAAQ,WAAU,wBAAuB;AAAA,UACxD,aAAa,kBAAa;AAAA;AAAA;AAAA,IAC7B;AAAA,IACA,6CAAC,SAAI,WAAU,6CAA4C;AAAA;AAAA,MAC1C;AAAA,MACf,4CAAC,YAAAA,SAAA,EAAK,MAAK,UAAS,WAAU,+CAA8C,qBAE5E;AAAA,OACF;AAAA,KACF;AAEJ;","names":["Link"]}
@@ -3,12 +3,15 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import Link from "next/link";
5
5
  import { Loader2, AlertCircle, CheckCircle2 } from "lucide-react";
6
+ import { Turnstile } from "@marsidev/react-turnstile";
6
7
  import { defaultEndpoints } from "./types";
8
+ const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
7
9
  function ForgotPasswordForm({ endpoints } = {}) {
8
10
  const ep = { ...defaultEndpoints, ...endpoints };
9
11
  const [email, setEmail] = useState("");
10
12
  const [submitting, setSubmitting] = useState(false);
11
13
  const [sent, setSent] = useState(false);
14
+ const [turnstileToken, setTurnstileToken] = useState("");
12
15
  const [error, setError] = useState(null);
13
16
  async function submit(e) {
14
17
  e.preventDefault();
@@ -18,7 +21,10 @@ function ForgotPasswordForm({ endpoints } = {}) {
18
21
  const res = await fetch(ep.forgotPassword, {
19
22
  method: "POST",
20
23
  headers: { "Content-Type": "application/json" },
21
- body: JSON.stringify({ email })
24
+ body: JSON.stringify({
25
+ email,
26
+ ...TURNSTILE_SITE_KEY ? { "cf-turnstile-response": turnstileToken } : {}
27
+ })
22
28
  });
23
29
  if (!res.ok) {
24
30
  const payload = await res.json().catch(() => null);
@@ -64,6 +70,15 @@ function ForgotPasswordForm({ endpoints } = {}) {
64
70
  }
65
71
  )
66
72
  ] }),
73
+ TURNSTILE_SITE_KEY && /* @__PURE__ */ jsx("div", { className: "flex justify-center py-1", children: /* @__PURE__ */ jsx(
74
+ Turnstile,
75
+ {
76
+ siteKey: TURNSTILE_SITE_KEY,
77
+ onSuccess: setTurnstileToken,
78
+ onError: () => setError("Security check failed."),
79
+ options: { theme: "auto" }
80
+ }
81
+ ) }),
67
82
  /* @__PURE__ */ jsxs(
68
83
  "button",
69
84
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ForgotPasswordForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';\nimport { defaultEndpoints, type AuthEndpoints } from './types';\n\nexport interface ForgotPasswordFormProps {\n endpoints?: Partial<AuthEndpoints>;\n}\n\nexport function ForgotPasswordForm({ endpoints }: ForgotPasswordFormProps = {}) {\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n const [email, setEmail] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [sent, setSent] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const res = await fetch(ep.forgotPassword, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email }),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as { error?: { message?: string } } | null;\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n setSent(true);\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n if (sent) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-start gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm text-foreground\">\n <CheckCircle2 className=\"h-4 w-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n If <strong>{email}</strong> has a Huudis account, we&rsquo;ve sent a reset link. It expires in 1 hour.\n </span>\n </div>\n <Link href=\"/login\" className=\"block text-center text-sm font-medium text-foreground hover:underline\">\n Back to sign in\n </Link>\n </div>\n );\n }\n\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n {error && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n autoFocus\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <button\n type=\"submit\"\n disabled={submitting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting ? 'Sending…' : 'Send reset link'}\n </button>\n <div className=\"text-center text-xs text-muted-foreground\">\n Remembered it?{' '}\n <Link href=\"/login\" className=\"font-medium text-foreground hover:underline\">\n Sign in\n </Link>\n </div>\n </form>\n );\n}\n"],"mappings":";AA4CU,cACA,YADA;AA1CV,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,SAAS,aAAa,oBAAoB;AACnD,SAAS,wBAA4C;AAM9C,SAAS,mBAAmB,EAAE,UAAU,IAA6B,CAAC,GAAG;AAC9E,QAAM,KAAoB,EAAE,GAAG,kBAAkB,GAAG,UAAU;AAC9D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AAEtD,iBAAe,OAAO,GAAoB;AACxC,MAAE,eAAe;AACjB,aAAS,IAAI;AACb,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,gBAAgB;AAAA,QACzC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,UAAW,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAClD,cAAM,IAAI,MAAM,SAAS,OAAO,WAAW,mBAAmB,IAAI,MAAM,GAAG;AAAA,MAC7E;AACA,cAAQ,IAAI;AAAA,IACd,SAAS,KAAK;AACZ,eAAU,IAAc,OAAO;AAAA,IACjC,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,MAAM;AACR,WACE,qBAAC,SAAI,WAAU,aACb;AAAA,2BAAC,SAAI,WAAU,8GACb;AAAA,4BAAC,gBAAa,WAAU,wCAAuC;AAAA,QAC/D,qBAAC,UAAK;AAAA;AAAA,UACD,oBAAC,YAAQ,iBAAM;AAAA,UAAS;AAAA,WAC7B;AAAA,SACF;AAAA,MACA,oBAAC,QAAK,MAAK,UAAS,WAAU,yEAAwE,6BAEtG;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,UAAK,UAAU,QAAQ,WAAU,aAC/B;AAAA,aACC,qBAAC,SAAI,WAAU,uHACb;AAAA,0BAAC,eAAY,WAAU,2BAA0B;AAAA,MACjD,oBAAC,UAAM,iBAAM;AAAA,OACf;AAAA,IAEF,qBAAC,SACC;AAAA,0BAAC,WAAM,WAAU,wDAAuD,mBAAK;AAAA,MAC7E;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,UAAQ;AAAA,UACR,OAAO;AAAA,UACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,cAAa;AAAA,UACb,WAAS;AAAA,UACT,WAAU;AAAA;AAAA,MACZ;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAU;AAAA,QACV,WAAU;AAAA,QAET;AAAA,wBAAc,oBAAC,WAAQ,WAAU,wBAAuB;AAAA,UACxD,aAAa,kBAAa;AAAA;AAAA;AAAA,IAC7B;AAAA,IACA,qBAAC,SAAI,WAAU,6CAA4C;AAAA;AAAA,MAC1C;AAAA,MACf,oBAAC,QAAK,MAAK,UAAS,WAAU,+CAA8C,qBAE5E;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/ForgotPasswordForm.tsx"],"sourcesContent":["'use client';\n\nimport { useState } from 'react';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport { defaultEndpoints, type AuthEndpoints } from './types';\n\n// Enabled family-wide via NEXT_PUBLIC_TURNSTILE_SITE_KEY (see AuthForm).\nconst TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;\n\nexport interface ForgotPasswordFormProps {\n endpoints?: Partial<AuthEndpoints>;\n}\n\nexport function ForgotPasswordForm({ endpoints }: ForgotPasswordFormProps = {}) {\n const ep: AuthEndpoints = { ...defaultEndpoints, ...endpoints };\n const [email, setEmail] = useState('');\n const [submitting, setSubmitting] = useState(false);\n const [sent, setSent] = useState(false);\n const [turnstileToken, setTurnstileToken] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const res = await fetch(ep.forgotPassword, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email,\n ...(TURNSTILE_SITE_KEY ? { 'cf-turnstile-response': turnstileToken } : {}),\n }),\n });\n if (!res.ok) {\n const payload = (await res.json().catch(() => null)) as { error?: { message?: string } } | null;\n throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n setSent(true);\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n if (sent) {\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-start gap-2 rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-sm text-foreground\">\n <CheckCircle2 className=\"h-4 w-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n If <strong>{email}</strong> has a Huudis account, we&rsquo;ve sent a reset link. It expires in 1 hour.\n </span>\n </div>\n <Link href=\"/login\" className=\"block text-center text-sm font-medium text-foreground hover:underline\">\n Back to sign in\n </Link>\n </div>\n );\n }\n\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n {error && (\n <div className=\"flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive\">\n <AlertCircle className=\"h-4 w-4 shrink-0 mt-0.5\" />\n <span>{error}</span>\n </div>\n )}\n <div>\n <label className=\"mb-1 block text-xs font-medium text-muted-foreground\">Email</label>\n <input\n type=\"email\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n autoComplete=\"email\"\n autoFocus\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n {TURNSTILE_SITE_KEY && (\n <div className=\"flex justify-center py-1\">\n <Turnstile\n siteKey={TURNSTILE_SITE_KEY}\n onSuccess={setTurnstileToken}\n onError={() => setError('Security check failed.')}\n options={{ theme: 'auto' }}\n />\n </div>\n )}\n <button\n type=\"submit\"\n disabled={submitting}\n className=\"flex w-full items-center justify-center gap-2 rounded-md bg-primary py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting ? 'Sending…' : 'Send reset link'}\n </button>\n <div className=\"text-center text-xs text-muted-foreground\">\n Remembered it?{' '}\n <Link href=\"/login\" className=\"font-medium text-foreground hover:underline\">\n Sign in\n </Link>\n </div>\n </form>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forjio/auth-ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Shared auth forms (login, signup, forgot-password, reset-password) for the Forjio family of SaaS products. Sister package to @forjio/website-ui and @forjio/portal-ui.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -54,5 +54,8 @@
54
54
  "login",
55
55
  "signup",
56
56
  "huudis"
57
- ]
57
+ ],
58
+ "dependencies": {
59
+ "@marsidev/react-turnstile": "^1.5.3"
60
+ }
58
61
  }