@forjio/auth-ui 0.7.0 → 0.8.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 +136 -7
- package/dist/AuthForm.cjs.map +1 -1
- package/dist/AuthForm.js +138 -9
- package/dist/AuthForm.js.map +1 -1
- package/dist/types.cjs +1 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +6 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/AuthForm.cjs
CHANGED
|
@@ -72,6 +72,15 @@ function AuthForm({
|
|
|
72
72
|
const [error, setError] = (0, import_react.useState)(
|
|
73
73
|
ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null
|
|
74
74
|
);
|
|
75
|
+
const [step, setStep] = (0, import_react.useState)("credentials");
|
|
76
|
+
const [mfaChallengeToken, setMfaChallengeToken] = (0, import_react.useState)("");
|
|
77
|
+
const [mfaMethods, setMfaMethods] = (0, import_react.useState)([]);
|
|
78
|
+
const [mfaExpiresAt, setMfaExpiresAt] = (0, import_react.useState)(null);
|
|
79
|
+
const [code, setCode] = (0, import_react.useState)("");
|
|
80
|
+
function redirectAfterAuth() {
|
|
81
|
+
router.push(returnTo);
|
|
82
|
+
router.refresh();
|
|
83
|
+
}
|
|
75
84
|
async function submit(e) {
|
|
76
85
|
e.preventDefault();
|
|
77
86
|
setError(null);
|
|
@@ -88,23 +97,80 @@ function AuthForm({
|
|
|
88
97
|
body: JSON.stringify(body)
|
|
89
98
|
});
|
|
90
99
|
if (!res.ok) {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
100
|
+
const payload2 = await res.json().catch(() => null);
|
|
101
|
+
if (payload2?.error?.code === "MFA_REQUIRED") {
|
|
93
102
|
setRedirecting(true);
|
|
94
103
|
const qs = new URLSearchParams({ return_to: returnTo, ...socialParams });
|
|
95
104
|
window.location.href = `${ep.socialStart}?${qs.toString()}`;
|
|
96
105
|
return;
|
|
97
106
|
}
|
|
107
|
+
throw new Error(payload2?.error?.message ?? `Request failed (${res.status})`);
|
|
108
|
+
}
|
|
109
|
+
const payload = await res.json().catch(() => null);
|
|
110
|
+
const challenge = payload?.data ?? payload;
|
|
111
|
+
if (challenge?.mfaRequired && challenge.mfaChallengeToken) {
|
|
112
|
+
setMfaChallengeToken(challenge.mfaChallengeToken);
|
|
113
|
+
setMfaMethods(Array.isArray(challenge.methods) ? challenge.methods : []);
|
|
114
|
+
setMfaExpiresAt(challenge.expiresAt ?? null);
|
|
115
|
+
setCode("");
|
|
116
|
+
setStep("otp");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
redirectAfterAuth();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
setError(err.message);
|
|
122
|
+
} finally {
|
|
123
|
+
setSubmitting(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function submitMfa(e) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
setError(null);
|
|
129
|
+
setSubmitting(true);
|
|
130
|
+
try {
|
|
131
|
+
const body = { mfaChallengeToken, code };
|
|
132
|
+
if (TURNSTILE_SITE_KEY && turnstileToken) body["cf-turnstile-response"] = turnstileToken;
|
|
133
|
+
const res = await fetch(ep.mfa, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
credentials: "include",
|
|
137
|
+
body: JSON.stringify(body)
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const payload = await res.json().catch(() => null);
|
|
98
141
|
throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);
|
|
99
142
|
}
|
|
100
|
-
|
|
101
|
-
router.refresh();
|
|
143
|
+
redirectAfterAuth();
|
|
102
144
|
} catch (err) {
|
|
103
145
|
setError(err.message);
|
|
104
146
|
} finally {
|
|
105
147
|
setSubmitting(false);
|
|
106
148
|
}
|
|
107
149
|
}
|
|
150
|
+
function backToCredentials() {
|
|
151
|
+
setStep("credentials");
|
|
152
|
+
setError(null);
|
|
153
|
+
setCode("");
|
|
154
|
+
setMfaChallengeToken("");
|
|
155
|
+
setMfaMethods([]);
|
|
156
|
+
setMfaExpiresAt(null);
|
|
157
|
+
}
|
|
158
|
+
const [secondsLeft, setSecondsLeft] = (0, import_react.useState)(null);
|
|
159
|
+
(0, import_react.useEffect)(() => {
|
|
160
|
+
if (step !== "otp" || !mfaExpiresAt) {
|
|
161
|
+
setSecondsLeft(null);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const target = new Date(mfaExpiresAt).getTime();
|
|
165
|
+
if (Number.isNaN(target)) {
|
|
166
|
+
setSecondsLeft(null);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const tick = () => setSecondsLeft(Math.max(0, Math.round((target - Date.now()) / 1e3)));
|
|
170
|
+
tick();
|
|
171
|
+
const id = setInterval(tick, 1e3);
|
|
172
|
+
return () => clearInterval(id);
|
|
173
|
+
}, [step, mfaExpiresAt]);
|
|
108
174
|
const otherMode = mode === "login" ? "signup" : "login";
|
|
109
175
|
const otherHref = `${otherMode === "signup" ? signupHref : loginHref}?return_to=${encodeURIComponent(returnTo)}`;
|
|
110
176
|
const socialUrl = (provider) => {
|
|
@@ -124,7 +190,70 @@ function AuthForm({
|
|
|
124
190
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Loader2, { className: "h-4 w-4 shrink-0 mt-0.5 animate-spin" }),
|
|
125
191
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Redirecting you to complete two-factor sign-in\u2026" })
|
|
126
192
|
] }),
|
|
127
|
-
|
|
193
|
+
step === "otp" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-4", children: [
|
|
194
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1", children: [
|
|
195
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
196
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ShieldCheck, { className: "h-4 w-4 shrink-0" }),
|
|
197
|
+
"Two-factor authentication"
|
|
198
|
+
] }),
|
|
199
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs leading-relaxed text-muted-foreground", children: "Enter the 6-digit code from your authenticator app." })
|
|
200
|
+
] }),
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submitMfa, className: "space-y-3", children: [
|
|
202
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
204
|
+
import_label.Label,
|
|
205
|
+
{
|
|
206
|
+
htmlFor: "mfa-code",
|
|
207
|
+
className: "mb-1 block text-xs leading-4 text-muted-foreground",
|
|
208
|
+
children: "Verification code"
|
|
209
|
+
}
|
|
210
|
+
),
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
212
|
+
import_input.Input,
|
|
213
|
+
{
|
|
214
|
+
id: "mfa-code",
|
|
215
|
+
type: "text",
|
|
216
|
+
required: true,
|
|
217
|
+
autoFocus: true,
|
|
218
|
+
inputMode: "numeric",
|
|
219
|
+
autoComplete: "one-time-code",
|
|
220
|
+
pattern: "[0-9]*",
|
|
221
|
+
maxLength: 6,
|
|
222
|
+
value: code,
|
|
223
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
224
|
+
"aria-label": "Six-digit verification code",
|
|
225
|
+
className: "h-auto border-border bg-background py-2 text-center text-lg tracking-[0.4em] shadow-none focus:outline-none focus:ring-1 focus:ring-primary"
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
secondsLeft !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-1 text-[11px] text-muted-foreground", children: secondsLeft > 0 ? `Code expires in ${Math.floor(secondsLeft / 60)}:${String(secondsLeft % 60).padStart(2, "0")}` : "Code expired \u2014 go back and sign in again." })
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
231
|
+
import_button.Button,
|
|
232
|
+
{
|
|
233
|
+
type: "submit",
|
|
234
|
+
disabled: submitting || code.length !== 6,
|
|
235
|
+
className: "flex h-auto w-full py-2.5 shadow-none hover:opacity-90",
|
|
236
|
+
children: [
|
|
237
|
+
submitting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
238
|
+
submitting ? "Verifying\u2026" : "Verify"
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
] }),
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
244
|
+
"button",
|
|
245
|
+
{
|
|
246
|
+
type: "button",
|
|
247
|
+
onClick: backToCredentials,
|
|
248
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
|
|
249
|
+
children: [
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowLeft, { className: "h-3.5 w-3.5" }),
|
|
251
|
+
"Back to sign in"
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
] }),
|
|
256
|
+
step === "credentials" && hasAnySocial && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
128
257
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid gap-2", children: [
|
|
129
258
|
showGoogle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
130
259
|
import_button.Button,
|
|
@@ -169,7 +298,7 @@ function AuthForm({
|
|
|
169
298
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex-1 border-t border-border" })
|
|
170
299
|
] })
|
|
171
300
|
] }),
|
|
172
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, className: "space-y-3", children: [
|
|
301
|
+
step === "credentials" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: submit, className: "space-y-3", children: [
|
|
173
302
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
174
303
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_label.Label, { className: "mb-1 block text-xs leading-4 text-muted-foreground", children: "Email" }),
|
|
175
304
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -238,7 +367,7 @@ function AuthForm({
|
|
|
238
367
|
}
|
|
239
368
|
)
|
|
240
369
|
] }),
|
|
241
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between pt-2 text-xs text-muted-foreground", children: [
|
|
370
|
+
step === "credentials" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between pt-2 text-xs text-muted-foreground", children: [
|
|
242
371
|
mode === "login" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: forgotPasswordHref, className: "hover:text-foreground", children: "Forgot password?" }),
|
|
243
372
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: mode === "login" ? "" : "ml-auto", children: [
|
|
244
373
|
mode === "login" ? `New to ${brand}?` : "Already have an account?",
|
package/dist/AuthForm.cjs.map
CHANGED
|
@@ -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 { Turnstile } from '@marsidev/react-turnstile';\nimport { useTurnstileTheme } from './useTurnstileTheme';\nimport { Button } from './components/ui/button';\nimport { Input } from './components/ui/input';\nimport { Label } from './components/ui/label';\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 turnstileTheme = useTurnstileTheme();\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 <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('google')}>\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n </Button>\n )}\n {showApple && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('apple')}>\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n </Button>\n )}\n {showFacebook && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('facebook')}>\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n </Button>\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 leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <Label className=\"mb-1 block text-xs leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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 leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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: turnstileTheme }}\n />\n </div>\n )}\n <Button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex h-auto w-full py-2.5 shadow-none hover:opacity-90\"\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 { useEffect, useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle, ShieldCheck, ArrowLeft } from 'lucide-react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport { useTurnstileTheme } from './useTurnstileTheme';\nimport { Button } from './components/ui/button';\nimport { Input } from './components/ui/input';\nimport { Label } from './components/ui/label';\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\n/** Shape of the product-hosted MFA challenge the login POST returns when\n * the user has MFA enabled and the product opted in. */\ninterface MfaChallenge {\n mfaRequired?: boolean;\n mfaChallengeToken?: string;\n methods?: string[];\n expiresAt?: string;\n}\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 turnstileTheme = useTurnstileTheme();\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n // Product-hosted MFA / OTP step. When the login POST returns\n // `{ mfaRequired, mfaChallengeToken, methods, expiresAt }` (optionally\n // wrapped in `{ data: … }`), the form swaps to the OTP step instead of\n // redirecting. Backward-compatible: a normal login response never sets\n // `mfaChallengeToken`, so the credentials step keeps today's behavior.\n const [step, setStep] = useState<'credentials' | 'otp'>('credentials');\n const [mfaChallengeToken, setMfaChallengeToken] = useState('');\n const [mfaMethods, setMfaMethods] = useState<string[]>([]);\n const [mfaExpiresAt, setMfaExpiresAt] = useState<string | null>(null);\n const [code, setCode] = useState('');\n\n function redirectAfterAuth() {\n router.push(returnTo);\n router.refresh();\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 // Product-hosted MFA opt-in: the SDK responds 200 with an MFA\n // challenge (possibly wrapped in `{ data: … }`) instead of a\n // session. Swap to the in-product OTP step rather than redirect.\n const payload = (await res.json().catch(() => null)) as\n | { data?: MfaChallenge } & MfaChallenge\n | null;\n const challenge = payload?.data ?? payload;\n if (challenge?.mfaRequired && challenge.mfaChallengeToken) {\n setMfaChallengeToken(challenge.mfaChallengeToken);\n setMfaMethods(Array.isArray(challenge.methods) ? challenge.methods : []);\n setMfaExpiresAt(challenge.expiresAt ?? null);\n setCode('');\n setStep('otp');\n return;\n }\n redirectAfterAuth();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n async function submitMfa(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const body: Record<string, unknown> = { mfaChallengeToken, code };\n // MFA verify doesn't need a fresh captcha; only forward a token the\n // credentials step already produced (never block on a missing one).\n if (TURNSTILE_SITE_KEY && turnstileToken) body['cf-turnstile-response'] = turnstileToken;\n const res = await fetch(ep.mfa, {\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 throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n redirectAfterAuth();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n function backToCredentials() {\n setStep('credentials');\n setError(null);\n setCode('');\n setMfaChallengeToken('');\n setMfaMethods([]);\n setMfaExpiresAt(null);\n }\n\n // Live countdown to the challenge expiry, shown as a small hint on the\n // OTP step. Recomputed every second while the OTP step is visible.\n const [secondsLeft, setSecondsLeft] = useState<number | null>(null);\n useEffect(() => {\n if (step !== 'otp' || !mfaExpiresAt) {\n setSecondsLeft(null);\n return;\n }\n const target = new Date(mfaExpiresAt).getTime();\n if (Number.isNaN(target)) {\n setSecondsLeft(null);\n return;\n }\n const tick = () => setSecondsLeft(Math.max(0, Math.round((target - Date.now()) / 1000)));\n tick();\n const id = setInterval(tick, 1000);\n return () => clearInterval(id);\n }, [step, mfaExpiresAt]);\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 {step === 'otp' && (\n <div className=\"space-y-4\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n <ShieldCheck className=\"h-4 w-4 shrink-0\" />\n Two-factor authentication\n </div>\n <p className=\"text-xs leading-relaxed text-muted-foreground\">\n Enter the 6-digit code from your authenticator app.\n </p>\n </div>\n\n <form onSubmit={submitMfa} className=\"space-y-3\">\n <div>\n <Label\n htmlFor=\"mfa-code\"\n className=\"mb-1 block text-xs leading-4 text-muted-foreground\"\n >\n Verification code\n </Label>\n <Input\n id=\"mfa-code\"\n type=\"text\"\n required\n autoFocus\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n pattern=\"[0-9]*\"\n maxLength={6}\n value={code}\n onChange={(e) => setCode(e.target.value.replace(/\\D/g, '').slice(0, 6))}\n aria-label=\"Six-digit verification code\"\n className=\"h-auto border-border bg-background py-2 text-center text-lg tracking-[0.4em] shadow-none focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {secondsLeft !== null && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {secondsLeft > 0\n ? `Code expires in ${Math.floor(secondsLeft / 60)}:${String(secondsLeft % 60).padStart(2, '0')}`\n : 'Code expired — go back and sign in again.'}\n </p>\n )}\n </div>\n <Button\n type=\"submit\"\n disabled={submitting || code.length !== 6}\n className=\"flex h-auto w-full py-2.5 shadow-none hover:opacity-90\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting ? 'Verifying…' : 'Verify'}\n </Button>\n </form>\n\n <button\n type=\"button\"\n onClick={backToCredentials}\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"h-3.5 w-3.5\" />\n Back to sign in\n </button>\n </div>\n )}\n\n {step === 'credentials' && hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('google')}>\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n </Button>\n )}\n {showApple && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('apple')}>\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n </Button>\n )}\n {showFacebook && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('facebook')}>\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n </Button>\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 {step === 'credentials' && (\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <Label className=\"mb-1 block text-xs leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <Label className=\"mb-1 block text-xs leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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 leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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: turnstileTheme }}\n />\n </div>\n )}\n <Button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex h-auto w-full py-2.5 shadow-none hover:opacity-90\"\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\n {step === 'credentials' && (\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 )}\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":["payload","Link"]}
|
package/dist/AuthForm.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
4
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import { Loader2, AlertCircle } from "lucide-react";
|
|
6
|
+
import { Loader2, AlertCircle, ShieldCheck, ArrowLeft } from "lucide-react";
|
|
7
7
|
import { Turnstile } from "@marsidev/react-turnstile";
|
|
8
8
|
import { useTurnstileTheme } from "./useTurnstileTheme";
|
|
9
9
|
import { Button } from "./components/ui/button";
|
|
@@ -39,6 +39,15 @@ function AuthForm({
|
|
|
39
39
|
const [error, setError] = useState(
|
|
40
40
|
ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null
|
|
41
41
|
);
|
|
42
|
+
const [step, setStep] = useState("credentials");
|
|
43
|
+
const [mfaChallengeToken, setMfaChallengeToken] = useState("");
|
|
44
|
+
const [mfaMethods, setMfaMethods] = useState([]);
|
|
45
|
+
const [mfaExpiresAt, setMfaExpiresAt] = useState(null);
|
|
46
|
+
const [code, setCode] = useState("");
|
|
47
|
+
function redirectAfterAuth() {
|
|
48
|
+
router.push(returnTo);
|
|
49
|
+
router.refresh();
|
|
50
|
+
}
|
|
42
51
|
async function submit(e) {
|
|
43
52
|
e.preventDefault();
|
|
44
53
|
setError(null);
|
|
@@ -55,23 +64,80 @@ function AuthForm({
|
|
|
55
64
|
body: JSON.stringify(body)
|
|
56
65
|
});
|
|
57
66
|
if (!res.ok) {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
67
|
+
const payload2 = await res.json().catch(() => null);
|
|
68
|
+
if (payload2?.error?.code === "MFA_REQUIRED") {
|
|
60
69
|
setRedirecting(true);
|
|
61
70
|
const qs = new URLSearchParams({ return_to: returnTo, ...socialParams });
|
|
62
71
|
window.location.href = `${ep.socialStart}?${qs.toString()}`;
|
|
63
72
|
return;
|
|
64
73
|
}
|
|
74
|
+
throw new Error(payload2?.error?.message ?? `Request failed (${res.status})`);
|
|
75
|
+
}
|
|
76
|
+
const payload = await res.json().catch(() => null);
|
|
77
|
+
const challenge = payload?.data ?? payload;
|
|
78
|
+
if (challenge?.mfaRequired && challenge.mfaChallengeToken) {
|
|
79
|
+
setMfaChallengeToken(challenge.mfaChallengeToken);
|
|
80
|
+
setMfaMethods(Array.isArray(challenge.methods) ? challenge.methods : []);
|
|
81
|
+
setMfaExpiresAt(challenge.expiresAt ?? null);
|
|
82
|
+
setCode("");
|
|
83
|
+
setStep("otp");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
redirectAfterAuth();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
setError(err.message);
|
|
89
|
+
} finally {
|
|
90
|
+
setSubmitting(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function submitMfa(e) {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setError(null);
|
|
96
|
+
setSubmitting(true);
|
|
97
|
+
try {
|
|
98
|
+
const body = { mfaChallengeToken, code };
|
|
99
|
+
if (TURNSTILE_SITE_KEY && turnstileToken) body["cf-turnstile-response"] = turnstileToken;
|
|
100
|
+
const res = await fetch(ep.mfa, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
credentials: "include",
|
|
104
|
+
body: JSON.stringify(body)
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const payload = await res.json().catch(() => null);
|
|
65
108
|
throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);
|
|
66
109
|
}
|
|
67
|
-
|
|
68
|
-
router.refresh();
|
|
110
|
+
redirectAfterAuth();
|
|
69
111
|
} catch (err) {
|
|
70
112
|
setError(err.message);
|
|
71
113
|
} finally {
|
|
72
114
|
setSubmitting(false);
|
|
73
115
|
}
|
|
74
116
|
}
|
|
117
|
+
function backToCredentials() {
|
|
118
|
+
setStep("credentials");
|
|
119
|
+
setError(null);
|
|
120
|
+
setCode("");
|
|
121
|
+
setMfaChallengeToken("");
|
|
122
|
+
setMfaMethods([]);
|
|
123
|
+
setMfaExpiresAt(null);
|
|
124
|
+
}
|
|
125
|
+
const [secondsLeft, setSecondsLeft] = useState(null);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (step !== "otp" || !mfaExpiresAt) {
|
|
128
|
+
setSecondsLeft(null);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const target = new Date(mfaExpiresAt).getTime();
|
|
132
|
+
if (Number.isNaN(target)) {
|
|
133
|
+
setSecondsLeft(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const tick = () => setSecondsLeft(Math.max(0, Math.round((target - Date.now()) / 1e3)));
|
|
137
|
+
tick();
|
|
138
|
+
const id = setInterval(tick, 1e3);
|
|
139
|
+
return () => clearInterval(id);
|
|
140
|
+
}, [step, mfaExpiresAt]);
|
|
75
141
|
const otherMode = mode === "login" ? "signup" : "login";
|
|
76
142
|
const otherHref = `${otherMode === "signup" ? signupHref : loginHref}?return_to=${encodeURIComponent(returnTo)}`;
|
|
77
143
|
const socialUrl = (provider) => {
|
|
@@ -91,7 +157,70 @@ function AuthForm({
|
|
|
91
157
|
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 shrink-0 mt-0.5 animate-spin" }),
|
|
92
158
|
/* @__PURE__ */ jsx("span", { children: "Redirecting you to complete two-factor sign-in\u2026" })
|
|
93
159
|
] }),
|
|
94
|
-
|
|
160
|
+
step === "otp" && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
161
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
162
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
163
|
+
/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 shrink-0" }),
|
|
164
|
+
"Two-factor authentication"
|
|
165
|
+
] }),
|
|
166
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: "Enter the 6-digit code from your authenticator app." })
|
|
167
|
+
] }),
|
|
168
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: submitMfa, className: "space-y-3", children: [
|
|
169
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
170
|
+
/* @__PURE__ */ jsx(
|
|
171
|
+
Label,
|
|
172
|
+
{
|
|
173
|
+
htmlFor: "mfa-code",
|
|
174
|
+
className: "mb-1 block text-xs leading-4 text-muted-foreground",
|
|
175
|
+
children: "Verification code"
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
/* @__PURE__ */ jsx(
|
|
179
|
+
Input,
|
|
180
|
+
{
|
|
181
|
+
id: "mfa-code",
|
|
182
|
+
type: "text",
|
|
183
|
+
required: true,
|
|
184
|
+
autoFocus: true,
|
|
185
|
+
inputMode: "numeric",
|
|
186
|
+
autoComplete: "one-time-code",
|
|
187
|
+
pattern: "[0-9]*",
|
|
188
|
+
maxLength: 6,
|
|
189
|
+
value: code,
|
|
190
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
191
|
+
"aria-label": "Six-digit verification code",
|
|
192
|
+
className: "h-auto border-border bg-background py-2 text-center text-lg tracking-[0.4em] shadow-none focus:outline-none focus:ring-1 focus:ring-primary"
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
secondsLeft !== null && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: secondsLeft > 0 ? `Code expires in ${Math.floor(secondsLeft / 60)}:${String(secondsLeft % 60).padStart(2, "0")}` : "Code expired \u2014 go back and sign in again." })
|
|
196
|
+
] }),
|
|
197
|
+
/* @__PURE__ */ jsxs(
|
|
198
|
+
Button,
|
|
199
|
+
{
|
|
200
|
+
type: "submit",
|
|
201
|
+
disabled: submitting || code.length !== 6,
|
|
202
|
+
className: "flex h-auto w-full py-2.5 shadow-none hover:opacity-90",
|
|
203
|
+
children: [
|
|
204
|
+
submitting && /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
205
|
+
submitting ? "Verifying\u2026" : "Verify"
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
] }),
|
|
210
|
+
/* @__PURE__ */ jsxs(
|
|
211
|
+
"button",
|
|
212
|
+
{
|
|
213
|
+
type: "button",
|
|
214
|
+
onClick: backToCredentials,
|
|
215
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
|
|
216
|
+
children: [
|
|
217
|
+
/* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" }),
|
|
218
|
+
"Back to sign in"
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
] }),
|
|
223
|
+
step === "credentials" && hasAnySocial && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
95
224
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
96
225
|
showGoogle && /* @__PURE__ */ jsx(
|
|
97
226
|
Button,
|
|
@@ -136,7 +265,7 @@ function AuthForm({
|
|
|
136
265
|
/* @__PURE__ */ jsx("div", { className: "flex-1 border-t border-border" })
|
|
137
266
|
] })
|
|
138
267
|
] }),
|
|
139
|
-
/* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-3", children: [
|
|
268
|
+
step === "credentials" && /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-3", children: [
|
|
140
269
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
141
270
|
/* @__PURE__ */ jsx(Label, { className: "mb-1 block text-xs leading-4 text-muted-foreground", children: "Email" }),
|
|
142
271
|
/* @__PURE__ */ jsx(
|
|
@@ -205,7 +334,7 @@ function AuthForm({
|
|
|
205
334
|
}
|
|
206
335
|
)
|
|
207
336
|
] }),
|
|
208
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-2 text-xs text-muted-foreground", children: [
|
|
337
|
+
step === "credentials" && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-2 text-xs text-muted-foreground", children: [
|
|
209
338
|
mode === "login" && /* @__PURE__ */ jsx(Link, { href: forgotPasswordHref, className: "hover:text-foreground", children: "Forgot password?" }),
|
|
210
339
|
/* @__PURE__ */ jsxs("span", { className: mode === "login" ? "" : "ml-auto", children: [
|
|
211
340
|
mode === "login" ? `New to ${brand}?` : "Already have an account?",
|
package/dist/AuthForm.js.map
CHANGED
|
@@ -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 { Turnstile } from '@marsidev/react-turnstile';\nimport { useTurnstileTheme } from './useTurnstileTheme';\nimport { Button } from './components/ui/button';\nimport { Input } from './components/ui/input';\nimport { Label } from './components/ui/label';\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 turnstileTheme = useTurnstileTheme();\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 <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('google')}>\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n </Button>\n )}\n {showApple && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('apple')}>\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n </Button>\n )}\n {showFacebook && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('facebook')}>\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n </Button>\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 leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <Label className=\"mb-1 block text-xs leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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 leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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: turnstileTheme }}\n />\n </div>\n )}\n <Button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex h-auto w-full py-2.5 shadow-none hover:opacity-90\"\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 { useEffect, useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport Link from 'next/link';\nimport { Loader2, AlertCircle, ShieldCheck, ArrowLeft } from 'lucide-react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport { useTurnstileTheme } from './useTurnstileTheme';\nimport { Button } from './components/ui/button';\nimport { Input } from './components/ui/input';\nimport { Label } from './components/ui/label';\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\n/** Shape of the product-hosted MFA challenge the login POST returns when\n * the user has MFA enabled and the product opted in. */\ninterface MfaChallenge {\n mfaRequired?: boolean;\n mfaChallengeToken?: string;\n methods?: string[];\n expiresAt?: string;\n}\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 turnstileTheme = useTurnstileTheme();\n const [error, setError] = useState<string | null>(\n ssoError ? `Sign-in failed: ${ssoDetail || ssoError}` : null,\n );\n\n // Product-hosted MFA / OTP step. When the login POST returns\n // `{ mfaRequired, mfaChallengeToken, methods, expiresAt }` (optionally\n // wrapped in `{ data: … }`), the form swaps to the OTP step instead of\n // redirecting. Backward-compatible: a normal login response never sets\n // `mfaChallengeToken`, so the credentials step keeps today's behavior.\n const [step, setStep] = useState<'credentials' | 'otp'>('credentials');\n const [mfaChallengeToken, setMfaChallengeToken] = useState('');\n const [mfaMethods, setMfaMethods] = useState<string[]>([]);\n const [mfaExpiresAt, setMfaExpiresAt] = useState<string | null>(null);\n const [code, setCode] = useState('');\n\n function redirectAfterAuth() {\n router.push(returnTo);\n router.refresh();\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 // Product-hosted MFA opt-in: the SDK responds 200 with an MFA\n // challenge (possibly wrapped in `{ data: … }`) instead of a\n // session. Swap to the in-product OTP step rather than redirect.\n const payload = (await res.json().catch(() => null)) as\n | { data?: MfaChallenge } & MfaChallenge\n | null;\n const challenge = payload?.data ?? payload;\n if (challenge?.mfaRequired && challenge.mfaChallengeToken) {\n setMfaChallengeToken(challenge.mfaChallengeToken);\n setMfaMethods(Array.isArray(challenge.methods) ? challenge.methods : []);\n setMfaExpiresAt(challenge.expiresAt ?? null);\n setCode('');\n setStep('otp');\n return;\n }\n redirectAfterAuth();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n async function submitMfa(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setSubmitting(true);\n try {\n const body: Record<string, unknown> = { mfaChallengeToken, code };\n // MFA verify doesn't need a fresh captcha; only forward a token the\n // credentials step already produced (never block on a missing one).\n if (TURNSTILE_SITE_KEY && turnstileToken) body['cf-turnstile-response'] = turnstileToken;\n const res = await fetch(ep.mfa, {\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 throw new Error(payload?.error?.message ?? `Request failed (${res.status})`);\n }\n redirectAfterAuth();\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setSubmitting(false);\n }\n }\n\n function backToCredentials() {\n setStep('credentials');\n setError(null);\n setCode('');\n setMfaChallengeToken('');\n setMfaMethods([]);\n setMfaExpiresAt(null);\n }\n\n // Live countdown to the challenge expiry, shown as a small hint on the\n // OTP step. Recomputed every second while the OTP step is visible.\n const [secondsLeft, setSecondsLeft] = useState<number | null>(null);\n useEffect(() => {\n if (step !== 'otp' || !mfaExpiresAt) {\n setSecondsLeft(null);\n return;\n }\n const target = new Date(mfaExpiresAt).getTime();\n if (Number.isNaN(target)) {\n setSecondsLeft(null);\n return;\n }\n const tick = () => setSecondsLeft(Math.max(0, Math.round((target - Date.now()) / 1000)));\n tick();\n const id = setInterval(tick, 1000);\n return () => clearInterval(id);\n }, [step, mfaExpiresAt]);\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 {step === 'otp' && (\n <div className=\"space-y-4\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-foreground\">\n <ShieldCheck className=\"h-4 w-4 shrink-0\" />\n Two-factor authentication\n </div>\n <p className=\"text-xs leading-relaxed text-muted-foreground\">\n Enter the 6-digit code from your authenticator app.\n </p>\n </div>\n\n <form onSubmit={submitMfa} className=\"space-y-3\">\n <div>\n <Label\n htmlFor=\"mfa-code\"\n className=\"mb-1 block text-xs leading-4 text-muted-foreground\"\n >\n Verification code\n </Label>\n <Input\n id=\"mfa-code\"\n type=\"text\"\n required\n autoFocus\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n pattern=\"[0-9]*\"\n maxLength={6}\n value={code}\n onChange={(e) => setCode(e.target.value.replace(/\\D/g, '').slice(0, 6))}\n aria-label=\"Six-digit verification code\"\n className=\"h-auto border-border bg-background py-2 text-center text-lg tracking-[0.4em] shadow-none focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n {secondsLeft !== null && (\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {secondsLeft > 0\n ? `Code expires in ${Math.floor(secondsLeft / 60)}:${String(secondsLeft % 60).padStart(2, '0')}`\n : 'Code expired — go back and sign in again.'}\n </p>\n )}\n </div>\n <Button\n type=\"submit\"\n disabled={submitting || code.length !== 6}\n className=\"flex h-auto w-full py-2.5 shadow-none hover:opacity-90\"\n >\n {submitting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {submitting ? 'Verifying…' : 'Verify'}\n </Button>\n </form>\n\n <button\n type=\"button\"\n onClick={backToCredentials}\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"h-3.5 w-3.5\" />\n Back to sign in\n </button>\n </div>\n )}\n\n {step === 'credentials' && hasAnySocial && (\n <>\n <div className=\"grid gap-2\">\n {showGoogle && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('google')}>\n <GoogleMark className=\"h-4 w-4\" />\n Continue with Google\n </a>\n </Button>\n )}\n {showApple && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('apple')}>\n <AppleMark className=\"h-4 w-4\" />\n Continue with Apple\n </a>\n </Button>\n )}\n {showFacebook && (\n <Button\n asChild\n variant=\"outline\"\n className=\"flex h-auto w-full border-border py-2 shadow-none\"\n >\n <a href={socialUrl('facebook')}>\n <FacebookMark className=\"h-4 w-4\" />\n Continue with Facebook\n </a>\n </Button>\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 {step === 'credentials' && (\n <form onSubmit={submit} className=\"space-y-3\">\n <div>\n <Label className=\"mb-1 block text-xs leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none focus:outline-none focus:ring-1 focus:ring-primary\"\n />\n </div>\n <div>\n <Label className=\"mb-1 block text-xs leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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 leading-4 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=\"h-auto border-border bg-background py-2 text-sm shadow-none 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: turnstileTheme }}\n />\n </div>\n )}\n <Button\n type=\"submit\"\n disabled={submitting || redirecting}\n className=\"flex h-auto w-full py-2.5 shadow-none hover:opacity-90\"\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\n {step === 'credentials' && (\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 )}\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":["payload"]}
|
package/dist/types.cjs
CHANGED
|
@@ -26,6 +26,7 @@ const defaultEndpoints = {
|
|
|
26
26
|
signup: "/api/v1/auth/signup",
|
|
27
27
|
forgotPassword: "/api/v1/auth/password-reset/request",
|
|
28
28
|
resetPassword: "/api/v1/auth/password-reset/complete",
|
|
29
|
+
mfa: "/api/v1/auth/mfa",
|
|
29
30
|
socialStart: "/api/v1/auth/huudis/start"
|
|
30
31
|
};
|
|
31
32
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/types.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/** Endpoint paths the auth forms hit on the host product's backend.\n * Defaults match the Forjio family convention (`@forjio/sdk/auth-\n * handlers`); override only for unusual layouts. */\nexport interface AuthEndpoints {\n login: string;\n signup: string;\n forgotPassword: string;\n resetPassword: string;\n /** Social-provider start path. `?provider=<google|apple|facebook>` is\n * appended by the form. Hitting it with no `provider=` lands on the\n * Huudis hosted login (used for the MFA hand-off). */\n socialStart: string;\n}\n\nexport const defaultEndpoints: AuthEndpoints = {\n login: '/api/v1/auth/login',\n signup: '/api/v1/auth/signup',\n forgotPassword: '/api/v1/auth/password-reset/request',\n resetPassword: '/api/v1/auth/password-reset/complete',\n socialStart: '/api/v1/auth/huudis/start',\n};\n\n/** Which social-login buttons to render. Default = both, since fail-\n * open matches the host product's expectation (Huudis tells the\n * consumer which providers are off via a separate fetch the host\n * does — pass the result here). */\nexport interface SocialProviders {\n google?: boolean;\n apple?: boolean;\n facebook?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/** Endpoint paths the auth forms hit on the host product's backend.\n * Defaults match the Forjio family convention (`@forjio/sdk/auth-\n * handlers`); override only for unusual layouts. */\nexport interface AuthEndpoints {\n login: string;\n signup: string;\n forgotPassword: string;\n resetPassword: string;\n /** Product-hosted MFA verify path. The login POST returns\n * `{ mfaRequired, mfaChallengeToken, methods, expiresAt }` when the\n * user has MFA enabled and the product opted into in-product OTP;\n * the form then POSTs `{ mfaChallengeToken, code }` here. Shares the\n * same base-path convention as `login`. */\n mfa: string;\n /** Social-provider start path. `?provider=<google|apple|facebook>` is\n * appended by the form. Hitting it with no `provider=` lands on the\n * Huudis hosted login (used for the MFA hand-off). */\n socialStart: string;\n}\n\nexport const defaultEndpoints: AuthEndpoints = {\n login: '/api/v1/auth/login',\n signup: '/api/v1/auth/signup',\n forgotPassword: '/api/v1/auth/password-reset/request',\n resetPassword: '/api/v1/auth/password-reset/complete',\n mfa: '/api/v1/auth/mfa',\n socialStart: '/api/v1/auth/huudis/start',\n};\n\n/** Which social-login buttons to render. Default = both, since fail-\n * open matches the host product's expectation (Huudis tells the\n * consumer which providers are off via a separate fetch the host\n * does — pass the result here). */\nexport interface SocialProviders {\n google?: boolean;\n apple?: boolean;\n facebook?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBO,MAAM,mBAAkC;AAAA,EAC7C,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,KAAK;AAAA,EACL,aAAa;AACf;","names":[]}
|
package/dist/types.d.cts
CHANGED
|
@@ -6,6 +6,12 @@ interface AuthEndpoints {
|
|
|
6
6
|
signup: string;
|
|
7
7
|
forgotPassword: string;
|
|
8
8
|
resetPassword: string;
|
|
9
|
+
/** Product-hosted MFA verify path. The login POST returns
|
|
10
|
+
* `{ mfaRequired, mfaChallengeToken, methods, expiresAt }` when the
|
|
11
|
+
* user has MFA enabled and the product opted into in-product OTP;
|
|
12
|
+
* the form then POSTs `{ mfaChallengeToken, code }` here. Shares the
|
|
13
|
+
* same base-path convention as `login`. */
|
|
14
|
+
mfa: string;
|
|
9
15
|
/** Social-provider start path. `?provider=<google|apple|facebook>` is
|
|
10
16
|
* appended by the form. Hitting it with no `provider=` lands on the
|
|
11
17
|
* Huudis hosted login (used for the MFA hand-off). */
|
package/dist/types.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ interface AuthEndpoints {
|
|
|
6
6
|
signup: string;
|
|
7
7
|
forgotPassword: string;
|
|
8
8
|
resetPassword: string;
|
|
9
|
+
/** Product-hosted MFA verify path. The login POST returns
|
|
10
|
+
* `{ mfaRequired, mfaChallengeToken, methods, expiresAt }` when the
|
|
11
|
+
* user has MFA enabled and the product opted into in-product OTP;
|
|
12
|
+
* the form then POSTs `{ mfaChallengeToken, code }` here. Shares the
|
|
13
|
+
* same base-path convention as `login`. */
|
|
14
|
+
mfa: string;
|
|
9
15
|
/** Social-provider start path. `?provider=<google|apple|facebook>` is
|
|
10
16
|
* appended by the form. Hitting it with no `provider=` lands on the
|
|
11
17
|
* Huudis hosted login (used for the MFA hand-off). */
|
package/dist/types.js
CHANGED
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/** Endpoint paths the auth forms hit on the host product's backend.\n * Defaults match the Forjio family convention (`@forjio/sdk/auth-\n * handlers`); override only for unusual layouts. */\nexport interface AuthEndpoints {\n login: string;\n signup: string;\n forgotPassword: string;\n resetPassword: string;\n /** Social-provider start path. `?provider=<google|apple|facebook>` is\n * appended by the form. Hitting it with no `provider=` lands on the\n * Huudis hosted login (used for the MFA hand-off). */\n socialStart: string;\n}\n\nexport const defaultEndpoints: AuthEndpoints = {\n login: '/api/v1/auth/login',\n signup: '/api/v1/auth/signup',\n forgotPassword: '/api/v1/auth/password-reset/request',\n resetPassword: '/api/v1/auth/password-reset/complete',\n socialStart: '/api/v1/auth/huudis/start',\n};\n\n/** Which social-login buttons to render. Default = both, since fail-\n * open matches the host product's expectation (Huudis tells the\n * consumer which providers are off via a separate fetch the host\n * does — pass the result here). */\nexport interface SocialProviders {\n google?: boolean;\n apple?: boolean;\n facebook?: boolean;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/** Endpoint paths the auth forms hit on the host product's backend.\n * Defaults match the Forjio family convention (`@forjio/sdk/auth-\n * handlers`); override only for unusual layouts. */\nexport interface AuthEndpoints {\n login: string;\n signup: string;\n forgotPassword: string;\n resetPassword: string;\n /** Product-hosted MFA verify path. The login POST returns\n * `{ mfaRequired, mfaChallengeToken, methods, expiresAt }` when the\n * user has MFA enabled and the product opted into in-product OTP;\n * the form then POSTs `{ mfaChallengeToken, code }` here. Shares the\n * same base-path convention as `login`. */\n mfa: string;\n /** Social-provider start path. `?provider=<google|apple|facebook>` is\n * appended by the form. Hitting it with no `provider=` lands on the\n * Huudis hosted login (used for the MFA hand-off). */\n socialStart: string;\n}\n\nexport const defaultEndpoints: AuthEndpoints = {\n login: '/api/v1/auth/login',\n signup: '/api/v1/auth/signup',\n forgotPassword: '/api/v1/auth/password-reset/request',\n resetPassword: '/api/v1/auth/password-reset/complete',\n mfa: '/api/v1/auth/mfa',\n socialStart: '/api/v1/auth/huudis/start',\n};\n\n/** Which social-login buttons to render. Default = both, since fail-\n * open matches the host product's expectation (Huudis tells the\n * consumer which providers are off via a separate fetch the host\n * does — pass the result here). */\nexport interface SocialProviders {\n google?: boolean;\n apple?: boolean;\n facebook?: boolean;\n}\n"],"mappings":"AAoBO,MAAM,mBAAkC;AAAA,EAC7C,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,KAAK;AAAA,EACL,aAAa;AACf;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forjio/auth-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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,
|