Installation
pnpm add @hookraft/useauthuseAuth
The core hook. Handle login, logout, JWT decoding, token refresh, and brute force protection in one place.
import { useAuth } from "@hookraft/useauth"const auth = useAuth({
onLogin: async (credentials) => await signIn(credentials),
onLogout: async () => await signOut(),
onRefresh: async () => await refreshToken(),
onTokenExpired: () => toast.error("Session expired"),
onError: (error) => toast.error("Login failed"),
maxAttempts: 5,
lockoutDuration: 30,
minAttemptInterval: 500,
storage: "localStorage",
decodeToken: true,
})Full Example
function LoginPage() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const auth = useAuth({
onLogin: async (credentials) => {
const res = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify(credentials),
})
const data = await res.json()
return { token: data.token, user: data.user }
},
onLogout: async () => {
await fetch("/api/auth/logout", { method: "POST" })
},
onRefresh: async () => {
const res = await fetch("/api/auth/refresh", { method: "POST" })
const data = await res.json()
return data.token
},
onTokenExpired: () => toast.error("Your session expired. Please log in again."),
onError: () => toast.error("Invalid credentials"),
maxAttempts: 5,
lockoutDuration: 30,
minAttemptInterval: 500,
storage: "localStorage",
storageKey: "my_app_token",
decodeToken: true,
})
if (auth.is("authenticated")) {
return (
<div>
<p>Welcome, {auth.user?.name}</p>
<p>Session expires: {auth.tokenExpiresAt?.toLocaleString()}</p>
<button onClick={auth.logout}>Logout</button>
</div>
)
}
return (
<div>
<input
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={() => auth.login({ email, password })}
disabled={auth.is("loading") || auth.is("locked")}
>
{auth.is("loading") ? "Logging in..." : "Login"}
</button>
{auth.is("error") && (
<p>{auth.attempts} failed attempt(s). {5 - auth.attempts} remaining before lockout.</p>
)}
{auth.is("locked") && auth.lockoutReason === "max_attempts" && (
<p>Too many failed attempts. Try again in {auth.remainingTime}s</p>
)}
{auth.is("locked") && auth.lockoutReason === "bot_detection" && (
<p>Suspicious activity detected. Please slow down.</p>
)}
</div>
)
}Options
| Prop | Type | Default |
|---|---|---|
onLogin | (credentials: C) => Promise<{ token: string; user?: U }> | - |
onLogout | () => Promise<void> | void | - |
onRefresh | () => Promise<string> | - |
onTokenExpired | () => void | - |
onError | (error: unknown) => void | - |
decodeToken | boolean | true |
storage | StorageType | localStorage |
storageKey | string | hookraft_auth_token |
maxAttempts | number | 5 |
lockoutDuration | number | 30 |
minAttemptInterval | number | 500 |
Returns
| Prop | Type | Default |
|---|---|---|
status | AuthStatus | - |
is(status) | (s: AuthStatus) => boolean | - |
user | U | undefined | - |
token | string | null | - |
login(credentials) | (credentials: C) => Promise<void> | - |
logout() | () => Promise<void> | - |
tokenPayload | TokenPayload | null | - |
tokenExpiresAt | Date | null | - |
attempts | number | - |
lockout | LockoutState | null | - |
remainingTime | number | - |
lockoutReason | LockoutReason | null | - |
Auth Component
The declarative JSX companion. Render different UI for each auth state without writing conditionals manually.
import { Auth } from "@hookraft/useauth"<Auth
when={auth.status}
onAuthenticated={() => router.push("/dashboard")}
onIdle={() => router.push("/login")}
fallback={<p>Please log in to continue</p>}
>
<Dashboard />
</Auth>With Slot Components
Use named slots to render specific UI for each state:
function App() {
const auth = useAuth({ ...options })
return (
<Auth when={auth.status} fallback={<LoginForm onLogin={auth.login} />}>
<Auth.Loading when={auth.status}>
<Spinner />
</Auth.Loading>
<Auth.Authenticated when={auth.status}>
<Dashboard user={auth.user} onLogout={auth.logout} />
</Auth.Authenticated>
<Auth.Locked when={auth.status}>
<p>
{auth.lockoutReason === "bot_detection"
? "Suspicious activity detected."
: "Too many attempts."}
{" "}Try again in {auth.remainingTime}s
</p>
</Auth.Locked>
<Auth.Error when={auth.status}>
<p>{auth.attempts} failed attempt(s). Try again.</p>
</Auth.Error>
</Auth>
)
}Props
| Prop | Type | Default |
|---|---|---|
when | AuthStatus | - |
onAuthenticated | () => void | - |
onLoading | () => void | - |
onLocked | () => void | - |
onError | () => void | - |
onIdle | () => void | - |
fallback | ReactNode | null |
children | ReactNode | - |
Slot Components
| Prop | Type | Default |
|---|---|---|
Auth.Authenticated | { when: AuthStatus, children: ReactNode } | - |
Auth.Loading | { when: AuthStatus, children: ReactNode } | - |
Auth.Locked | { when: AuthStatus, children: ReactNode } | - |
Auth.Error | { when: AuthStatus, children: ReactNode } | - |