@better-auth-ui/core
Version:
Authentication components and data utilities for [Better Auth](https://better-auth.com), available for React and Solid.
352 lines (326 loc) • 8.58 kB
text/typescript
import type { Localization } from "./localization"
export type AuthResultFlow =
| "accountLinking"
| "emailVerification"
| "oauth"
| "passwordReset"
| "unknown"
export type AuthResultReason =
| "accountLinkConflict"
| "accountLinked"
| "callbackFailed"
| "cancelled"
| "emailNotVerified"
| "emailVerified"
| "expiredLink"
| "genericError"
| "genericSuccess"
| "missingEmail"
| "passwordReset"
| "signupComplete"
| "signupDisabled"
export type AuthResultAction =
| "accountSettings"
| "continue"
| "forgotPassword"
| "signIn"
| "signUp"
| "verifyEmail"
export type AuthResult = {
action: AuthResultAction
code?: string
flow: AuthResultFlow
intent: "danger" | "success" | "warning"
reason: AuthResultReason
redirectTo?: string
}
export type AuthResultMessage = {
description: string
title: string
}
const CANCELLED_ERRORS = new Set([
"access_denied",
"authorization_declined",
"cancelled",
"canceled",
"user_cancelled",
"user_canceled"
])
const EXPIRED_LINK_ERRORS = new Set([
"expired_token",
"invalid_token",
"token_expired"
])
const ACCOUNT_LINK_ERRORS = new Set([
"account_already_linked_to_different_user",
"account_not_linked",
"email_does_not_match",
"unable_to_link_account"
])
const CALLBACK_ERRORS = new Set([
"internal_server_error",
"invalid_callback_request",
"invalid_code",
"issuer_mismatch",
"issuer_missing",
"no_callback_url",
"no_code",
"nonce_binding_missing",
"oauth_provider_not_found",
"provider_not_found",
"state_mismatch",
"state_not_found",
"unable_to_get_user_info"
])
const normalizeValue = (value: string | null) =>
value
?.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_") ?? ""
function parseFlow(params: URLSearchParams): AuthResultFlow {
const flow = normalizeValue(params.get("flow") ?? params.get("source"))
if (["account_link", "account_linking", "link_account"].includes(flow)) {
return "accountLinking"
}
if (["email", "email_verification", "verify_email"].includes(flow)) {
return "emailVerification"
}
if (["password", "password_reset", "reset_password"].includes(flow)) {
return "passwordReset"
}
if (["oauth", "social", "social_sign_in"].includes(flow)) return "oauth"
return "unknown"
}
const AUTH_REDIRECT_ORIGIN = "https://better-auth-ui.local"
/** Only allow a local redirect path from callback query parameters. */
export function getSafeAuthRedirect(value: string | null) {
const redirect = value?.trim()
if (!redirect?.startsWith("/")) return undefined
try {
return new URL(redirect, AUTH_REDIRECT_ORIGIN).origin ===
AUTH_REDIRECT_ORIGIN
? redirect
: undefined
} catch {
return undefined
}
}
/** Classify Better Auth callback parameters into a stable UI result. */
export function parseAuthResult(
search: string | URLSearchParams,
fallbackIntent: "danger" | "success" = "success"
): AuthResult {
const params =
typeof search === "string"
? new URLSearchParams(search.startsWith("?") ? search.slice(1) : search)
: search
const code =
normalizeValue(params.get("error")) || normalizeValue(params.get("code"))
const result =
normalizeValue(params.get("result")) || normalizeValue(params.get("status"))
const flow = parseFlow(params)
const redirectTo = getSafeAuthRedirect(
params.get("redirectTo") ?? params.get("returnTo")
)
if (code) {
if (CANCELLED_ERRORS.has(code)) {
return {
action: "signIn",
code,
flow: flow === "unknown" ? "oauth" : flow,
intent: "warning",
reason: "cancelled"
}
}
if (EXPIRED_LINK_ERRORS.has(code)) {
const action =
flow === "passwordReset"
? "forgotPassword"
: flow === "emailVerification"
? "verifyEmail"
: "signIn"
return { action, code, flow, intent: "danger", reason: "expiredLink" }
}
if (ACCOUNT_LINK_ERRORS.has(code)) {
return {
action: "accountSettings",
code,
flow: "accountLinking",
intent: "danger",
reason: "accountLinkConflict"
}
}
if (code === "signup_disabled") {
return {
action: "signIn",
code,
flow,
intent: "danger",
reason: "signupDisabled"
}
}
if (code === "email_not_verified") {
return {
action: "verifyEmail",
code,
flow,
intent: "warning",
reason: "emailNotVerified"
}
}
if (code === "email_not_found") {
return {
action: "signIn",
code,
flow,
intent: "danger",
reason: "missingEmail"
}
}
if (CALLBACK_ERRORS.has(code)) {
return {
action: "signIn",
code,
flow: flow === "unknown" ? "oauth" : flow,
intent: "danger",
reason: "callbackFailed"
}
}
return {
action: "signIn",
code,
flow,
intent: "danger",
reason: "genericError"
}
}
if (["cancelled", "canceled"].includes(result)) {
return {
action: "signIn",
flow,
intent: "warning",
reason: "cancelled"
}
}
if (
["email_verified", "verified"].includes(result) ||
(result === "success" && flow === "emailVerification")
) {
return {
action: redirectTo ? "continue" : "signIn",
flow: "emailVerification",
intent: "success",
reason: "emailVerified",
redirectTo
}
}
if (
result === "account_linked" ||
(result === "success" && flow === "accountLinking")
) {
return {
action: redirectTo ? "continue" : "accountSettings",
flow: "accountLinking",
intent: "success",
reason: "accountLinked",
redirectTo
}
}
if (
result === "password_reset" ||
(result === "success" && flow === "passwordReset")
) {
return {
action: "signIn",
flow: "passwordReset",
intent: "success",
reason: "passwordReset"
}
}
if (["signup_complete", "signed_up"].includes(result)) {
return {
action: redirectTo ? "continue" : "signIn",
flow,
intent: "success",
reason: "signupComplete",
redirectTo
}
}
if (fallbackIntent === "danger") {
return {
action: "signIn",
flow,
intent: "danger",
reason: "genericError"
}
}
return {
action: redirectTo ? "continue" : "signIn",
flow,
intent: "success",
reason: "genericSuccess",
redirectTo
}
}
/** Resolve localized copy for a classified callback result. */
export function getAuthResultMessage(
result: AuthResult,
localization: Localization
): AuthResultMessage {
const auth = localization.auth
const messages: Record<AuthResultReason, AuthResultMessage> = {
accountLinkConflict: {
description: auth.callbackAccountLinkConflictDescription,
title: auth.callbackAccountLinkConflictTitle
},
accountLinked: {
description: auth.callbackAccountLinkedDescription,
title: auth.callbackAccountLinkedTitle
},
callbackFailed: {
description: auth.callbackFailedDescription,
title: auth.callbackFailedTitle
},
cancelled: {
description: auth.callbackCancelledDescription,
title: auth.callbackCancelledTitle
},
emailNotVerified: {
description: auth.callbackEmailNotVerifiedDescription,
title: auth.callbackEmailNotVerifiedTitle
},
emailVerified: {
description: auth.callbackEmailVerifiedDescription,
title: auth.callbackEmailVerifiedTitle
},
expiredLink: {
description: auth.callbackExpiredLinkDescription,
title: auth.callbackExpiredLinkTitle
},
genericError: {
description: auth.callbackGenericErrorDescription,
title: auth.callbackGenericErrorTitle
},
genericSuccess: {
description: auth.callbackGenericSuccessDescription,
title: auth.callbackGenericSuccessTitle
},
missingEmail: {
description: auth.callbackMissingEmailDescription,
title: auth.callbackMissingEmailTitle
},
passwordReset: {
description: auth.callbackPasswordResetDescription,
title: auth.callbackPasswordResetTitle
},
signupComplete: {
description: auth.callbackSignupCompleteDescription,
title: auth.callbackSignupCompleteTitle
},
signupDisabled: {
description: auth.callbackSignupDisabledDescription,
title: auth.callbackSignupDisabledTitle
}
}
return messages[result.reason]
}