179 lines
5.9 KiB
Markdown
179 lines
5.9 KiB
Markdown
|
|
# @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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pnpm add @lilith/auth-provider
|
||
|
|
```
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
### 1. Wrap your app with AuthProvider
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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 user
|
||
|
|
- `isLoading`: `boolean` - Loading state
|
||
|
|
- `isAuthenticated`: `boolean` - Whether user is logged in
|
||
|
|
- `error`: `Error | null` - Authentication error
|
||
|
|
|
||
|
|
#### Methods
|
||
|
|
|
||
|
|
- `login(credentials)`: `Promise<void>` - Log in a user
|
||
|
|
- `register(data)`: `Promise<void>` - Register a new user
|
||
|
|
- `logout()`: `Promise<void>` - Log out current user
|
||
|
|
- `refreshAuth()`: `Promise<void>` - Refresh authentication state
|
||
|
|
|
||
|
|
## How SSO Works
|
||
|
|
|
||
|
|
1. **Login**: User logs in at any deployment (e.g., `/fanclub`)
|
||
|
|
2. **Token Storage**: JWT tokens stored in localStorage (domain-scoped)
|
||
|
|
3. **Cross-Tab Sync**: BroadcastChannel + storage events sync auth state
|
||
|
|
4. **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
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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
|