@tendant/simple-idm-solid
v2.0.8
Published
SolidJS authentication components for simple-idm
Downloads
1,661
Maintainers
Readme
simple-idm-solid
SolidJS authentication components for simple-idm
Overview
simple-idm-solid is an open-source SolidJS UI component library providing ready-to-use authentication UIs — login, registration, password reset, and MFA — designed for seamless integration with simple-idm, a Go-based identity management and OIDC provider.
This project enables developers to easily embed identity workflows into any SolidJS, SolidStart, or Astro application without rebuilding authentication flows.
Two Ways to Use
🎨 Styled Components (Quick Start)
Pre-built, styled components ready to drop into your app:
import { LoginForm } from '@tendant/simple-idm-solid';
<LoginForm apiBaseUrl="http://localhost:4000" onSuccess={...} />🎯 Headless Hooks (Custom UI)
Business logic without UI for complete design control:
import { useLogin } from '@tendant/simple-idm-solid';
const login = useLogin({ client: 'http://localhost:4000' });
// Build your own UI with 100% control→ See Migration Guide to choose the right approach
Features
- 🔐 Complete Auth Components: Login, Magic Link, Registration (passwordless & password)
- 🔑 Password Reset: Forgot password and reset password flows with token validation
- 👤 Profile Management: Update username, phone, and password with validation
- 🔒 Two-Factor Authentication: TOTP, SMS, and email 2FA setup and management
- ✉️ Email Verification: Token validation and verification status tracking
- 🎯 Headless Hooks: Business logic without UI for custom designs
- 🎨 Styled Components: Ready-to-use with Tailwind CSS
- 📦 Lightweight: <50KB gzipped
- 🔒 Secure: Built for HTTP-only cookie authentication
- ♿ Accessible: WCAG AA compliant styled components
- 📘 TypeScript: Full type safety
- 🧪 Testable: Easy to test with mocked APIs
- 🚀 Zero Config: Works with simple-idm out of the box
Table of Contents
- Installation
- Quick Start
- Type System Guide ⭐
- Styled Components
- Headless Hooks
- API Client
- Hooks
- Examples
- Migration Guide
- Customization
Installation
npm install @tendant/simple-idm-solid
# or
pnpm add @tendant/simple-idm-solid
# or
yarn add @tendant/simple-idm-solidQuick Start
Same Origin Setup (Recommended)
If your frontend and backend are served from the same domain (e.g., behind a reverse proxy), you don't need to specify the API URL:
import { LoginForm } from '@tendant/simple-idm-solid';
import '@tendant/simple-idm-solid/styles';
function LoginPage() {
return (
<LoginForm
onSuccess={(response) => {
console.log('Login successful!', response);
window.location.href = '/dashboard';
}}
/>
);
}No apiBaseUrl needed! The library uses relative URLs automatically.
Type System Guide
⭐ Important: This library uses two distinct type systems - OIDC standard types and simple-idm custom types.
Quick Reference:
UserInfo(OIDC standard) - From/oauth2/userinfoendpoint{ sub, email, name, preferred_username }IdmUser(simple-idm custom) - From login and custom endpoints{ id, email, name, roles }
📖 See TYPE_SYSTEM.md for complete guide
Different Origin Setup
If your API is on a different domain/port (e.g., development):
import { LoginForm } from '@tendant/simple-idm-solid';
import '@tendant/simple-idm-solid/styles';
function LoginPage() {
return (
<LoginForm
apiBaseUrl="http://localhost:4000"
onSuccess={(response) => {
console.log('Login successful!', response);
window.location.href = '/dashboard';
}}
showMagicLinkOption
showRegistrationLink
/>
);
}Styled Components
Pre-built, styled components ready to use.
LoginForm
Username/password login form with HTTP-only cookie authentication.
import { LoginForm } from '@tendant/simple-idm-solid';
<LoginForm
apiBaseUrl="http://localhost:4000"
onSuccess={(response) => {
// Handle successful login
window.location.href = '/dashboard';
}}
onError={(error) => {
console.error('Login failed:', error);
}}
redirectUrl="/dashboard"
showMagicLinkOption
showRegistrationLink
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendonSuccess?: (response: LoginResponse) => void: Success callbackonError?: (error: string) => void: Error callbackredirectUrl?: string: Auto-redirect after loginshowMagicLinkOption?: boolean: Show magic link login optionshowRegistrationLink?: boolean: Show registration link
MagicLinkForm
Request a magic link for passwordless authentication.
import { MagicLinkForm } from '@tendant/simple-idm-solid';
<MagicLinkForm
apiBaseUrl="http://localhost:4000"
onSuccess={() => {
console.log('Magic link sent!');
}}
showPasswordLoginLink
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendonSuccess?: () => void: Success callback (link sent)onError?: (error: string) => void: Error callbackshowPasswordLoginLink?: boolean: Show back to password login link
MagicLinkValidate
Validate a magic link token (from email).
import { MagicLinkValidate } from '@tendant/simple-idm-solid';
import { useSearchParams } from '@solidjs/router';
function MagicLinkValidatePage() {
const [searchParams] = useSearchParams();
const token = searchParams.token as string;
return (
<MagicLinkValidate
apiBaseUrl="http://localhost:4000"
token={token}
onSuccess={(response) => {
console.log('Logged in!', response);
}}
redirectUrl="/dashboard"
autoValidate // Automatically validate on mount
/>
);
}Props:
apiBaseUrl(required): Base URL of simple-idm backendtoken(required): Magic link token from URLonSuccess?: (response: MagicLinkValidateResponse) => void: Success callbackonError?: (error: string) => void: Error callbackautoValidate?: boolean: Auto-validate on mount (default: true)redirectUrl?: string: Auto-redirect after validation
PasswordlessRegistrationForm
Register without a password (uses magic link).
import { PasswordlessRegistrationForm } from '@tendant/simple-idm-solid';
<PasswordlessRegistrationForm
apiBaseUrl="http://localhost:4000"
onSuccess={(response) => {
console.log('Account created!', response);
}}
requireInvitationCode={false}
showLoginLink
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendonSuccess?: (response: SignupResponse) => void: Success callbackonError?: (error: string) => void: Error callbackrequireInvitationCode?: boolean: Require invitation codeshowLoginLink?: boolean: Show login linkredirectUrl?: string: Auto-redirect after registration
PasswordRegistrationForm
Register with username and password.
import { PasswordRegistrationForm } from '@tendant/simple-idm-solid';
<PasswordRegistrationForm
apiBaseUrl="http://localhost:4000"
onSuccess={(response) => {
console.log('Account created!', response);
window.location.href = '/login';
}}
requireInvitationCode={false}
showLoginLink
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendonSuccess?: (response: SignupResponse) => void: Success callbackonError?: (error: string) => void: Error callbackrequireInvitationCode?: boolean: Require invitation codeshowLoginLink?: boolean: Show login linkredirectUrl?: string: Auto-redirect after registration
ProfileSettingsForm
Manage user profile with tabbed interface for username, phone, and password updates.
import { ProfileSettingsForm } from '@tendant/simple-idm-solid';
<ProfileSettingsForm
apiBaseUrl="http://localhost:4000"
onSuccess={(response, operation) => {
console.log(`${operation} updated!`, response);
}}
defaultTab="username"
showPhoneTab={true}
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendonSuccess?: (response, operation) => void: Success callback with operation typeonError?: (error, operation) => void: Error callback with operation typedefaultTab?: 'username' | 'phone' | 'password': Initial active tabshowPhoneTab?: boolean: Show phone tab (default: true)
Features:
- ✓ Tabbed interface for Username / Phone / Password
- ✓ Password strength indicator
- ✓ Validation for each form
- ✓ Success/error feedback per operation
TwoFactorAuthSetup
Complete 2FA setup wizard with QR code display and status management.
import { TwoFactorAuthSetup } from '@tendant/simple-idm-solid';
<TwoFactorAuthSetup
apiBaseUrl="http://localhost:4000"
onSuccess={(response, operation) => {
console.log(`2FA ${operation} successful!`, response);
}}
autoLoadStatus={true}
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendonSuccess?: (response, operation) => void: Success callback with operation typeonError?: (error, operation) => void: Error callback with operation typeautoLoadStatus?: boolean: Auto-load 2FA status on mount (default: true)
Features:
- ✓ Multi-step wizard (Status → Setup → Verify)
- ✓ QR code display for TOTP
- ✓ Backup codes display
- ✓ Enable/disable toggle
- ✓ Status badge showing current state
EmailVerificationPage
Email verification page with auto-verification from URL token.
import { EmailVerificationPage } from '@tendant/simple-idm-solid';
import { useSearchParams } from '@solidjs/router';
function VerifyEmail() {
const [params] = useSearchParams();
return (
<EmailVerificationPage
apiBaseUrl="http://localhost:4000"
token={params.token}
autoVerify={true}
onSuccess={(response) => {
console.log('Email verified!', response);
}}
loginUrl="/login"
/>
);
}Props:
apiBaseUrl(required): Base URL of simple-idm backendtoken?: string: Verification token from URL query parameterautoVerify?: boolean: Auto-verify on mount if token provided (default: true)onSuccess?: (response) => void: Success callbackonError?: (error) => void: Error callbackloginUrl?: string: URL to login page (default: /login)
Features:
- ✓ Auto-verification from URL token
- ✓ Loading/success/error states with icons
- ✓ Resend email button
- ✓ Manual token entry fallback
ForgotPasswordForm
Request a password reset link via email or username.
import { ForgotPasswordForm } from '@tendant/simple-idm-solid';
<ForgotPasswordForm
apiBaseUrl="http://localhost:4000"
method="email"
onSuccess={(response) => {
console.log('Password reset email sent!', response);
}}
loginUrl="/login"
/>Props:
apiBaseUrl(required): Base URL of simple-idm backendmethod?: 'email' | 'username' | 'both': Input method (default: 'email')onSuccess?: (response) => void: Success callbackonError?: (error) => void: Error callbackloginUrl?: string: URL to login page (default: /login)
Features:
- ✓ Email or username input validation
- ✓ Success message with instructions
- ✓ Error handling with retry
- ✓ Back to login link
ResetPasswordForm
Reset password using token from email with strength validation.
import { ResetPasswordForm } from '@tendant/simple-idm-solid';
import { useSearchParams } from '@solidjs/router';
function ResetPassword() {
const [params] = useSearchParams();
return (
<ResetPasswordForm
apiBaseUrl="http://localhost:4000"
token={params.token}
autoLoadPolicy={true}
onSuccess={(response) => {
console.log('Password reset!', response);
window.location.href = '/login';
}}
loginUrl="/login"
/>
);
}Props:
apiBaseUrl(required): Base URL of simple-idm backendtoken?: string: Reset token from URL query parametershowTokenInput?: boolean: Show token input field (default: false if token provided)autoLoadPolicy?: boolean: Auto-load password policy (default: true)onSuccess?: (response) => void: Success callbackonError?: (error) => void: Error callbackloginUrl?: string: URL to login page (default: /login)
Features:
- ✓ Password strength indicator
- ✓ Password policy validation from backend
- ✓ Confirmation matching
- ✓ Success state with redirect to login
- ✓ Manual token entry fallback
Headless Hooks
Headless hooks provide business logic without UI, giving you 100% control over the presentation layer.
Why use headless hooks?
- ✅ Complete UI customization
- ✅ Works with any CSS framework (Tailwind, UnoCSS, vanilla CSS)
- ✅ Smaller bundle size (logic only)
- ✅ Better testability
- ✅ Reuse logic across different UIs
useLogin
Password-based login hook with 2FA and multi-user support.
import { useLogin } from '@tendant/simple-idm-solid';
function MyCustomLogin() {
const login = useLogin({
client: 'http://localhost:4000',
onSuccess: (response) => {
console.log('Logged in!', response);
window.location.href = '/dashboard';
},
autoRedirect: true,
redirectUrl: '/dashboard',
});
return (
<div class="my-custom-design">
<input
value={login.username()}
onInput={(e) => login.setUsername(e.currentTarget.value)}
placeholder="Username"
/>
<input
type="password"
value={login.password()}
onInput={(e) => login.setPassword(e.currentTarget.value)}
placeholder="Password"
/>
<button
onClick={() => login.submit()}
disabled={!login.canSubmit() || login.isLoading()}
>
{login.isLoading() ? 'Signing in...' : 'Sign In'}
</button>
{login.error() && <div class="error">{login.error()}</div>}
</div>
);
}Config:
client: SimpleIdmClient instance or base URL stringonSuccess?: (response: LoginResponse) => void: Success callbackonError?: (error: string) => void: Error callbackautoRedirect?: boolean: Auto-redirect after loginredirectUrl?: string: URL to redirect toredirectDelay?: number: Delay before redirect (ms)
Returns:
username(),setUsername(value: string): Username statepassword(),setPassword(value: string): Password stateisLoading(): Whether login is in progresserror(): Error message if login failedsuccess(): Success message if login succeededresponse(): Full login response (includes 2FA, multiple users)submit(): Submit loginreset(): Reset form statecanSubmit(): Whether form can be submitted
useMagicLink
Passwordless authentication hook with cooldown timer.
import { useMagicLink } from '@tendant/simple-idm-solid';
import { Show } from 'solid-js';
function MyCustomMagicLink() {
const magic = useMagicLink({
client: 'http://localhost:4000',
cooldownSeconds: 60,
});
return (
<div>
<Show when={!magic.success()} fallback={<p>Check your email!</p>}>
<input
type="email"
value={magic.username()}
onInput={(e) => magic.setUsername(e.currentTarget.value)}
placeholder="Email"
/>
<button
onClick={() => magic.submit()}
disabled={!magic.canSubmit()}
>
{magic.cooldown() > 0
? `Wait ${magic.cooldown()}s`
: 'Send Magic Link'}
</button>
</Show>
{magic.error() && <p class="error">{magic.error()}</p>}
<Show when={magic.success() && magic.canResend()}>
<button onClick={() => magic.resend()}>Resend</button>
</Show>
</div>
);
}Config:
client: SimpleIdmClient instance or base URL stringonSuccess?: (response: MagicLinkResponse) => void: Success callbackonError?: (error: string) => void: Error callbackcooldownSeconds?: number: Cooldown duration (default: 60)
Returns:
username(),setUsername(value: string): Email/username stateisLoading(): Whether request is in progresserror(),success(): Error/success messagesresponse(): Full API responsecooldown(): Seconds remaining before resend allowedsubmit(): Send magic linkresend(): Resend magic link (after cooldown)reset(): Reset form statecanSubmit(),canResend(): Validation helpers
useRegistration
User registration hook supporting both password and passwordless modes.
import { useRegistration } from '@tendant/simple-idm-solid';
import { Show } from 'solid-js';
function MyCustomRegistration() {
const reg = useRegistration({
client: 'http://localhost:4000',
mode: 'password', // or 'passwordless'
onSuccess: () => window.location.href = '/login',
});
return (
<form onSubmit={(e) => { e.preventDefault(); reg.submit(); }}>
<input
value={reg.username()}
onInput={(e) => reg.setUsername(e.currentTarget.value)}
placeholder="Username"
/>
<input
type="email"
value={reg.email()}
onInput={(e) => reg.setEmail(e.currentTarget.value)}
placeholder="Email"
/>
{/* Password mode only */}
<Show when={reg.mode === 'password'}>
<input
type="password"
value={reg.password()}
onInput={(e) => reg.setPassword(e.currentTarget.value)}
placeholder="Password"
/>
{/* Password Strength */}
<Show when={reg.password()}>
<div class="strength-indicator">
<div
class={reg.passwordStrength().color}
style={{ width: `${reg.passwordStrength().percentage}%` }}
/>
<span>{reg.passwordStrength().text}</span>
</div>
</Show>
<input
type="password"
value={reg.confirmPassword()}
onInput={(e) => reg.setConfirmPassword(e.currentTarget.value)}
placeholder="Confirm Password"
/>
<Show when={!reg.passwordsMatch() && reg.confirmPassword()}>
<p class="error">Passwords do not match</p>
</Show>
</Show>
<button disabled={!reg.canSubmit()}>Register</button>
</form>
);
}Config:
client: SimpleIdmClient instance or base URL stringmode?: 'password' | 'passwordless': Registration mode (default: 'password')onSuccess?: (response: SignupResponse) => void: Success callbackonError?: (error: string) => void: Error callbackrequireInvitationCode?: boolean: Whether invitation code is requiredautoRedirect?: boolean: Auto-redirect after registrationredirectUrl?: string: URL to redirect to
Returns:
username(),setUsername(value): Username stateemail(),setEmail(value): Email statepassword(),setPassword(value): Password stateconfirmPassword(),setConfirmPassword(value): Confirm password statefullname(),setFullname(value): Full name stateinvitationCode(),setInvitationCode(value): Invitation code stateisLoading(): Whether registration is in progresserror(),success(): Error/success messagespasswordStrength(): Password strength calculationpercentage: number: Strength 0-100level: 'weak' | 'medium' | 'strong'color: string: CSS class for colortext: string: Display text
passwordsMatch(): Whether passwords matchsubmit(): Submit registrationreset(): Reset form statecanSubmit(): Whether form can be submitted
useProfile
Profile management hook for updating username, phone, and password.
import { useProfile } from '@tendant/simple-idm-solid';
import { Show, createSignal } from 'solid-js';
function MyProfileSettings() {
const profile = useProfile({
client: 'http://localhost:4000',
onSuccess: (response, operation) => {
console.log(`${operation} updated!`, response);
},
});
const [tab, setTab] = createSignal<'username' | 'phone' | 'password'>('username');
return (
<div>
{/* Update Username */}
<Show when={tab() === 'username'}>
<form onSubmit={(e) => { e.preventDefault(); profile.updateUsername(); }}>
<input
value={profile.username()}
onInput={(e) => profile.setUsername(e.currentTarget.value)}
placeholder="New Username"
/>
<input
type="password"
value={profile.usernameCurrentPassword()}
onInput={(e) => profile.setUsernameCurrentPassword(e.currentTarget.value)}
placeholder="Current Password (required)"
/>
<button disabled={!profile.canSubmitUsername()}>
Update Username
</button>
</form>
</Show>
{/* Update Password */}
<Show when={tab() === 'password'}>
<form onSubmit={(e) => { e.preventDefault(); profile.updatePassword(); }}>
<input
type="password"
value={profile.currentPassword()}
onInput={(e) => profile.setCurrentPassword(e.currentTarget.value)}
placeholder="Current Password"
/>
<input
type="password"
value={profile.newPassword()}
onInput={(e) => profile.setNewPassword(e.currentTarget.value)}
placeholder="New Password"
/>
{/* Password Strength */}
<Show when={profile.newPassword()}>
<div>Strength: {profile.passwordStrength().text}</div>
</Show>
<input
type="password"
value={profile.confirmNewPassword()}
onInput={(e) => profile.setConfirmNewPassword(e.currentTarget.value)}
placeholder="Confirm New Password"
/>
<button disabled={!profile.canSubmitPassword()}>
Update Password
</button>
</form>
</Show>
</div>
);
}Config:
client: SimpleIdmClient instance or base URL stringonSuccess?: (response, operation) => void: Success callbackonError?: (error, operation) => void: Error callbackminPasswordLength?: number: Minimum password length (default: 8)
Returns:
username(),setUsername(value): New username stateusernameCurrentPassword(),setUsernameCurrentPassword(value): Current password for username updatephone(),setPhone(value): Phone number statecurrentPassword(),setCurrentPassword(value): Current password for password updatenewPassword(),setNewPassword(value): New password stateconfirmNewPassword(),setConfirmNewPassword(value): Confirm new password stateisLoading(): Whether update is in progresserror(),success(): Error/success messagescurrentOperation(): Current operation ('username', 'phone', or 'password')passwordStrength(): New password strength calculationpasswordsMatch(): Whether new passwords matchupdateUsername(),updatePhone(),updatePassword(): Submit updatescanSubmitUsername(),canSubmitPhone(),canSubmitPassword(): Validation helpers
use2FA
Two-factor authentication setup and management hook supporting TOTP, SMS, and email.
import { use2FA } from '@tendant/simple-idm-solid';
import { Show } from 'solid-js';
function My2FASetup() {
const twoFA = use2FA({
client: 'http://localhost:4000',
autoLoadStatus: true,
onSuccess: (response, operation) => {
console.log(`2FA ${operation} successful!`, response);
},
});
return (
<div>
{/* Current Status */}
<p>2FA Enabled: {twoFA.isEnabled() ? 'Yes' : 'No'}</p>
{/* Setup TOTP */}
<Show when={!twoFA.isEnabled()}>
<button onClick={() => twoFA.setupTOTP()}>
Setup Authenticator App
</button>
<Show when={twoFA.qrCode()}>
<div>
<img src={twoFA.qrCode()!} alt="QR Code" />
<p>Secret: {twoFA.secret()}</p>
<input
value={twoFA.code()}
onInput={(e) => twoFA.setCode(e.currentTarget.value)}
placeholder="Enter code from app"
/>
<button onClick={() => twoFA.enable()} disabled={!twoFA.canEnable()}>
Enable 2FA
</button>
</div>
</Show>
</Show>
{/* Disable 2FA */}
<Show when={twoFA.isEnabled()}>
{twoFA.enabledTypes().map((type) => (
<button onClick={() => twoFA.disable(type as any)}>
Disable {type}
</button>
))}
</Show>
</div>
);
}Config:
client: SimpleIdmClient instance or base URL stringonSuccess?: (response, operation) => void: Success callbackonError?: (error, operation) => void: Error callbackautoLoadStatus?: boolean: Auto-load status on mount (default: true)
Returns:
status(): Current 2FA statusisEnabled(): Whether 2FA is enabledenabledTypes(): Array of enabled 2FA typessetupData(): TOTP setup responseqrCode(): Base64 QR code for TOTPsecret(): TOTP secret for manual entrybackupCodes(): Backup codes from setuptype(),setType(type): Current 2FA type ('totp', 'sms', 'email')code(),setCode(value): Verification code statedeliveryOption(),setDeliveryOption(value): Phone/email for SMS/email 2FAisLoading(): Whether operation is in progresserror(),success(): Error/success messagesloadStatus(): Load current 2FA statussetupTOTP(): Setup TOTP 2FA (generates QR code)enable(): Enable 2FA after setupdisable(type): Disable specific 2FA typesendCode(): Send 2FA code via SMS/emailvalidate(): Validate 2FA codecanEnable(),canSendCode(),canValidate(): Validation helpers
useEmailVerification
Email verification hook for token validation, resending emails, and checking status.
import { useEmailVerification } from '@tendant/simple-idm-solid';
import { useSearchParams } from '@solidjs/router';
import { Show } from 'solid-js';
// Auto-verify from URL token
function EmailVerifyPage() {
const [params] = useSearchParams();
const emailVerify = useEmailVerification({
client: 'http://localhost:4000',
initialToken: params.token,
autoVerify: true,
});
return (
<div>
<Show when={emailVerify.isLoading()}>
<p>Verifying your email...</p>
</Show>
<Show when={emailVerify.success()}>
<div>
<h2>Email Verified!</h2>
<p>{emailVerify.success()}</p>
<a href="/login">Continue to Login</a>
</div>
</Show>
<Show when={emailVerify.error()}>
<div>
<p>{emailVerify.error()}</p>
<button onClick={() => emailVerify.resend()}>
Resend Verification Email
</button>
</div>
</Show>
</div>
);
}
// Check status widget
function EmailStatusWidget() {
const emailVerify = useEmailVerification({
client: 'http://localhost:4000',
autoLoadStatus: true,
});
return (
<div>
<Show when={emailVerify.isVerified()}>
<p>✓ Email verified</p>
</Show>
<Show when={!emailVerify.isVerified()}>
<button onClick={() => emailVerify.resend()}>
Send Verification Email
</button>
</Show>
</div>
);
}Config:
client: SimpleIdmClient instance or base URL stringonSuccess?: (response, operation) => void: Success callbackonError?: (error, operation) => void: Error callbackinitialToken?: string: Initial verification tokenautoVerify?: boolean: Auto-verify on mount if initialToken providedautoLoadStatus?: boolean: Auto-load verification status on mount
Returns:
token(),setToken(value): Verification token statestatus(): Verification status responseisVerified(): Whether email is verifiedverifiedAt(): When email was verified (ISO 8601 string)isLoading(): Whether operation is in progresserror(),success(): Error/success messagesverifyResponse(): Last verification responseverify(): Verify email with current tokenresend(): Resend verification email (requires auth)loadStatus(): Load verification status (requires auth)canVerify(): Whether verify form is valid
useForgotPassword
Password reset request hook for initiating password reset via email or username.
import { useForgotPassword } from '@tendant/simple-idm-solid';
import { Show } from 'solid-js';
function ForgotPasswordPage() {
const forgotPassword = useForgotPassword({
client: 'http://localhost:4000',
method: 'email', // or 'username' or 'both'
onSuccess: (response) => {
console.log('Reset email sent!', response);
},
});
return (
<form onSubmit={(e) => { e.preventDefault(); forgotPassword.submit(); }}>
<input
type={forgotPassword.method() === 'email' ? 'email' : 'text'}
value={forgotPassword.identifier()}
onInput={(e) => forgotPassword.setIdentifier(e.currentTarget.value)}
placeholder={forgotPassword.method() === 'email' ? '[email protected]' : 'username'}
/>
<button
type="submit"
disabled={!forgotPassword.canSubmit() || forgotPassword.isLoading()}
>
{forgotPassword.isLoading() ? 'Sending...' : 'Send Reset Link'}
</button>
<Show when={forgotPassword.error()}>
<p class="error">{forgotPassword.error()}</p>
</Show>
<Show when={forgotPassword.success()}>
<p class="success">{forgotPassword.success()}</p>
</Show>
</form>
);
}Config:
client: SimpleIdmClient instance or base URL stringmethod?: 'email' | 'username' | 'both': Input method (default: 'email')onSuccess?: (response) => void: Success callbackonError?: (error) => void: Error callback
Returns:
identifier(),setIdentifier(value): Email/username input stateisLoading(): Whether request is in progresserror(),success(): Error/success messagesresponse(): Last API responsesubmit(): Submit password reset requestreset(): Reset form statecanSubmit(): Whether form is validmethod(): Configured input method
useResetPassword
Password reset completion hook with token validation and password strength checking.
import { useResetPassword } from '@tendant/simple-idm-solid';
import { useSearchParams } from '@solidjs/router';
import { Show } from 'solid-js';
function ResetPasswordPage() {
const [params] = useSearchParams();
const resetPassword = useResetPassword({
client: 'http://localhost:4000',
initialToken: params.token,
autoLoadPolicy: true,
onSuccess: (response) => {
console.log('Password reset!', response);
window.location.href = '/login';
},
});
return (
<form onSubmit={(e) => { e.preventDefault(); resetPassword.submit(); }}>
<Show when={!params.token}>
<input
type="text"
value={resetPassword.token()}
onInput={(e) => resetPassword.setToken(e.currentTarget.value)}
placeholder="Reset token"
/>
</Show>
<input
type="password"
value={resetPassword.newPassword()}
onInput={(e) => resetPassword.setNewPassword(e.currentTarget.value)}
placeholder="New password"
/>
{/* Password Strength Indicator */}
<Show when={resetPassword.newPassword().length > 0}>
<div class="strength-indicator">
<span class={resetPassword.passwordStrength().color}>
{resetPassword.passwordStrength().label}
</span>
<div class="progress-bar">
<div style={{ width: `${resetPassword.passwordStrength().percentage}%` }} />
</div>
</div>
</Show>
<input
type="password"
value={resetPassword.confirmPassword()}
onInput={(e) => resetPassword.setConfirmPassword(e.currentTarget.value)}
placeholder="Confirm password"
/>
<Show when={!resetPassword.passwordsMatch() && resetPassword.confirmPassword()}>
<p class="error">Passwords do not match</p>
</Show>
<button
type="submit"
disabled={!resetPassword.canSubmit() || resetPassword.isLoading()}
>
{resetPassword.isLoading() ? 'Resetting...' : 'Reset Password'}
</button>
<Show when={resetPassword.error()}>
<p class="error">{resetPassword.error()}</p>
</Show>
<Show when={resetPassword.success()}>
<p class="success">{resetPassword.success()}</p>
</Show>
</form>
);
}Config:
client: SimpleIdmClient instance or base URL stringinitialToken?: string: Initial reset token from URLautoLoadPolicy?: boolean: Auto-load password policy (default: true)minPasswordLength?: number: Minimum password length (default: 8, overridden by policy)onSuccess?: (response) => void: Success callbackonError?: (error) => void: Error callback
Returns:
token(),setToken(value): Reset token statenewPassword(),setNewPassword(value): New password stateconfirmPassword(),setConfirmPassword(value): Confirmation stateisLoading(): Whether operation is in progresserror(),success(): Error/success messagesresponse(): Last API responsepolicy(): Password policy from backendpasswordStrength(): Password strength analysis (level, label, percentage, color)passwordsMatch(): Whether passwords matchmeetsPolicy(): Whether password meets policy requirementssubmit(): Submit password resetreset(): Reset form statecanSubmit(): Whether form is validloadPolicy(): Load password policy from API
Learn more:
API Client
Use the SimpleIdmClient directly for custom implementations:
import { SimpleIdmClient } from '@tendant/simple-idm-solid';
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
onUnauthorized: () => {
// Handle 401 errors (e.g., redirect to login)
window.location.href = '/login';
},
});
// Login
const response = await client.login({
username: '[email protected]',
password: 'password',
});
// Get current user
const user = await client.getCurrentUser();
// Logout
await client.logout();API Prefix Configuration
The SimpleIdmClient supports configurable endpoint prefixes for flexible API gateway routing and versioning.
Simple Configuration with Base Prefix (Recommended)
Set one prefix for all endpoints - the simplest way to configure:
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
basePrefix: '/api/v1/idm',
// All endpoints automatically use /api/v1/idm/* pattern:
// auth: /api/v1/idm/auth
// signup: /api/v1/idm/signup
// profile: /api/v1/idm/profile
// twoFA: /api/v1/idm/2fa
// email: /api/v1/idm/email
// passwordReset: /api/v1/idm/password-reset
// oauth2: /api/v1/idm/oauth2
});With selective overrides:
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
basePrefix: '/api/v1/idm',
prefixes: {
// Override just 2FA to route to a different service
twoFA: '/security-service/2fa',
},
});Default Configuration (v1)
By default (without any configuration), all endpoints use the v1 prefix pattern:
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
// Defaults to v1 prefixes:
// auth: /api/v1/idm/auth
// signup: /api/v1/idm/signup
// profile: /api/v1/idm/profile
// twoFA: /api/v1/idm/2fa
// email: /api/v1/idm/email
// passwordReset: /api/v1/idm/password-reset
// oauth2: /api/v1/oauth2
});Version-Based Configuration
Specify an API version for automatic prefix generation:
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
apiVersion: 'v2', // All endpoints will use /api/v2/idm/*
});Custom Prefix Configuration
Override specific prefixes for per-route-group customization:
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
prefixes: {
auth: '/custom/auth',
signup: '/custom/signup',
// Other prefixes use defaults
},
});Legacy Mode
For backward compatibility with pre-v2.0.0 simple-idm backends:
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
useLegacyPrefixes: true,
// Uses legacy prefixes including:
// twoFA: /idm/2fa (inconsistent - missing /api prefix)
});Note: Legacy mode includes the inconsistent 2FA prefix /idm/2fa/* instead of /api/idm/2fa/*. Use apiVersion: 'v1' or custom prefixes for new deployments.
Configuration Priority
When multiple options are specified, priority is:
- basePrefix (highest - simplest configuration)
- apiVersion
- useLegacyPrefixes
- prefixes (partial overrides - merges with above)
- DEFAULT_V1_PREFIXES (default)
const client = new SimpleIdmClient({
baseUrl: 'http://localhost:4000',
basePrefix: '/api/v1/idm', // This takes precedence
prefixes: {
auth: '/custom/auth', // This overrides just the auth route
},
});
// Result: auth = /custom/auth, others = /api/v1/idm/*API Gateway Integration
Configure prefixes to match your API gateway routing rules:
// Simple: All routes through one gateway path
const client = new SimpleIdmClient({
baseUrl: 'https://api.example.com',
basePrefix: '/gateway/idm',
// All routes: /gateway/idm/auth, /gateway/idm/signup, etc.
});
// Advanced: Route different features to different services
const client = new SimpleIdmClient({
baseUrl: 'https://api.example.com',
basePrefix: '/gateway/idm',
prefixes: {
// Override specific routes to different backend services
profile: '/user-service/profile',
twoFA: '/security-service/2fa',
},
});
// Kong/nginx Gateway example with version
const client = new SimpleIdmClient({
baseUrl: 'https://api.example.com',
apiVersion: 'v1',
// Uses consistent /api/v1/idm/* pattern for all routes
});Hooks
useAuth
Manage authentication state:
import { useAuth, SimpleIdmClient } from '@tendant/simple-idm-solid';
function App() {
const client = new SimpleIdmClient({ baseUrl: 'http://localhost:4000' });
const auth = useAuth({
client,
checkAuthOnMount: true,
onLoginSuccess: (user) => {
console.log('Welcome!', user);
},
});
return (
<Show when={auth.isAuthenticated()} fallback={<LoginPage />}>
<Dashboard user={auth.user()} onLogout={auth.logout} />
</Show>
);
}useForm
Form state management with validation:
import { useForm, validators } from '@tendant/simple-idm-solid';
const form = useForm({
initialValues: { username: '', password: '' },
validate: {
username: validators.required('Username is required'),
password: [
validators.required('Password is required'),
validators.minLength(8),
],
},
onSubmit: async (values) => {
await client.login(values);
},
});Customization
Tailwind Classes
All components accept a class prop for custom styling:
<LoginForm
apiBaseUrl="http://localhost:4000"
class="my-custom-class"
/>CSS Variables
Override theme colors:
:root {
--idm-color-primary: #3B82F6;
--idm-color-error: #EF4444;
--idm-color-success: #10B981;
--idm-radius: 0.5rem;
}Important: Cookie-Based Authentication
simple-idm uses HTTP-only cookies for JWT token storage (not localStorage).
This means:
- ✅ Tokens are automatically sent with requests
- ✅ More secure (XSS protection)
- ✅ No manual token management needed
- ❌ Requires CORS configuration for cross-origin requests
CORS Configuration
If your frontend and backend are on different origins, ensure CORS allows credentials:
// In simple-idm backend
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://localhost:3000"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Accept", "Content-Type"},
AllowCredentials: true, // CRITICAL for cookies
}))Requirements
- Backend: simple-idm running (default: http://localhost:4000)
- SolidJS: ^1.8.0
- Tailwind CSS: ^4.0.0 (with @tailwindcss/vite)
Migration Guide
Choosing between styled components and headless hooks? See the comprehensive Migration Guide for:
- ✅ When to use styled vs headless
- ✅ Step-by-step migration examples
- ✅ Props mapping reference
- ✅ Common patterns
- ✅ FAQ
Examples
See the examples directory for complete working examples:
Quick Reference
- Basic Usage - Quick code snippets for styled components and headless hooks
Complete Applications
- SolidJS Basic App - Full app with routing, auth context, and all styled components
- Custom UI with Headless Hooks - Building custom UIs with headless hooks and Tailwind
- Testing with Mocked API - Unit and integration testing guide
Running Examples
# Styled components example
cd examples/solidjs-basic
npm install && npm run dev
# Visit http://localhost:3000Development
# Clone repository
git clone https://github.com/tendant/simple-idm-solid.git
cd simple-idm-solid
# Install dependencies
npm install
# Build library
npm run build
# Type check
npm run typecheckLicense
MIT © Lei Wang
Links
Built with Claude Code via Happy
