@burdenoff/microfe-auth 2026.828.2 → 2026.830.2
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/auth/AuthRoot.js +43 -41
- package/dist/auth/AuthRoot.js.map +1 -1
- package/dist/auth/components/AuthLayout.js +65 -67
- package/dist/auth/components/AuthLayout.js.map +1 -1
- package/dist/auth/components/CaptchaV2.js +21 -24
- package/dist/auth/components/CaptchaV2.js.map +1 -1
- package/dist/auth/components/ConfirmActionDialog.js +33 -32
- package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
- package/dist/auth/components/profile/UserProfileMenu.js +44 -46
- package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
- package/dist/auth/components/workspace/WorkspaceSelector.js +62 -68
- package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
- package/dist/auth/pages/AuditLogPage.js +65 -68
- package/dist/auth/pages/AuditLogPage.js.map +1 -1
- package/dist/auth/pages/EmailVerifyPage.js +51 -54
- package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
- package/dist/auth/pages/LoginPage.js +223 -225
- package/dist/auth/pages/LoginPage.js.map +1 -1
- package/dist/auth/pages/MagicLinkPage.js +67 -69
- package/dist/auth/pages/MagicLinkPage.js.map +1 -1
- package/dist/auth/pages/MfaSetupPage.js +85 -88
- package/dist/auth/pages/MfaSetupPage.js.map +1 -1
- package/dist/auth/pages/MfaVerifyPage.js +60 -63
- package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
- package/dist/auth/pages/OAuthCallbackPage.js +37 -40
- package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
- package/dist/auth/pages/PasswordResetPage.js +102 -105
- package/dist/auth/pages/PasswordResetPage.js.map +1 -1
- package/dist/auth/pages/ReactivatePage.js +54 -57
- package/dist/auth/pages/ReactivatePage.js.map +1 -1
- package/dist/auth/pages/RegisterPage.js +171 -174
- package/dist/auth/pages/RegisterPage.js.map +1 -1
- package/dist/auth/pages/SSOClearPage.js +21 -20
- package/dist/auth/pages/SSOClearPage.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.js +27 -26
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.js +19 -18
- package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
- package/dist/auth/pages/SecurityPage.js +247 -250
- package/dist/auth/pages/SecurityPage.js.map +1 -1
- package/dist/auth/pages/SessionsPage.js +95 -98
- package/dist/auth/pages/SessionsPage.js.map +1 -1
- package/dist/auth/pages/SsoCallbackPage.js +53 -56
- package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
- package/dist/auth/pages/SsoConfigPage.js +296 -299
- package/dist/auth/pages/SsoConfigPage.js.map +1 -1
- package/dist/auth/pages/SsoLoginPage.js +65 -68
- package/dist/auth/pages/SsoLoginPage.js.map +1 -1
- package/dist/auth/pages/UserDetailsPage.js +124 -127
- package/dist/auth/pages/UserDetailsPage.js.map +1 -1
- package/dist/auth/pages/UserProfilePage.js +171 -174
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.js +322 -325
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/shared/hooks/index.d.ts +1 -6
- package/dist/shared/hooks/useTr.d.ts +16 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/dist/translations/en.d.ts +806 -248
- package/dist/translations/en.js +870 -0
- package/dist/translations/en.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RegisterPage.js","names":[],"sources":["../../../src/auth/pages/RegisterPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { validateEmail, validatePassword } from \"../utils/validation\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useOAuthOperations } from \"../hooks/useOAuthOperations\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport {\n EmailIcon,\n LockIcon,\n UserIcon,\n EyeIcon,\n EyeOffIcon,\n AlertIcon,\n CheckIcon,\n XIcon,\n SpinnerIcon,\n GoogleIcon,\n GitHubIcon,\n MicrosoftIcon,\n} from \"../components/AuthIcons\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { CaptchaV2 } from \"../components/CaptchaV2\";\nimport { useSignupCaptcha } from \"../hooks/useSignupCaptcha\";\nimport type { SignUpInput } from \"../types\";\n\n/**\n * Registration page with professional design\n */\nexport function RegisterPage() {\n const { productName } = useAuth();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const { signUp, resendVerification, isLoading, errorCode } =\n useAuthOperations();\n const { initiateGoogleOAuth, initiateMicrosoftOAuth, isProviderConfigured } =\n useOAuthOperations();\n const [showPassword, setShowPassword] = useState(false);\n const [formData, setFormData] = useState<SignUpInput>({\n email: \"\",\n username: \"\",\n password: \"\",\n firstName: \"\",\n lastName: \"\",\n });\n const [validationErrors, setValidationErrors] = useState<string[]>([]);\n const [signupSuccess, setSignupSuccess] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [oauthError, setOauthError] = useState<string | null>(null);\n const [captchaToken, setCaptchaToken] = useState<string | null>(null);\n // reCAPTCHA v2 tokens are single-use. Bumping this key remounts the widget so\n // a fresh token is issued after a failed submit (the prior token is consumed\n // server-side; reusing it returns timeout-or-duplicate → CAPTCHA_FAILED loop).\n const [captchaResetKey, setCaptchaResetKey] = useState(0);\n const { enabled: captchaEnabled, siteKey: captchaSiteKey } =\n useSignupCaptcha();\n const [isResendingVerification, setIsResendingVerification] = useState(false);\n const [resendError, setResendError] = useState<string | null>(null);\n\n // Signing up with an email that already belongs to a non-active account:\n // route the user to the right recovery path instead of dead-ending.\n const isAccountDeactivated =\n Boolean(error) && errorCode === \"ACCOUNT_DEACTIVATED\";\n const isAccountRemoved = Boolean(error) && errorCode === \"ACCOUNT_REMOVED\";\n const isAccountSuspended =\n Boolean(error) && errorCode === \"ACCOUNT_SUSPENDED\";\n\n // Password strength indicators\n const passwordChecks = (() => {\n const password = formData.password;\n return [\n {\n label: tr(\"auth.register.pwMin8\", \"At least 12 characters\"),\n met: password.length >= 12,\n },\n {\n label: tr(\"auth.register.pwUppercase\", \"One uppercase letter\"),\n met: /[A-Z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwLowercase\", \"One lowercase letter\"),\n met: /[a-z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwNumber\", \"One number\"),\n met: /\\d/.test(password),\n },\n {\n label: tr(\"auth.register.pwSpecial\", \"One special character\"),\n met: /[!@#$%^&*(),.?\":{}|<>]/.test(password),\n },\n ];\n })();\n\n const passwordStrength = (() => {\n const metCount = passwordChecks.filter((c) => c.met).length;\n if (metCount <= 2)\n return {\n level: tr(\"auth.register.passwordStrengthWeak\", \"weak\"),\n color: \"bg-status-error-bg-subtle0\",\n width: \"w-1/4\",\n };\n if (metCount <= 3)\n return {\n level: tr(\"auth.register.passwordStrengthFair\", \"fair\"),\n color: \"bg-status-warning-text\",\n width: \"w-2/4\",\n };\n if (metCount <= 4)\n return {\n level: tr(\"auth.register.passwordStrengthGood\", \"good\"),\n color: \"bg-status-warning-bg-subtle0\",\n width: \"w-3/4\",\n };\n return {\n level: tr(\"auth.register.passwordStrengthStrong\", \"strong\"),\n color: \"bg-status-success-text\",\n width: \"w-full\",\n };\n })();\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n const errors: string[] = [];\n if (!validateEmail(formData.email)) {\n errors.push(\n tr(\"auth.register.invalidEmail\", \"Please enter a valid email address\"),\n );\n }\n if (!formData.username || formData.username.length < 3) {\n errors.push(\n tr(\n \"auth.register.usernameMin3\",\n \"Username must be at least 3 characters\",\n ),\n );\n }\n if (!/^[a-zA-Z0-9_-]+$/.test(formData.username)) {\n errors.push(\n tr(\n \"auth.register.usernameChars\",\n \"Username can only contain letters, numbers, underscores, and hyphens\",\n ),\n );\n }\n const passwordValidation = validatePassword(formData.password);\n if (!passwordValidation.valid) {\n errors.push(...passwordValidation.errors);\n }\n\n if (captchaEnabled && !captchaToken) {\n errors.push(\n tr(\n \"auth.register.completeCaptcha\",\n \"Please complete the reCAPTCHA verification\",\n ),\n );\n }\n\n if (errors.length > 0) {\n setValidationErrors(errors);\n return;\n }\n\n setValidationErrors([]);\n setError(null);\n try {\n await signUp({\n ...formData,\n ...(captchaToken ? { recaptchaToken: captchaToken } : {}),\n });\n // Sign up successful - check if email verification is required\n // (Backend returns empty tokens when email verification is needed)\n setSignupSuccess(true);\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\"auth.register.signUpFailed\", \"Sign up failed\");\n setError(message);\n console.error(\"Sign up failed:\", err);\n // The captcha token (if any) was consumed during this attempt — discard it\n // and remount the widget so the user gets a fresh, solvable challenge on\n // retry instead of being trapped in a CAPTCHA_FAILED loop.\n if (captchaEnabled) {\n setCaptchaToken(null);\n setCaptchaResetKey((k) => k + 1);\n }\n }\n };\n\n /**\n * Handle Google OAuth signup\n */\n const handleGoogleSignup = () => {\n setOauthError(null);\n try {\n initiateGoogleOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.googleSignUpFailed\",\n \"Failed to initiate Google sign up\",\n ),\n );\n }\n };\n\n /**\n * Handle Microsoft OAuth signup\n */\n const handleMicrosoftSignup = () => {\n setOauthError(null);\n try {\n initiateMicrosoftOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.microsoftSignUpFailed\",\n \"Failed to initiate Microsoft sign up\",\n ),\n );\n }\n };\n\n // Check if OAuth providers are configured\n const isGoogleConfigured = isProviderConfigured(\"google\");\n const isMicrosoftConfigured = isProviderConfigured(\"microsoft\");\n\n const oauthProviders = [\n {\n name: \"Google\",\n icon: GoogleIcon,\n onClick: handleGoogleSignup,\n disabled: !isGoogleConfigured,\n tooltip: isGoogleConfigured\n ? undefined\n : tr(\n \"auth.register.googleNotConfigured\",\n \"Google sign up is not configured\",\n ),\n },\n {\n name: \"GitHub\",\n icon: GitHubIcon,\n onClick: () => {},\n disabled: true,\n tooltip: tr(\n \"auth.register.githubNotConfigured\",\n \"GitHub sign up is not configured\",\n ),\n },\n {\n name: \"Microsoft\",\n icon: MicrosoftIcon,\n onClick: handleMicrosoftSignup,\n disabled: !isMicrosoftConfigured,\n tooltip: isMicrosoftConfigured\n ? undefined\n : tr(\n \"auth.register.microsoftNotConfigured\",\n \"Microsoft sign up is not configured\",\n ),\n },\n ];\n\n const verificationEmailMessage = tr(\n \"auth.register.verificationEmailSentTo\",\n \"We've sent a verification email to {{email}}. Please check your inbox and click the verification link to activate your account.\",\n );\n const [verificationEmailPrefix, verificationEmailSuffix = \"\"] =\n verificationEmailMessage.split(\"{{email}}\");\n\n // Show success state after signup\n if (signupSuccess) {\n return (\n <AuthLayout\n title={tr(\"auth.register.checkEmail\", \"Check your email\")}\n subtitle={tr(\n \"auth.register.verificationLinkSent\",\n \"We've sent you a verification link\",\n )}\n >\n <div className=\"flex flex-col items-center justify-center py-6\">\n {/* Success Icon */}\n <div className=\"size-20 rounded-full bg-status-success-bg-subtle flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <h3 className=\"text-lg font-semibold text-[var(--color-text-primary)] mb-2\">\n {tr(\n \"auth.register.accountCreated\",\n \"Account created successfully!\",\n )}\n </h3>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-8 max-w-sm\">\n {verificationEmailPrefix}\n <span className=\"font-medium text-[var(--color-text-primary)]\">\n {formData.email}\n </span>\n {verificationEmailSuffix}\n </p>\n\n <div className=\"space-y-3 w-full\">\n <Button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\")}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.register.continueToSignIn\", \"Continue to sign in\")}\n </Button>\n\n {resendError && (\n <p className=\"text-center text-sm text-status-error-text\">\n {resendError}\n </p>\n )}\n <p className=\"text-center text-sm text-[var(--color-text-tertiary)]\">\n {tr(\n \"auth.register.didntReceiveEmail\",\n \"Didn't receive the email? Check your spam folder or\",\n )}{\" \"}\n <button\n type=\"button\"\n disabled={isResendingVerification}\n onClick={async () => {\n setResendError(null);\n setIsResendingVerification(true);\n try {\n await resendVerification(formData.email);\n } catch (err) {\n setResendError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.resendFailed\",\n \"Failed to resend verification email\",\n ),\n );\n } finally {\n setIsResendingVerification(false);\n }\n }}\n className=\"text-text-link hover:text-text-linkHover font-medium disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isResendingVerification\n ? tr(\"auth.register.resending\", \"resending...\")\n : tr(\"auth.register.tryAgain\", \"try again\")}\n </button>\n </p>\n </div>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout\n title={tr(\"auth.register.createAccount\", \"Create your account\")}\n subtitle={tr(\n \"auth.register.startJourney\",\n \"Start your journey with {{productName}}\",\n ).replace(\"{{productName}}\", productName || \"Burdenoff Workspaces\")}\n productName={productName}\n >\n <PagePurpose className=\"mb-5\">\n {tr(\n \"auth.register.purpose\",\n \"Create your account to get a personal workspace and start collaborating. You'll verify your email, then you can sign in and invite your team.\",\n )}\n </PagePurpose>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Deactivated account → offer self-service reactivation */}\n {isAccountDeactivated && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n \"auth.register.accountDeactivated\",\n \"This email belongs to a deactivated account.\",\n )}\n </p>\n <p className=\"text-xs text-status-warning-text/80\">\n {tr(\n \"auth.register.accountDeactivatedHelp\",\n \"If this is your account, you can reactivate it instead of creating a new one.\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() =>\n navigate(\"/auth/reactivate\", {\n state: { email: formData.email },\n })\n }\n className=\"text-sm font-semibold text-status-warning-text underline underline-offset-2\"\n >\n {tr(\"auth.register.reactivateCta\", \"Reactivate my account\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Admin-removed / suspended → point to support (no self-service path) */}\n {(isAccountRemoved || isAccountSuspended) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-status-warning-text\">{error}</p>\n </div>\n )}\n\n {/* Generic error / validation alert */}\n {((error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended) ||\n validationErrors.length > 0) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\">\n <AlertIcon className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-1\">\n {error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended && (\n <p className=\"text-sm text-status-error-text\">{error}</p>\n )}\n {validationErrors.map((err) => (\n <p key={err} className=\"text-sm text-status-error-text\">\n {err}\n </p>\n ))}\n </div>\n </div>\n )}\n\n {/* Name Fields */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <label\n htmlFor=\"firstName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.firstName\", \"First Name\")}\n </label>\n <input\n id=\"firstName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.firstName\", \"First Name\")}\n value={formData.firstName}\n onChange={(e) =>\n setFormData({ ...formData, firstName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.firstNamePlaceholder\",\n \"Enter first name\",\n )}\n />\n </div>\n <div className=\"space-y-2\">\n <label\n htmlFor=\"lastName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.lastName\", \"Last Name\")}\n </label>\n <input\n id=\"lastName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.lastName\", \"Last Name\")}\n value={formData.lastName}\n onChange={(e) =>\n setFormData({ ...formData, lastName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.lastNamePlaceholder\",\n \"Enter last name\",\n )}\n />\n </div>\n </div>\n\n {/* Username Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"username\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.username\", \"Username\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <UserIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"username\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.username\", \"Username\")}\n autoComplete=\"username\"\n value={formData.username}\n onChange={(e) =>\n setFormData({ ...formData, username: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.usernamePlaceholder\",\n \"Choose a username\",\n )}\n />\n </div>\n </div>\n\n {/* Email Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.email\", \"Email\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <EmailIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.email\", \"Email\")}\n autoComplete=\"email\"\n value={formData.email}\n onChange={(e) =>\n setFormData({ ...formData, email: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.emailPlaceholder\",\n \"you@example.com\",\n )}\n />\n </div>\n </div>\n\n {/* Password Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.password\", \"Password\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <LockIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.password\", \"Password\")}\n autoComplete=\"new-password\"\n value={formData.password}\n onChange={(e) =>\n setFormData({ ...formData, password: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.passwordPlaceholder\",\n \"Create a strong password\",\n )}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n aria-label={showPassword ? \"Hide password\" : \"Show password\"}\n className=\"absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors\"\n >\n {showPassword ? (\n <EyeOffIcon className=\"size-5\" />\n ) : (\n <EyeIcon className=\"size-5\" />\n )}\n </button>\n </div>\n\n {/* Password Strength Indicator */}\n {formData.password && (\n <div className=\"space-y-3 pt-2\">\n {/* Strength Bar */}\n <div className=\"space-y-1\">\n <div className=\"h-1.5 bg-[var(--color-bg-sunken)] rounded-full overflow-hidden\">\n <div\n className={`h-full ${passwordStrength.color} ${passwordStrength.width} transition-all duration-300`}\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-tertiary)] capitalize\">\n {tr(\"auth.register.passwordStrength\", \"Password strength:\")}{\" \"}\n <span className=\"font-medium\">{passwordStrength.level}</span>\n </p>\n </div>\n\n {/* Requirements Checklist */}\n <div className=\"grid grid-cols-2 gap-2\">\n {passwordChecks.map((check) => (\n <div key={check.label} className=\"flex items-center gap-2\">\n {check.met ? (\n <CheckIcon className=\"size-4 text-status-success-text\" />\n ) : (\n <XIcon className=\"size-4 text-[var(--color-text-tertiary)]\" />\n )}\n <span\n className={`text-xs ${\n check.met\n ? \"text-status-success-text\"\n : \"text-[var(--color-text-tertiary)]\"\n }`}\n >\n {check.label}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n\n {/* Terms Agreement */}\n <p className=\"text-xs text-[var(--color-text-tertiary)] leading-relaxed\">\n {tr(\n \"auth.register.agreeToTerms\",\n \"By creating an account, you agree to our\",\n )}{\" \"}\n <a\n href=\"/legal/terms\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.termsOfService\", \"Terms of Service\")}\n </a>{\" \"}\n {tr(\"auth.register.and\", \"and\")}{\" \"}\n <a\n href=\"/legal/privacy\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.privacyPolicy\", \"Privacy Policy\")}\n </a>\n .\n </p>\n\n {/* reCAPTCHA v2 Checkbox — enabled per-product via the platform catalog.\n `key` remounts it after a failed submit so a fresh token is issued. */}\n <CaptchaV2\n key={captchaResetKey}\n enabled={captchaEnabled}\n siteKey={captchaSiteKey}\n onVerify={setCaptchaToken}\n onExpired={() => setCaptchaToken(null)}\n />\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n disabled={isLoading}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>\n {tr(\"auth.register.creatingAccount\", \"Creating account...\")}\n </span>\n </>\n ) : (\n tr(\"auth.register.createAccountBtn\", \"Create account\")\n )}\n </Button>\n </form>\n\n {/* Divider */}\n <div className=\"relative my-8\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-[var(--color-border-default)]\" />\n </div>\n <div className=\"relative flex justify-center\">\n <span className=\"px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]\">\n {tr(\"auth.register.orSignUpWith\", \"or sign up with\")}\n </span>\n </div>\n </div>\n\n {/* OAuth Error */}\n {oauthError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{oauthError}</p>\n </div>\n )}\n\n {/* OAuth Buttons */}\n <div className=\"grid grid-cols-3 gap-3\">\n {oauthProviders.map((provider) => (\n <button\n key={provider.name}\n type=\"button\"\n onClick={provider.onClick}\n disabled={provider.disabled}\n title={provider.tooltip}\n className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${\n provider.disabled\n ? \"border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50\"\n : \"border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]\"\n }`}\n >\n <provider.icon\n className={`size-5 ${provider.disabled ? \"opacity-50\" : \"\"}`}\n />\n <span\n className={`sr-only sm:not-sr-only text-sm font-medium transition-colors ${\n provider.disabled\n ? \"text-[var(--color-text-disabled)]\"\n : \"text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]\"\n }`}\n >\n {provider.name}\n </span>\n </button>\n ))}\n </div>\n\n {/* Sign In Link */}\n <p className=\"mt-8 text-center text-sm text-[var(--color-text-secondary)]\">\n {tr(\"auth.register.alreadyHaveAccount\", \"Already have an account?\")}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\")}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.register.signIn\", \"Sign in\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RegisterPage.js","names":[],"sources":["../../../src/auth/pages/RegisterPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { validateEmail, validatePassword } from \"../utils/validation\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useOAuthOperations } from \"../hooks/useOAuthOperations\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport {\n EmailIcon,\n LockIcon,\n UserIcon,\n EyeIcon,\n EyeOffIcon,\n AlertIcon,\n CheckIcon,\n XIcon,\n SpinnerIcon,\n GoogleIcon,\n GitHubIcon,\n MicrosoftIcon,\n} from \"../components/AuthIcons\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { CaptchaV2 } from \"../components/CaptchaV2\";\nimport { useSignupCaptcha } from \"../hooks/useSignupCaptcha\";\nimport type { SignUpInput } from \"../types\";\n\n/**\n * Registration page with professional design\n */\nexport function RegisterPage() {\n const tr = useTr();\n const { productName } = useAuth();\n const navigate = useNavigate();\n\n const { signUp, resendVerification, isLoading, errorCode } =\n useAuthOperations();\n const { initiateGoogleOAuth, initiateMicrosoftOAuth, isProviderConfigured } =\n useOAuthOperations();\n const [showPassword, setShowPassword] = useState(false);\n const [formData, setFormData] = useState<SignUpInput>({\n email: \"\",\n username: \"\",\n password: \"\",\n firstName: \"\",\n lastName: \"\",\n });\n const [validationErrors, setValidationErrors] = useState<string[]>([]);\n const [signupSuccess, setSignupSuccess] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [oauthError, setOauthError] = useState<string | null>(null);\n const [captchaToken, setCaptchaToken] = useState<string | null>(null);\n // reCAPTCHA v2 tokens are single-use. Bumping this key remounts the widget so\n // a fresh token is issued after a failed submit (the prior token is consumed\n // server-side; reusing it returns timeout-or-duplicate → CAPTCHA_FAILED loop).\n const [captchaResetKey, setCaptchaResetKey] = useState(0);\n const { enabled: captchaEnabled, siteKey: captchaSiteKey } =\n useSignupCaptcha();\n const [isResendingVerification, setIsResendingVerification] = useState(false);\n const [resendError, setResendError] = useState<string | null>(null);\n\n // Signing up with an email that already belongs to a non-active account:\n // route the user to the right recovery path instead of dead-ending.\n const isAccountDeactivated =\n Boolean(error) && errorCode === \"ACCOUNT_DEACTIVATED\";\n const isAccountRemoved = Boolean(error) && errorCode === \"ACCOUNT_REMOVED\";\n const isAccountSuspended =\n Boolean(error) && errorCode === \"ACCOUNT_SUSPENDED\";\n\n // Password strength indicators\n const passwordChecks = (() => {\n const password = formData.password;\n return [\n {\n label: tr(\"auth.register.pwMin8\", \"At least 12 characters\"),\n met: password.length >= 12,\n },\n {\n label: tr(\"auth.register.pwUppercase\", \"One uppercase letter\"),\n met: /[A-Z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwLowercase\", \"One lowercase letter\"),\n met: /[a-z]/.test(password),\n },\n {\n label: tr(\"auth.register.pwNumber\", \"One number\"),\n met: /\\d/.test(password),\n },\n {\n label: tr(\"auth.register.pwSpecial\", \"One special character\"),\n met: /[!@#$%^&*(),.?\":{}|<>]/.test(password),\n },\n ];\n })();\n\n const passwordStrength = (() => {\n const metCount = passwordChecks.filter((c) => c.met).length;\n if (metCount <= 2)\n return {\n level: tr(\"auth.register.passwordStrengthWeak\", \"weak\"),\n color: \"bg-status-error-bg-subtle0\",\n width: \"w-1/4\",\n };\n if (metCount <= 3)\n return {\n level: tr(\"auth.register.passwordStrengthFair\", \"fair\"),\n color: \"bg-status-warning-text\",\n width: \"w-2/4\",\n };\n if (metCount <= 4)\n return {\n level: tr(\"auth.register.passwordStrengthGood\", \"good\"),\n color: \"bg-status-warning-bg-subtle0\",\n width: \"w-3/4\",\n };\n return {\n level: tr(\"auth.register.passwordStrengthStrong\", \"strong\"),\n color: \"bg-status-success-text\",\n width: \"w-full\",\n };\n })();\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n const errors: string[] = [];\n if (!validateEmail(formData.email)) {\n errors.push(\n tr(\"auth.register.invalidEmail\", \"Please enter a valid email address\"),\n );\n }\n if (!formData.username || formData.username.length < 3) {\n errors.push(\n tr(\n \"auth.register.usernameMin3\",\n \"Username must be at least 3 characters\",\n ),\n );\n }\n if (!/^[a-zA-Z0-9_-]+$/.test(formData.username)) {\n errors.push(\n tr(\n \"auth.register.usernameChars\",\n \"Username can only contain letters, numbers, underscores, and hyphens\",\n ),\n );\n }\n const passwordValidation = validatePassword(formData.password);\n if (!passwordValidation.valid) {\n errors.push(...passwordValidation.errors);\n }\n\n if (captchaEnabled && !captchaToken) {\n errors.push(\n tr(\n \"auth.register.completeCaptcha\",\n \"Please complete the reCAPTCHA verification\",\n ),\n );\n }\n\n if (errors.length > 0) {\n setValidationErrors(errors);\n return;\n }\n\n setValidationErrors([]);\n setError(null);\n try {\n await signUp({\n ...formData,\n ...(captchaToken ? { recaptchaToken: captchaToken } : {}),\n });\n // Sign up successful - check if email verification is required\n // (Backend returns empty tokens when email verification is needed)\n setSignupSuccess(true);\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\"auth.register.signUpFailed\", \"Sign up failed\");\n setError(message);\n console.error(\"Sign up failed:\", err);\n // The captcha token (if any) was consumed during this attempt — discard it\n // and remount the widget so the user gets a fresh, solvable challenge on\n // retry instead of being trapped in a CAPTCHA_FAILED loop.\n if (captchaEnabled) {\n setCaptchaToken(null);\n setCaptchaResetKey((k) => k + 1);\n }\n }\n };\n\n /**\n * Handle Google OAuth signup\n */\n const handleGoogleSignup = () => {\n setOauthError(null);\n try {\n initiateGoogleOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.googleSignUpFailed\",\n \"Failed to initiate Google sign up\",\n ),\n );\n }\n };\n\n /**\n * Handle Microsoft OAuth signup\n */\n const handleMicrosoftSignup = () => {\n setOauthError(null);\n try {\n initiateMicrosoftOAuth();\n } catch (err) {\n setOauthError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.microsoftSignUpFailed\",\n \"Failed to initiate Microsoft sign up\",\n ),\n );\n }\n };\n\n // Check if OAuth providers are configured\n const isGoogleConfigured = isProviderConfigured(\"google\");\n const isMicrosoftConfigured = isProviderConfigured(\"microsoft\");\n\n const oauthProviders = [\n {\n name: \"Google\",\n icon: GoogleIcon,\n onClick: handleGoogleSignup,\n disabled: !isGoogleConfigured,\n tooltip: isGoogleConfigured\n ? undefined\n : tr(\n \"auth.register.googleNotConfigured\",\n \"Google sign up is not configured\",\n ),\n },\n {\n name: \"GitHub\",\n icon: GitHubIcon,\n onClick: () => {},\n disabled: true,\n tooltip: tr(\n \"auth.register.githubNotConfigured\",\n \"GitHub sign up is not configured\",\n ),\n },\n {\n name: \"Microsoft\",\n icon: MicrosoftIcon,\n onClick: handleMicrosoftSignup,\n disabled: !isMicrosoftConfigured,\n tooltip: isMicrosoftConfigured\n ? undefined\n : tr(\n \"auth.register.microsoftNotConfigured\",\n \"Microsoft sign up is not configured\",\n ),\n },\n ];\n\n const verificationEmailMessage = tr(\n \"auth.register.verificationEmailSentTo\",\n \"We've sent a verification email to {{email}}. Please check your inbox and click the verification link to activate your account.\",\n );\n const [verificationEmailPrefix, verificationEmailSuffix = \"\"] =\n verificationEmailMessage.split(\"{{email}}\");\n\n // Show success state after signup\n if (signupSuccess) {\n return (\n <AuthLayout\n title={tr(\"auth.register.checkEmail\", \"Check your email\")}\n subtitle={tr(\n \"auth.register.verificationLinkSent\",\n \"We've sent you a verification link\",\n )}\n >\n <div className=\"flex flex-col items-center justify-center py-6\">\n {/* Success Icon */}\n <div className=\"size-20 rounded-full bg-status-success-bg-subtle flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <h3 className=\"text-lg font-semibold text-[var(--color-text-primary)] mb-2\">\n {tr(\n \"auth.register.accountCreated\",\n \"Account created successfully!\",\n )}\n </h3>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-8 max-w-sm\">\n {verificationEmailPrefix}\n <span className=\"font-medium text-[var(--color-text-primary)]\">\n {formData.email}\n </span>\n {verificationEmailSuffix}\n </p>\n\n <div className=\"space-y-3 w-full\">\n <Button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\")}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.register.continueToSignIn\", \"Continue to sign in\")}\n </Button>\n\n {resendError && (\n <p className=\"text-center text-sm text-status-error-text\">\n {resendError}\n </p>\n )}\n <p className=\"text-center text-sm text-[var(--color-text-tertiary)]\">\n {tr(\n \"auth.register.didntReceiveEmail\",\n \"Didn't receive the email? Check your spam folder or\",\n )}{\" \"}\n <button\n type=\"button\"\n disabled={isResendingVerification}\n onClick={async () => {\n setResendError(null);\n setIsResendingVerification(true);\n try {\n await resendVerification(formData.email);\n } catch (err) {\n setResendError(\n err instanceof Error\n ? err.message\n : tr(\n \"auth.register.resendFailed\",\n \"Failed to resend verification email\",\n ),\n );\n } finally {\n setIsResendingVerification(false);\n }\n }}\n className=\"text-text-link hover:text-text-linkHover font-medium disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isResendingVerification\n ? tr(\"auth.register.resending\", \"resending...\")\n : tr(\"auth.register.tryAgain\", \"try again\")}\n </button>\n </p>\n </div>\n </div>\n </AuthLayout>\n );\n }\n\n return (\n <AuthLayout\n title={tr(\"auth.register.createAccount\", \"Create your account\")}\n subtitle={tr(\n \"auth.register.startJourney\",\n \"Start your journey with {{productName}}\",\n ).replace(\"{{productName}}\", productName || \"Burdenoff Workspaces\")}\n productName={productName}\n >\n <PagePurpose className=\"mb-5\">\n {tr(\n \"auth.register.purpose\",\n \"Create your account to get a personal workspace and start collaborating. You'll verify your email, then you can sign in and invite your team.\",\n )}\n </PagePurpose>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Deactivated account → offer self-service reactivation */}\n {isAccountDeactivated && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n \"auth.register.accountDeactivated\",\n \"This email belongs to a deactivated account.\",\n )}\n </p>\n <p className=\"text-xs text-status-warning-text/80\">\n {tr(\n \"auth.register.accountDeactivatedHelp\",\n \"If this is your account, you can reactivate it instead of creating a new one.\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() =>\n navigate(\"/auth/reactivate\", {\n state: { email: formData.email },\n })\n }\n className=\"text-sm font-semibold text-status-warning-text underline underline-offset-2\"\n >\n {tr(\"auth.register.reactivateCta\", \"Reactivate my account\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Admin-removed / suspended → point to support (no self-service path) */}\n {(isAccountRemoved || isAccountSuspended) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-warning-bg-subtle border border-status-warning-border\">\n <AlertIcon className=\"size-5 text-status-warning-text flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-status-warning-text\">{error}</p>\n </div>\n )}\n\n {/* Generic error / validation alert */}\n {((error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended) ||\n validationErrors.length > 0) && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\">\n <AlertIcon className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-1\">\n {error &&\n !isAccountDeactivated &&\n !isAccountRemoved &&\n !isAccountSuspended && (\n <p className=\"text-sm text-status-error-text\">{error}</p>\n )}\n {validationErrors.map((err) => (\n <p key={err} className=\"text-sm text-status-error-text\">\n {err}\n </p>\n ))}\n </div>\n </div>\n )}\n\n {/* Name Fields */}\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <label\n htmlFor=\"firstName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.firstName\", \"First Name\")}\n </label>\n <input\n id=\"firstName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.firstName\", \"First Name\")}\n value={formData.firstName}\n onChange={(e) =>\n setFormData({ ...formData, firstName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.firstNamePlaceholder\",\n \"Enter first name\",\n )}\n />\n </div>\n <div className=\"space-y-2\">\n <label\n htmlFor=\"lastName\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.lastName\", \"Last Name\")}\n </label>\n <input\n id=\"lastName\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.lastName\", \"Last Name\")}\n value={formData.lastName}\n onChange={(e) =>\n setFormData({ ...formData, lastName: e.target.value })\n }\n className=\"block w-full px-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.lastNamePlaceholder\",\n \"Enter last name\",\n )}\n />\n </div>\n </div>\n\n {/* Username Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"username\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.username\", \"Username\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <UserIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"username\"\n type=\"text\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.username\", \"Username\")}\n autoComplete=\"username\"\n value={formData.username}\n onChange={(e) =>\n setFormData({ ...formData, username: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.usernamePlaceholder\",\n \"Choose a username\",\n )}\n />\n </div>\n </div>\n\n {/* Email Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.email\", \"Email\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <EmailIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.email\", \"Email\")}\n autoComplete=\"email\"\n value={formData.email}\n onChange={(e) =>\n setFormData({ ...formData, email: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-4 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.emailPlaceholder\",\n \"you@example.com\",\n )}\n />\n </div>\n </div>\n\n {/* Password Input */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)]\"\n >\n {tr(\"auth.register.password\", \"Password\")}\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none\">\n <LockIcon className=\"size-5 text-[var(--color-text-tertiary)]\" />\n </div>\n <input\n id=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n required\n aria-required=\"true\"\n aria-label={tr(\"auth.register.password\", \"Password\")}\n autoComplete=\"new-password\"\n value={formData.password}\n onChange={(e) =>\n setFormData({ ...formData, password: e.target.value })\n }\n className=\"block w-full pl-[3.5rem] pr-12 py-3 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n placeholder={tr(\n \"auth.register.passwordPlaceholder\",\n \"Create a strong password\",\n )}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n aria-label={\n showPassword\n ? tr(\"auth.register.hidePassword\", \"Hide password\")\n : tr(\"auth.register.showPassword\", \"Show password\")\n }\n className=\"absolute inset-y-0 right-0 pr-3.5 flex items-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors\"\n >\n {showPassword ? (\n <EyeOffIcon className=\"size-5\" />\n ) : (\n <EyeIcon className=\"size-5\" />\n )}\n </button>\n </div>\n\n {/* Password Strength Indicator */}\n {formData.password && (\n <div className=\"space-y-3 pt-2\">\n {/* Strength Bar */}\n <div className=\"space-y-1\">\n <div className=\"h-1.5 bg-[var(--color-bg-sunken)] rounded-full overflow-hidden\">\n <div\n className={`h-full ${passwordStrength.color} ${passwordStrength.width} transition-all duration-300`}\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-tertiary)] capitalize\">\n {tr(\"auth.register.passwordStrength\", \"Password strength:\")}{\" \"}\n <span className=\"font-medium\">{passwordStrength.level}</span>\n </p>\n </div>\n\n {/* Requirements Checklist */}\n <div className=\"grid grid-cols-2 gap-2\">\n {passwordChecks.map((check) => (\n <div key={check.label} className=\"flex items-center gap-2\">\n {check.met ? (\n <CheckIcon className=\"size-4 text-status-success-text\" />\n ) : (\n <XIcon className=\"size-4 text-[var(--color-text-tertiary)]\" />\n )}\n <span\n className={`text-xs ${\n check.met\n ? \"text-status-success-text\"\n : \"text-[var(--color-text-tertiary)]\"\n }`}\n >\n {check.label}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n\n {/* Terms Agreement */}\n <p className=\"text-xs text-[var(--color-text-tertiary)] leading-relaxed\">\n {tr(\n \"auth.register.agreeToTerms\",\n \"By creating an account, you agree to our\",\n )}{\" \"}\n <a\n href=\"/legal/terms\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.termsOfService\", \"Terms of Service\")}\n </a>{\" \"}\n {tr(\"auth.register.and\", \"and\")}{\" \"}\n <a\n href=\"/legal/privacy\"\n className=\"text-text-link underline underline-offset-2\"\n >\n {tr(\"auth.register.privacyPolicy\", \"Privacy Policy\")}\n </a>\n .\n </p>\n\n {/* reCAPTCHA v2 Checkbox — enabled per-product via the platform catalog.\n `key` remounts it after a failed submit so a fresh token is issued. */}\n <CaptchaV2\n key={captchaResetKey}\n enabled={captchaEnabled}\n siteKey={captchaSiteKey}\n onVerify={setCaptchaToken}\n onExpired={() => setCaptchaToken(null)}\n />\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n disabled={isLoading}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>\n {tr(\"auth.register.creatingAccount\", \"Creating account...\")}\n </span>\n </>\n ) : (\n tr(\"auth.register.createAccountBtn\", \"Create account\")\n )}\n </Button>\n </form>\n\n {/* Divider */}\n <div className=\"relative my-8\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-[var(--color-border-default)]\" />\n </div>\n <div className=\"relative flex justify-center\">\n <span className=\"px-4 bg-[var(--color-bg-canvas)] text-sm text-[var(--color-text-tertiary)]\">\n {tr(\"auth.register.orSignUpWith\", \"or sign up with\")}\n </span>\n </div>\n </div>\n\n {/* OAuth Error */}\n {oauthError && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl border bg-status-error-bg-subtle border-status-error-border\">\n <AlertIcon className=\"size-5 flex-shrink-0 mt-0.5 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">{oauthError}</p>\n </div>\n )}\n\n {/* OAuth Buttons */}\n <div className=\"grid grid-cols-3 gap-3\">\n {oauthProviders.map((provider) => (\n <button\n key={provider.name}\n type=\"button\"\n onClick={provider.onClick}\n disabled={provider.disabled}\n title={provider.tooltip}\n className={`flex items-center justify-center gap-2 px-4 py-3 rounded-xl border transition-all duration-200 group ${\n provider.disabled\n ? \"border-[var(--color-border-subtle)] bg-[var(--color-bg-disabled)] cursor-not-allowed opacity-50\"\n : \"border-[var(--color-border-default)] bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-sunken)] hover:border-[var(--color-border-strong)]\"\n }`}\n >\n <provider.icon\n className={`size-5 ${provider.disabled ? \"opacity-50\" : \"\"}`}\n />\n <span\n className={`sr-only sm:not-sr-only text-sm font-medium transition-colors ${\n provider.disabled\n ? \"text-[var(--color-text-disabled)]\"\n : \"text-[var(--color-text-secondary)] group-hover:text-[var(--color-text-primary)]\"\n }`}\n >\n {provider.name}\n </span>\n </button>\n ))}\n </div>\n\n {/* Sign In Link */}\n <p className=\"mt-8 text-center text-sm text-[var(--color-text-secondary)]\">\n {tr(\"auth.register.alreadyHaveAccount\", \"Already have an account?\")}{\" \"}\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\")}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.register.signIn\", \"Sign in\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,31 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { isAllowedSsoOrigin as t } from "../utils/ssoOrigins.js";
|
|
3
|
+
import { useEffect as n } from "react";
|
|
4
|
+
import { clearLocalAuthState as r } from "@burdenoff/fe-libs/shared/components";
|
|
5
|
+
import { useSearchParams as i } from "react-router";
|
|
6
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
7
|
+
import { clearShellSsoCookies as o } from "@burdenoff/fe-libs/shared/utils";
|
|
8
|
+
import { BrandedSplash as s } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
import { getAuthBridge as c } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
|
|
9
10
|
//#region src/auth/pages/SSOClearPage.tsx
|
|
10
|
-
function
|
|
11
|
-
if (!
|
|
11
|
+
function l(e) {
|
|
12
|
+
if (!e) return "/auth/login?reason=logout";
|
|
12
13
|
try {
|
|
13
|
-
let n = new URL(
|
|
14
|
-
if (n.origin === window.location.origin ||
|
|
14
|
+
let n = new URL(e, window.location.origin);
|
|
15
|
+
if (n.origin === window.location.origin || t(n)) return n.href;
|
|
15
16
|
} catch {}
|
|
16
17
|
return "/auth/login?reason=logout";
|
|
17
18
|
}
|
|
18
|
-
function
|
|
19
|
-
let [
|
|
20
|
-
return
|
|
21
|
-
let
|
|
19
|
+
function u() {
|
|
20
|
+
let t = e(), [u] = i();
|
|
21
|
+
return n(() => {
|
|
22
|
+
let e = l(u.get("returnTo"));
|
|
22
23
|
try {
|
|
23
|
-
|
|
24
|
+
r(), o(), c()?.notifyLogoutAll();
|
|
24
25
|
} catch {}
|
|
25
|
-
window.location.replace(
|
|
26
|
-
}, [
|
|
26
|
+
window.location.replace(e);
|
|
27
|
+
}, [u]), /* @__PURE__ */ a(s, { label: t("auth.ssoCallback.signingOut", "Signing you out…") });
|
|
27
28
|
}
|
|
28
29
|
//#endregion
|
|
29
|
-
export {
|
|
30
|
+
export { u as SSOClearPage };
|
|
30
31
|
|
|
31
32
|
//# sourceMappingURL=SSOClearPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSOClearPage.js","names":[],"sources":["../../../src/auth/pages/SSOClearPage.tsx"],"sourcesContent":["/**\n * SSOClearPage — Cross-Domain Logout at the Central Bridge Host\n *\n * Companion to SSODepositPage. When a user signs out on a product origin,\n * the product app cleared its own localStorage / cookies but the bridge\n * host (alphaapp/app.burdenoff.com) still held the deposited session in\n * first-party (unpartitioned) localStorage. A later visit to any product\n * would then bridge-host-relay that stale session back, effectively\n * undoing the logout.\n *\n * AuthProvider.logout() / logoutAll() on a non-bridge-host origin redirect\n * here via a top-level navigation:\n *\n * product.com (logout)\n * → app.burdenoff.com/auth/sso-clear?returnTo=<encoded-return-url>\n * — wipes bridge-host localStorage + bf-sso cookie\n * → <returnTo>\n *\n * URL: /auth/sso-clear\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { clearShellSsoCookies } from \"@burdenoff/fe-libs/shared/utils\";\nimport { clearLocalAuthState } from \"@burdenoff/fe-libs/shared/components\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/auth/login?reason=logout\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n\n try {\n clearLocalAuthState();\n clearShellSsoCookies();\n // Broadcast to any same-origin shells still listening so they drop the\n // session immediately too (best-effort — only fires on the bridge\n // host's origin, which is the only place where iframe storage isn't\n // partitioned cross-product).\n getAuthBridge()?.notifyLogoutAll();\n } catch {\n /* ignore — proceed with the redirect regardless */\n }\n\n window.location.replace(returnTo);\n }, [searchParams]);\n\n return <BrandedSplash
|
|
1
|
+
{"version":3,"file":"SSOClearPage.js","names":[],"sources":["../../../src/auth/pages/SSOClearPage.tsx"],"sourcesContent":["/**\n * SSOClearPage — Cross-Domain Logout at the Central Bridge Host\n *\n * Companion to SSODepositPage. When a user signs out on a product origin,\n * the product app cleared its own localStorage / cookies but the bridge\n * host (alphaapp/app.burdenoff.com) still held the deposited session in\n * first-party (unpartitioned) localStorage. A later visit to any product\n * would then bridge-host-relay that stale session back, effectively\n * undoing the logout.\n *\n * AuthProvider.logout() / logoutAll() on a non-bridge-host origin redirect\n * here via a top-level navigation:\n *\n * product.com (logout)\n * → app.burdenoff.com/auth/sso-clear?returnTo=<encoded-return-url>\n * — wipes bridge-host localStorage + bf-sso cookie\n * → <returnTo>\n *\n * URL: /auth/sso-clear\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { clearShellSsoCookies } from \"@burdenoff/fe-libs/shared/utils\";\nimport { clearLocalAuthState } from \"@burdenoff/fe-libs/shared/components\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/auth/login?reason=logout\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/auth/login?reason=logout\";\n}\n\nexport function SSOClearPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n\n try {\n clearLocalAuthState();\n clearShellSsoCookies();\n // Broadcast to any same-origin shells still listening so they drop the\n // session immediately too (best-effort — only fires on the bridge\n // host's origin, which is the only place where iframe storage isn't\n // partitioned cross-product).\n getAuthBridge()?.notifyLogoutAll();\n } catch {\n /* ignore — proceed with the redirect regardless */\n }\n\n window.location.replace(returnTo);\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingOut\", \"Signing you out…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;;AA8BA,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAe;CAC7B,IAAM,IAAK,GAAO,EACZ,CAAC,KAAgB,GAAiB;AAoBxC,QAlBA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC;AAEhE,MAAI;AAOF,GANA,GAAqB,EACrB,GAAsB,EAKtB,GAAe,EAAE,iBAAiB;UAC5B;AAIR,SAAO,SAAS,QAAQ,EAAS;IAChC,CAAC,EAAa,CAAC,EAGhB,kBAAC,GAAD,EACE,OAAO,EAAG,+BAA+B,mBAAmB,EAC5D,CAAA"}
|
|
@@ -1,46 +1,47 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { useTr as t } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { isAllowedSsoOrigin as n } from "../utils/ssoOrigins.js";
|
|
4
|
+
import { useEffect as r } from "react";
|
|
5
|
+
import { useSearchParams as i } from "react-router";
|
|
6
|
+
import { jsx as a } from "react/jsx-runtime";
|
|
7
|
+
import { setSSoCookie as o } from "@burdenoff/fe-libs/shared/utils";
|
|
8
|
+
import { BrandedSplash as s } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
import { getAuthBridge as c } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
|
|
9
10
|
//#region src/auth/pages/SSODepositPage.tsx
|
|
10
|
-
function
|
|
11
|
+
function l(e) {
|
|
11
12
|
if (!e) return "/";
|
|
12
13
|
try {
|
|
13
|
-
let
|
|
14
|
-
if (
|
|
14
|
+
let t = new URL(e, window.location.origin);
|
|
15
|
+
if (t.origin === window.location.origin || n(t)) return t.href;
|
|
15
16
|
} catch {}
|
|
16
17
|
return "/";
|
|
17
18
|
}
|
|
18
|
-
function
|
|
19
|
-
let [
|
|
20
|
-
return
|
|
21
|
-
let
|
|
22
|
-
if (!
|
|
23
|
-
window.location.replace(
|
|
19
|
+
function u() {
|
|
20
|
+
let n = t(), [u] = i();
|
|
21
|
+
return r(() => {
|
|
22
|
+
let t = l(u.get("returnTo")), n = window.location.hash;
|
|
23
|
+
if (!n.startsWith("#sso=")) {
|
|
24
|
+
window.location.replace(t);
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
try {
|
|
27
|
-
let
|
|
28
|
-
if (!i || !
|
|
29
|
-
window.location.replace(
|
|
28
|
+
let r = n.slice(5), { profileId: i, tokens: a, user: s, profiles: l } = JSON.parse(atob(r));
|
|
29
|
+
if (!i || !a) {
|
|
30
|
+
window.location.replace(t);
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
|
-
localStorage.setItem(`bf-p-${i}-tokens`,
|
|
33
|
-
user:
|
|
34
|
-
tokens:
|
|
33
|
+
localStorage.setItem(`bf-p-${i}-tokens`, a), s && localStorage.setItem(`bf-p-${i}-user`, s), l && localStorage.setItem("bf-profiles", l), sessionStorage.setItem("bf-active-profile", i), o(i), e.getState().loadFromProfile(i), window.dispatchEvent(new Event("bf-profile-storage-change")), c()?.notifyLogin({
|
|
34
|
+
user: s ? JSON.parse(s) : null,
|
|
35
|
+
tokens: a ? JSON.parse(a) : null,
|
|
35
36
|
activeProfileId: i,
|
|
36
37
|
profiles: l ? JSON.parse(l) : []
|
|
37
|
-
}), window.location.replace(
|
|
38
|
+
}), window.location.replace(t);
|
|
38
39
|
} catch {
|
|
39
|
-
window.location.replace(
|
|
40
|
+
window.location.replace(t);
|
|
40
41
|
}
|
|
41
|
-
}, [
|
|
42
|
+
}, [u]), /* @__PURE__ */ a(s, { label: n("auth.ssoCallback.signingIn", "Signing you in…") });
|
|
42
43
|
}
|
|
43
44
|
//#endregion
|
|
44
|
-
export {
|
|
45
|
+
export { u as SSODepositPage };
|
|
45
46
|
|
|
46
47
|
//# sourceMappingURL=SSODepositPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSODepositPage.js","names":[],"sources":["../../../src/auth/pages/SSODepositPage.tsx"],"sourcesContent":["/**\n * SSODepositPage — Cross-Domain Token Deposit at the Central Bridge Host\n *\n * Loaded as a top-level navigation (NOT an iframe) on the bridge host\n * (alphaapp/app.burdenoff.com) right after a user signs in on any other\n * product origin. It receives the freshly-issued tokens in the URL fragment\n * (`#sso=<base64>` — fragments are never sent to servers) and stores them\n * in this origin's first-party (unpartitioned) localStorage so that:\n *\n * 1. A later top-level visit to the bridge host renders authenticated.\n * 2. Other product origins can discover the session via\n * `/auth/sso-relay?returnTo=...` against this host.\n *\n * Flow:\n * product.com/auth/login → user signs in\n * product.com → POST-success redirect to:\n * app.burdenoff.com/auth/sso-deposit?returnTo=<encoded-return-url>#sso=<base64>\n * → writes tokens here, sets bf-sso cookie\n * <returnTo> → user lands back at intended dashboard\n *\n * URL: /auth/sso-deposit\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n window.location.replace(returnTo);\n return;\n }\n\n try {\n const encoded = hash.slice(5);\n const payload = JSON.parse(atob(encoded)) as {\n profileId?: string;\n tokens?: string;\n user?: string;\n profiles?: string;\n };\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(returnTo);\n return;\n }\n\n // First-party (unpartitioned) writes on the bridge host origin —\n // future top-level visits here see the session.\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the same-origin auth bridge so any other open shells on\n // this origin pick up the new session immediately.\n getAuthBridge()?.notifyLogin({\n user: user ? (JSON.parse(user) as Record<string, unknown>) : null,\n tokens: tokens ? (JSON.parse(tokens) as { accessToken: string }) : null,\n activeProfileId: profileId,\n profiles: profiles\n ? (JSON.parse(profiles) as Array<Record<string, unknown>>)\n : [],\n });\n\n window.location.replace(returnTo);\n } catch {\n window.location.replace(returnTo);\n }\n }, [searchParams]);\n\n return <BrandedSplash
|
|
1
|
+
{"version":3,"file":"SSODepositPage.js","names":[],"sources":["../../../src/auth/pages/SSODepositPage.tsx"],"sourcesContent":["/**\n * SSODepositPage — Cross-Domain Token Deposit at the Central Bridge Host\n *\n * Loaded as a top-level navigation (NOT an iframe) on the bridge host\n * (alphaapp/app.burdenoff.com) right after a user signs in on any other\n * product origin. It receives the freshly-issued tokens in the URL fragment\n * (`#sso=<base64>` — fragments are never sent to servers) and stores them\n * in this origin's first-party (unpartitioned) localStorage so that:\n *\n * 1. A later top-level visit to the bridge host renders authenticated.\n * 2. Other product origins can discover the session via\n * `/auth/sso-relay?returnTo=...` against this host.\n *\n * Flow:\n * product.com/auth/login → user signs in\n * product.com → POST-success redirect to:\n * app.burdenoff.com/auth/sso-deposit?returnTo=<encoded-return-url>#sso=<base64>\n * → writes tokens here, sets bf-sso cookie\n * <returnTo> → user lands back at intended dashboard\n *\n * URL: /auth/sso-deposit\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { isAllowedSsoOrigin } from \"../utils/ssoOrigins\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction resolveSafeReturn(returnTo: string | null): string {\n if (!returnTo) return \"/\";\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin === window.location.origin || isAllowedSsoOrigin(url)) {\n return url.href;\n }\n } catch {\n /* ignore */\n }\n return \"/\";\n}\n\nexport function SSODepositPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = resolveSafeReturn(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n window.location.replace(returnTo);\n return;\n }\n\n try {\n const encoded = hash.slice(5);\n const payload = JSON.parse(atob(encoded)) as {\n profileId?: string;\n tokens?: string;\n user?: string;\n profiles?: string;\n };\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(returnTo);\n return;\n }\n\n // First-party (unpartitioned) writes on the bridge host origin —\n // future top-level visits here see the session.\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the same-origin auth bridge so any other open shells on\n // this origin pick up the new session immediately.\n getAuthBridge()?.notifyLogin({\n user: user ? (JSON.parse(user) as Record<string, unknown>) : null,\n tokens: tokens ? (JSON.parse(tokens) as { accessToken: string }) : null,\n activeProfileId: profileId,\n profiles: profiles\n ? (JSON.parse(profiles) as Array<Record<string, unknown>>)\n : [],\n });\n\n window.location.replace(returnTo);\n } catch {\n window.location.replace(returnTo);\n }\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingIn\", \"Signing you in…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;;AAgCA,SAAS,EAAkB,GAAiC;AAC1D,KAAI,CAAC,EAAU,QAAO;AACtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AACrD,MAAI,EAAI,WAAW,OAAO,SAAS,UAAU,EAAmB,EAAI,CAClE,QAAO,EAAI;SAEP;AAGR,QAAO;;AAGT,SAAgB,IAAiB;CAC/B,IAAM,IAAK,GAAO,EACZ,CAAC,KAAgB,GAAiB;AAyDxC,QAvDA,QAAgB;EACd,IAAM,IAAW,EAAkB,EAAa,IAAI,WAAW,CAAC,EAC1D,IAAO,OAAO,SAAS;AAE7B,MAAI,CAAC,EAAK,WAAW,QAAQ,EAAE;AAC7B,UAAO,SAAS,QAAQ,EAAS;AACjC;;AAGF,MAAI;GACF,IAAM,IAAU,EAAK,MAAM,EAAE,EAOvB,EAAE,cAAW,WAAQ,SAAM,gBANjB,KAAK,MAAM,KAAK,EAAQ,CAAC;AAQzC,OAAI,CAAC,KAAa,CAAC,GAAQ;AACzB,WAAO,SAAS,QAAQ,EAAS;AACjC;;AA4BF,GAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,EAAO,EACpD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,EAAK,EAElD,KACF,aAAa,QAAQ,eAAe,EAAS,EAE/C,eAAe,QAAQ,qBAAqB,EAAU,EACtD,EAAa,EAAU,EACvB,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAClD,OAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC,EAI5D,GAAe,EAAE,YAAY;IAC3B,MAAM,IAAQ,KAAK,MAAM,EAAK,GAA+B;IAC7D,QAAQ,IAAU,KAAK,MAAM,EAAO,GAA+B;IACnE,iBAAiB;IACjB,UAAU,IACL,KAAK,MAAM,EAAS,GACrB,EAAE;IACP,CAAC,EAEF,OAAO,SAAS,QAAQ,EAAS;UAC3B;AACN,UAAO,SAAS,QAAQ,EAAS;;IAElC,CAAC,EAAa,CAAC,EAGhB,kBAAC,GAAD,EACE,OAAO,EAAG,8BAA8B,kBAAkB,EAC1D,CAAA"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { useTr as t } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { useEffect as n } from "react";
|
|
4
|
+
import { useSearchParams as r } from "react-router";
|
|
5
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
6
|
+
import { setSSoCookie as a } from "@burdenoff/fe-libs/shared/utils";
|
|
7
|
+
import { BrandedSplash as o } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
import { getAuthBridge as s } from "@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient";
|
|
8
9
|
//#region src/auth/pages/SSORelayCallbackPage.tsx
|
|
9
|
-
function
|
|
10
|
+
function c(e) {
|
|
10
11
|
if (!e) return "/dashboard";
|
|
11
12
|
try {
|
|
12
13
|
let t = new URL(e, window.location.origin);
|
|
@@ -15,32 +16,32 @@ function s(e) {
|
|
|
15
16
|
return "/dashboard";
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
function
|
|
19
|
-
let [
|
|
20
|
-
return
|
|
21
|
-
let t =
|
|
19
|
+
function l() {
|
|
20
|
+
let l = t(), [u] = r();
|
|
21
|
+
return n(() => {
|
|
22
|
+
let t = c(u.get("returnTo")), n = window.location.hash;
|
|
22
23
|
if (!n.startsWith("#sso=")) {
|
|
23
24
|
window.location.replace("/auth/login");
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
try {
|
|
27
|
-
let r = n.slice(5), { profileId:
|
|
28
|
-
if (!
|
|
28
|
+
let r = n.slice(5), { profileId: i, tokens: o, user: c, profiles: l } = JSON.parse(atob(r));
|
|
29
|
+
if (!i || !o) {
|
|
29
30
|
window.location.replace("/auth/login");
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
|
-
localStorage.setItem(`bf-p-${
|
|
33
|
+
localStorage.setItem(`bf-p-${i}-tokens`, o), c && localStorage.setItem(`bf-p-${i}-user`, c), l && localStorage.setItem("bf-profiles", l), sessionStorage.setItem("bf-active-profile", i), a(i), e.getState().loadFromProfile(i), window.dispatchEvent(new Event("bf-profile-storage-change")), s()?.notifyLogin({
|
|
33
34
|
user: c ? JSON.parse(c) : null,
|
|
34
|
-
tokens:
|
|
35
|
-
activeProfileId:
|
|
35
|
+
tokens: o ? JSON.parse(o) : null,
|
|
36
|
+
activeProfileId: i,
|
|
36
37
|
profiles: l ? JSON.parse(l) : []
|
|
37
38
|
}), window.location.replace(t);
|
|
38
39
|
} catch {
|
|
39
40
|
window.location.replace("/auth/login");
|
|
40
41
|
}
|
|
41
|
-
}, [
|
|
42
|
+
}, [u]), /* @__PURE__ */ i(o, { label: l("auth.ssoCallback.signingIn", "Signing you in…") });
|
|
42
43
|
}
|
|
43
44
|
//#endregion
|
|
44
|
-
export {
|
|
45
|
+
export { l as SSORelayCallbackPage };
|
|
45
46
|
|
|
46
47
|
//# sourceMappingURL=SSORelayCallbackPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SSORelayCallbackPage.js","names":[],"sources":["../../../src/auth/pages/SSORelayCallbackPage.tsx"],"sourcesContent":["/**\n * SSORelayCallbackPage — Receives tokens from the SSO relay and stores them locally.\n *\n * Tokens arrive in the URL fragment (#sso=<base64>) which is never sent to servers.\n * After storing in localStorage, redirects to the returnTo path.\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\n\nfunction getSafeReturnTo(returnTo: string | null): string {\n if (!returnTo) return \"/dashboard\";\n\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin !== window.location.origin) return \"/dashboard\";\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return \"/dashboard\";\n }\n}\n\nexport function SSORelayCallbackPage() {\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = getSafeReturnTo(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n // No SSO data — redirect to login\n window.location.replace(\"/auth/login\");\n return;\n }\n\n try {\n const encoded = hash.slice(5); // Remove \"#sso=\"\n const payload = JSON.parse(atob(encoded));\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(\"/auth/login\");\n return;\n }\n\n // Store tokens in localStorage\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n\n // Dispatch event so AuthProvider picks up the new state\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the auth bridge so other open shells can pick up the new session\n getAuthBridge()?.notifyLogin({\n user: user ? JSON.parse(user) : null,\n tokens: tokens ? JSON.parse(tokens) : null,\n activeProfileId: profileId,\n profiles: profiles ? JSON.parse(profiles) : [],\n });\n\n // Clear the hash and redirect\n window.location.replace(returnTo);\n } catch {\n window.location.replace(\"/auth/login\");\n }\n }, [searchParams]);\n\n return <BrandedSplash
|
|
1
|
+
{"version":3,"file":"SSORelayCallbackPage.js","names":[],"sources":["../../../src/auth/pages/SSORelayCallbackPage.tsx"],"sourcesContent":["/**\n * SSORelayCallbackPage — Receives tokens from the SSO relay and stores them locally.\n *\n * Tokens arrive in the URL fragment (#sso=<base64>) which is never sent to servers.\n * After storing in localStorage, redirects to the returnTo path.\n */\n\nimport { useEffect } from \"react\";\nimport { BrandedSplash } from \"@burdenoff/fe-libs/ui\";\nimport { useSearchParams } from \"react-router\";\nimport { getAuthBridge } from \"@burdenoff/fe-libs/shared/providers/shell/AuthBridgeClient\";\nimport { setSSoCookie } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useTr } from \"../../shared/hooks/useTr\";\n\nfunction getSafeReturnTo(returnTo: string | null): string {\n if (!returnTo) return \"/dashboard\";\n\n try {\n const url = new URL(returnTo, window.location.origin);\n if (url.origin !== window.location.origin) return \"/dashboard\";\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return \"/dashboard\";\n }\n}\n\nexport function SSORelayCallbackPage() {\n const tr = useTr();\n const [searchParams] = useSearchParams();\n\n useEffect(() => {\n const returnTo = getSafeReturnTo(searchParams.get(\"returnTo\"));\n const hash = window.location.hash;\n\n if (!hash.startsWith(\"#sso=\")) {\n // No SSO data — redirect to login\n window.location.replace(\"/auth/login\");\n return;\n }\n\n try {\n const encoded = hash.slice(5); // Remove \"#sso=\"\n const payload = JSON.parse(atob(encoded));\n const { profileId, tokens, user, profiles } = payload;\n\n if (!profileId || !tokens) {\n window.location.replace(\"/auth/login\");\n return;\n }\n\n // Store tokens in localStorage\n localStorage.setItem(`bf-p-${profileId}-tokens`, tokens);\n if (user) {\n localStorage.setItem(`bf-p-${profileId}-user`, user);\n }\n if (profiles) {\n localStorage.setItem(\"bf-profiles\", profiles);\n }\n sessionStorage.setItem(\"bf-active-profile\", profileId);\n setSSoCookie(profileId);\n useAuthStore.getState().loadFromProfile(profileId);\n\n // Dispatch event so AuthProvider picks up the new state\n window.dispatchEvent(new Event(\"bf-profile-storage-change\"));\n\n // Notify the auth bridge so other open shells can pick up the new session\n getAuthBridge()?.notifyLogin({\n user: user ? JSON.parse(user) : null,\n tokens: tokens ? JSON.parse(tokens) : null,\n activeProfileId: profileId,\n profiles: profiles ? JSON.parse(profiles) : [],\n });\n\n // Clear the hash and redirect\n window.location.replace(returnTo);\n } catch {\n window.location.replace(\"/auth/login\");\n }\n }, [searchParams]);\n\n return (\n <BrandedSplash\n label={tr(\"auth.ssoCallback.signingIn\", \"Signing you in…\")}\n />\n );\n}\n"],"mappings":";;;;;;;;;AAeA,SAAS,EAAgB,GAAiC;AACxD,KAAI,CAAC,EAAU,QAAO;AAEtB,KAAI;EACF,IAAM,IAAM,IAAI,IAAI,GAAU,OAAO,SAAS,OAAO;AAErD,SADI,EAAI,WAAW,OAAO,SAAS,SAC5B,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,SADQ;SAE5C;AACN,SAAO;;;AAIX,SAAgB,IAAuB;CACrC,IAAM,IAAK,GAAO,EACZ,CAAC,KAAgB,GAAiB;AAoDxC,QAlDA,QAAgB;EACd,IAAM,IAAW,EAAgB,EAAa,IAAI,WAAW,CAAC,EACxD,IAAO,OAAO,SAAS;AAE7B,MAAI,CAAC,EAAK,WAAW,QAAQ,EAAE;AAE7B,UAAO,SAAS,QAAQ,cAAc;AACtC;;AAGF,MAAI;GACF,IAAM,IAAU,EAAK,MAAM,EAAE,EAEvB,EAAE,cAAW,WAAQ,SAAM,gBADjB,KAAK,MAAM,KAAK,EAAQ,CAAC;AAGzC,OAAI,CAAC,KAAa,CAAC,GAAQ;AACzB,WAAO,SAAS,QAAQ,cAAc;AACtC;;AA2BF,GAvBA,aAAa,QAAQ,QAAQ,EAAU,UAAU,EAAO,EACpD,KACF,aAAa,QAAQ,QAAQ,EAAU,QAAQ,EAAK,EAElD,KACF,aAAa,QAAQ,eAAe,EAAS,EAE/C,eAAe,QAAQ,qBAAqB,EAAU,EACtD,EAAa,EAAU,EACvB,EAAa,UAAU,CAAC,gBAAgB,EAAU,EAGlD,OAAO,cAAc,IAAI,MAAM,4BAA4B,CAAC,EAG5D,GAAe,EAAE,YAAY;IAC3B,MAAM,IAAO,KAAK,MAAM,EAAK,GAAG;IAChC,QAAQ,IAAS,KAAK,MAAM,EAAO,GAAG;IACtC,iBAAiB;IACjB,UAAU,IAAW,KAAK,MAAM,EAAS,GAAG,EAAE;IAC/C,CAAC,EAGF,OAAO,SAAS,QAAQ,EAAS;UAC3B;AACN,UAAO,SAAS,QAAQ,cAAc;;IAEvC,CAAC,EAAa,CAAC,EAGhB,kBAAC,GAAD,EACE,OAAO,EAAG,8BAA8B,kBAAkB,EAC1D,CAAA"}
|