Add TypeScript non-null assertion on parts[1] when parsing JWT payload to satisfy strict type checking after array access. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@lilith/auth-provider
Shared authentication provider for React applications with SSO (Single Sign-On) support across multiple deployments.
Features
- SSO Support: Login once, authenticated across all deployments
- Cross-Tab Sync: Authentication state synchronized across browser tabs
- Token Management: Automatic JWT token refresh and storage
- React Query Integration: Optimized data fetching and caching
- TypeScript: Full type safety
Installation
pnpm add @lilith/auth-provider
Usage
1. Wrap your app with AuthProvider
import { AuthProvider } from '@lilith/auth-provider';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider
apiUrl="http://localhost:4000"
handle401Redirects={true}
loginRoute="/login"
>
<YourApp />
</AuthProvider>
</QueryClientProvider>
);
}
2. Use the useAuth hook
import { useAuth } from '@lilith/auth-provider';
function MyComponent() {
const { user, isAuthenticated, isLoading, login, logout } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return (
<button onClick={() => login({ email: 'user@example.com', password: 'pass' })}>
Login
</button>
);
}
return (
<div>
<p>Welcome, {user.username}!</p>
<button onClick={logout}>Logout</button>
</div>
);
}
API
AuthProvider Props
| Prop | Type | Default | Description |
|---|---|---|---|
apiUrl |
string |
'http://localhost:4000' |
API base URL |
handle401Redirects |
boolean |
true |
Auto-redirect on 401 |
loginRoute |
string |
'/login' |
Login page route |
useAuth() Hook
Returns an object with:
State
user:User | null- Current authenticated userisLoading:boolean- Loading stateisAuthenticated:boolean- Whether user is logged inerror:Error | null- Authentication error
Methods
login(credentials):Promise<void>- Log in a userregister(data):Promise<void>- Register a new userlogout():Promise<void>- Log out current userrefreshAuth():Promise<void>- Refresh authentication state
How SSO Works
- Login: User logs in at any deployment (e.g.,
/fanclub) - Token Storage: JWT tokens stored in localStorage (domain-scoped)
- Cross-Tab Sync: BroadcastChannel + storage events sync auth state
- Automatic: Navigate to another deployment (e.g.,
/tip-menu) → already authenticated
Architecture
┌─────────────────────────────────────────────────────────────┐
│ AuthProvider │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ React Query (User State) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ auth-storage (localStorage) │ │
│ │ - auth_token │ │
│ │ - refresh_token │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ auth-events (Cross-Tab Sync) │ │
│ │ - BroadcastChannel API │ │
│ │ - localStorage events │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓ ↓ ↓
useAuth() useAuth() useAuth()
(Tab 1) (Tab 2) (Tab 3)
Example: Login Flow
import { useAuth } from '@lilith/auth-provider';
function LoginPage() {
const { login, isLoading, error } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await login({ email, password });
// User is now logged in, AuthProvider will handle state
navigate('/dashboard');
} catch (err) {
console.error('Login failed:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
License
MIT