UNPKG

@leancodepl/kratos

Version:

Headless React components library for building Ory Kratos authentication flows

638 lines (515 loc) 15.6 kB
# @leancodepl/kratos Headless React components library for building Ory Kratos authentication flows. ## Installation ```bash npm install @leancodepl/kratos # or yarn add @leancodepl/kratos ``` ## API ### `mkKratos(queryClient, basePath, traits, SessionManager)` Creates a Kratos client factory with authentication flows, session management, and React providers. **Parameters:** - `queryClient: QueryClient` - React Query client instance for managing server state - `basePath: string` - Base URL for the Kratos API server - `traits?: TTraitsConfig` - Optional traits configuration object for user schema validation - `SessionManager?: new (props: BaseSessionManagerContructorProps) => TSessionManager` - Optional session manager constructor, defaults to BaseSessionManager **Returns:** Object with the following structure: #### `flows` Authentication flow components and hooks: - `useLogout()` - Hook providing logout functionality with optional returnTo parameter - `LoginFlow` - Complete login flow component with multi-step authentication support - `RecoveryFlow` - Password recovery flow component with email verification and reset - `RegistrationFlow` - User registration flow component with traits collection and verification - `SettingsFlow` - User settings flow component for account management and profile updates - `VerificationFlow` - Email verification flow component #### `providers` React context providers for the application: - `KratosProviders` - Composite provider that wraps your app with necessary Kratos context - Includes `KratosClientProvider` for API access - Includes `KratosSessionProvider` for session management #### `session` Session management utilities: - `sessionManager` - Session manager instance with methods and hooks for: - **Async Methods**: `getSession()`, `getIdentity()`, `getUserId()`, `isLoggedIn()`, `checkIfLoggedIn()` - **React Hooks**: `useSession()`, `useIdentity()`, `useUserId()`, `useIsLoggedIn()`, `useIsAal2Required()` - **Extensible**: Can be extended to provide trait-specific methods like `useEmail()`, `useFirstName()` for typed user data access ### `BaseSessionManager(queryClient, api)` Manages Ory Kratos session and identity state with React Query integration. **Parameters:** - `queryClient: QueryClient` - React Query `QueryClient` instance for caching and fetching session data - `api: FrontendApi` - Ory Kratos `FrontendApi` instance for session and identity requests ## Usage Examples ### Basic Setup To use the library, you need to create new instance of Kratos client with `mkKratos` factory: ```typescript // traits.ts export const traitsConfig = { Email: { trait: "email", type: "string" }, GivenName: { trait: "given_name", type: "string" }, RegulationsAccepted: { trait: "regulations_accepted", type: "boolean" }, } as const export type AuthTraitsConfig = typeof traitsConfig ``` ```typescript // kratosService.ts import { environment } from "./environments" import { queryClient } from "./queryService" import { traitsConfig } from "./traits" const { session: { sessionManager }, providers: { KratosProviders }, flows: { LoginFlow, RegistrationFlow, SettingsFlow, VerificationFlow, RecoveryFlow, useLogout }, } = mkKratos({ queryClient, basePath: environment.authUrl, traits: traitsConfig, }) // session export { sessionManager } // providers export { KratosProviders } // flows export { LoginFlow, RecoveryFlow, RegistrationFlow, SettingsFlow, useLogout, VerificationFlow } ``` And then wrap your app with `KratosProviders` from `mkKratos`: ```tsx // main.tsx import { QueryClientProvider } from "@tanstack/react-query" import { KratosProviders } from "./kratosService" import { queryClient } from "./queryService" function App() { return ( <QueryClientProvider client={queryClient}> <KratosProviders> <Routes> <Route path="/login" element={<LoginPage />} /> <Route path="/register" element={<RegisterPage />} /> <Route path="/settings" element={<SettingsPage />} /> </Routes> </KratosProviders> </QueryClientProvider> ) } ``` ### Extending session manager You can add new functionalities to the session manager by extending `BaseSessionManager` class: ```typescript // session.ts import { BaseSessionManager } from "@leancodepl/kratos" import { queryClient } from "./queryService" import type { AuthTraitsConfig } from "./traits" export class SessionManager extends BaseSessionManager<AuthTraitsConfig> { getTraits = async () => { const identity = await this.getIdentity() return identity?.traits } getEmail = async () => { const traits = await this.getTraits() return traits?.email } // Hooks for React components useTraits = () => { const { identity, isLoading, error } = this.useIdentity() return { traits: identity?.traits, isLoading, error, } } useEmail = () => { const { traits, isLoading, error } = this.useTraits() return { email: traits?.email, isLoading, error, } } } ``` ```typescript // kratosService.ts import { environment } from "./environments" import { queryClient } from "./queryService" import { SessionManager } from "./session" import { traitsConfig } from "./traits" const { session: { sessionManager }, providers: { KratosProviders }, flows: { LoginFlow, RegistrationFlow, SettingsFlow, VerificationFlow, RecoveryFlow, useLogout }, } = mkKratos({ queryClient, basePath: environment.authUrl, traits: traitsConfig, SessionManager, }) ``` ### Session Management ```tsx import { sessionManager } from "./kratosService" function UserProfile() { const { isLoggedIn, isLoading } = sessionManager.useIsLoggedIn() const { userId } = sessionManager.useUserId() const { email } = sessionManager.useEmail() const { firstName } = sessionManager.useFirstName() if (isLoading) return <div>Loading...</div> if (!isLoggedIn) return <div>Not logged in</div> return ( <div> <h1>User Profile</h1> <p>ID: {userId}</p> <p>Email: {email}</p> <p>Name: {firstName}</p> </div> ) } ``` ### Login Flow ```tsx import { LoginFlow } from "./kratosService" function LoginPage() { return ( <LoginFlow loaderComponent={Loader} chooseMethodForm={ChooseMethodForm} secondFactorForm={SecondFactorForm} secondFactorEmailForm={SecondFactorEmailForm} emailVerificationForm={EmailVerificationForm} returnTo="/identity" onLoginSuccess={() => { console.log("Login successful") }} onSessionAlreadyAvailable={() => { location.href = "/identity" }} onError={({ target, errors }) => { console.error(`Error in ${target}:`, errors) }} /> ) } ``` ### Registration Flow ```tsx import { RegistrationFlow } from "./kratosService" function RegisterPage() { return ( <RegistrationFlow chooseMethodForm={ChooseMethodForm} emailVerificationForm={EmailVerificationForm} traitsForm={TraitsForm} returnTo="/welcome" onRegistrationSuccess={() => { console.log("Registration successful") }} onVerificationSuccess={() => { console.log("Email verified") }} onError={({ target, errors }) => { console.error(`Registration error in ${target}:`, errors) }} /> ) } ``` ### Settings Flow ```tsx import { SettingsFlow } from "./kratosService" function SettingsPage() { const { isLoggedIn, isLoading } = sessionManager.useIsLoggedIn() if (isLoading) return <div>Loading...</div> if (!isLoggedIn) return <div>Access denied</div> return ( <SettingsFlow newPasswordForm={NewPasswordForm} traitsForm={TraitsForm} passkeysForm={PasskeysForm} totpForm={TotpForm} oidcForm={OidcForm} settingsForm={({ traitsForm, newPasswordForm, passkeysForm }) => ( <div> {traitsForm} {newPasswordForm} {passkeysForm} </div> )} onChangePasswordSuccess={() => { console.log("Password updated") }} onChangeTraitsSuccess={() => { console.log("Profile updated") }} /> ) } ``` ### Logout Functionality ```tsx import { useLogout } from "./kratosService" function LogoutButton() { const { logout } = useLogout() const handleLogout = async () => { const result = await logout({ returnTo: "/login" }) if (result.isSuccess) { console.log("Logout successful") } else { console.error("Logout failed:", result.error) } } return <button onClick={handleLogout}>Logout</button> } ``` ### Recovery Flow ```tsx import { RecoveryFlow } from "./kratosService" function RecoveryPage() { return ( <RecoveryFlow emailForm={EmailForm} codeForm={CodeForm} newPasswordForm={NewPasswordForm} onRecoverySuccess={() => { console.log("Password recovery completed") }} onError={error => { console.error("Recovery failed:", error) }} /> ) } ``` ### Custom Error Handling ```typescript import { AuthError } from "@leancodepl/kratos" function getErrorMessage(error: AuthError) { switch (error.id) { case "Error_InvalidCredentials": return "Invalid email or password" case "Error_MissingProperty": return "This field is required" case "Error_TooShort": return "Password is too short" default: return "An error occurred" } } function handleError({ target, errors }) { if (target === "root") { console.error("Form errors:", errors.map(getErrorMessage)) } else { console.error(`Field ${target} errors:`, errors.map(getErrorMessage)) } } ``` ### Implementing flow form Each form in every flow exposes [slot components](https://www.radix-ui.com/primitives/docs/utilities/slot) for inputs and buttons. When wrapping your custom components with these slots, relevant props (such as the `onInput` event handler) are automatically applied. If you use a custom component as a child, you receive props from one of the following types: `CommonInputFieldProps`, `CommonCheckboxFieldProps`, or `CommonButtonProps`. Forms like `TraitsForm` have dynamic parameters determined by the `traitsConfig` you provide to the `mkKratos` factory. To type these correctly, use a generic such as `registrationFlow.TraitsFormProps<AuthTraitsConfig>` and pass your traits config. Some forms are typed as discriminated unions with multiple variants. #### Custom Input component with CommonInputFieldProps ```tsx import { CommonInputFieldProps } from "@leancodepl/kratos" import { getErrorMessage } from "./kratosService" type InputProps = CommonInputFieldProps & { placeholder?: string } export const Input = ({ errors, ...props }: InputProps) => ( <div> <input {...props} /> {errors && errors.length > 0 && ( <div> {errors.map(error => ( <div key={error.id}>{getErrorMessage(error)}</div> ))} </div> )} </div> ) ``` #### ChooseMethodForm in login flow ```tsx import { loginFlow } from "@leancodepl/kratos" import { LoginFlow, getErrorMessage } from "./kratosService" import type { AuthTraitsConfig } from "./traits" function LoginPage() { return ( <LoginFlow chooseMethodForm={ChooseMethodForm} // other props /> ) } function ChooseMethodForm(props: loginFlow.ChooseMethodFormProps) { const { errors, isSubmitting, isValidating } = props if (props.isRefresh) { const { passwordFields, Google, Passkey, Apple, Facebook, identifier } = props return ( <div> {identifier && ( <h2> Complete login process as <strong>{identifier}</strong> </h2> )} {passwordFields && ( <> <passwordFields.Password> <input placeholder="Password" /> </passwordFields.Password> <passwordFields.Submit> <button>Login</button> </passwordFields.Submit> </> )} {Google && ( <Google> <button>Sign in with Google</button> </Google> )} {Apple && ( <Apple> <button>Sign in with Apple</button> </Apple> )} {Facebook && ( <Facebook> <button>Sign in with Facebook</button> </Facebook> )} {Passkey && ( <Passkey> <button>Sign in with Passkey</button> </Passkey> )} {errors && errors.length > 0 && ( <div> {errors.map(error => ( <div key={error.id}>{getErrorMessage(error)}</div> ))} </div> )} </div> ) } const { passwordFields: { Identifier, Password, Submit }, Google, Passkey, Apple, Facebook, } = props return ( <div> <Identifier> <input placeholder="Identifier" /> </Identifier> <Password> <input placeholder="Password" /> </Password> <Submit> <button>Login</button> </Submit> <Google> <button>Sign in with Google</button> </Google> <Apple> <button>Sign in with Apple</button> </Apple> <Facebook> <button>Sign in with Facebook</button> </Facebook> <Passkey> <button>Sign in with Passkey</button> </Passkey> {errors && errors.length > 0 && ( <div> {errors.map(error => ( <div key={error.id}>{getErrorMessage(error)}</div> ))} </div> )} </div> ) } ``` #### TraitsForm in registration flow ```tsx import { registrationFlow } from "@leancodepl/kratos" import { RegistrationFlow, getErrorMessage } from "./kratosService" import type { AuthTraitsConfig } from "./traits" function RegisterPage() { return ( <RegistrationFlow traitsForm={TraitsForm} onError={handleError} // other props /> ) } function TraitsForm({ errors, Email, RegulationsAccepted, GivenName, Google, Apple, Facebook, isSubmitting, isValidating, }: registrationFlow.TraitsFormProps<AuthTraitsConfig>) { return ( <div> {Email && ( <Email> <input placeholder="Email" /> </Email> )} {GivenName && ( <GivenName> <input placeholder="First name" /> </GivenName> )} {RegulationsAccepted && ( <RegulationsAccepted> <input placeholder="Regulations accepted" type="checkbox"> I accept the regulations </input> </RegulationsAccepted> )} <button type="submit">Register</button> {Google && ( <Google> <button>Sign up with Google</button> </Google> )} {Apple && ( <Apple> <button>Sign up with Apple</button> </Apple> )} {Facebook && ( <Facebook> <button>Sign up with Facebook</button> </Facebook> )} {errors && errors.length > 0 && ( <div> {errors.map(error => ( <div key={error.id}>{getErrorMessage(error)}</div> ))} </div> )} </div> ) } const handleError: registrationFlow.OnRegistrationFlowError<AuthTraitsConfig> = ({ target, errors }) => { if (target === "root") { console.error("Form errors:", errors.map(getErrorMessage)) } else { console.error(`Field ${target} errors:`, errors.map(getErrorMessage)) } } ```