@alphinex/auth
v1.0.1
Published
Auth state/session primitives: AuthProvider, useAuth, route guards.
Readme
@alphinex/auth
Backend-agnostic auth state and session primitives: an AuthProvider/useAuth context driven by
a pluggable AuthAdapter, a RequireAuth route guard, and swappable session storage strategies.
This package doesn't know about Sanctum, CSRF, or bearer tokens — that lives in
@alphinex/api-laravel (or any backend adapter you write); auth just needs an AuthAdapter.
AuthProvider / useAuth
AuthProvider takes an adapter: AuthAdapter<TUser, TCredentials> — an object with
fetchSession(), login(credentials), and logout() — and calls fetchSession() once on mount
to resolve the initial status ("idle" → "loading" → "authenticated"/"unauthenticated").
useAuth() reads status, user, error, and exposes login, logout, and refresh:
import { AuthProvider, useAuth, type AuthAdapter } from "@alphinex/auth";
import { ApiError } from "@alphinex/api";
interface Credentials {
email: string;
password: string;
}
const authAdapter: AuthAdapter<AppUser, Credentials> = {
fetchSession: async () => {
try {
return await apiClient.get<AppUser>("/user");
} catch (error) {
if (error instanceof ApiError && error.status === 401) return null;
throw error;
}
},
login: (credentials) => apiClient.post<AppUser>("/login", credentials),
logout: () => apiClient.post("/logout"),
};
function Root() {
return (
<AuthProvider adapter={authAdapter}>
<App />
</AuthProvider>
);
}
function LoginForm() {
const { login, error } = useAuth<AppUser, Credentials>();
const fieldErrors = error instanceof ApiError ? error.fieldErrors : undefined;
async function handleSubmit(email: string, password: string) {
await login({ email, password });
}
// ...
}Pass storage to persist the last-known user across reloads (see session storage below), and
subscribeSessionExpired to wire a backend-emitted session-expiry signal — e.g.
@alphinex/api-laravel's events.on("session-expired", ...) — to an immediate local sign-out,
without this package depending on api-laravel directly:
import { createLaravelApiClient } from "@alphinex/api-laravel";
const { client, events } = createLaravelApiClient({ baseUrl: "https://api.example.com" });
<AuthProvider
adapter={authAdapter}
subscribeSessionExpired={(handler) => events.on("session-expired", () => handler())}
>
<App />
</AuthProvider>;useAuth() throws if called outside an AuthProvider.
RequireAuth
A route guard that renders children only once useAuth() resolves to "authenticated". Renders
loading while status is "idle"/"loading", fallback while "unauthenticated" (both default
to nothing), and calls onUnauthenticated as a side effect when the user becomes unauthenticated —
kept as a callback rather than a hard router dependency so you can navigate("/login") with
whichever router you use:
import { RequireAuth } from "@alphinex/auth";
import { useNavigate } from "react-router-dom";
function ProtectedRoute() {
const navigate = useNavigate();
return (
<RequireAuth
loading={<p>Checking session…</p>}
fallback={<LoginForm />}
onUnauthenticated={() => navigate("/login")}
>
<Dashboard />
</RequireAuth>
);
}Session storage (createInMemorySessionStorage, createLocalStorageSessionStorage)
AuthProvider's storage option implements the SessionStorage<TUser> contract
(getUser()/setUser()). It defaults to createInMemorySessionStorage() — nothing persists
across a reload. Use createLocalStorageSessionStorage(key?) to avoid an auth-status flash on
reload; it's purely an optimistic-UI cache, fetchSession() remains the source of truth:
import { AuthProvider, createLocalStorageSessionStorage } from "@alphinex/auth";
<AuthProvider adapter={authAdapter} storage={createLocalStorageSessionStorage("myapp.auth.user")}>
<App />
</AuthProvider>;AuthAdapter / AuthStatus
The contract the whole package is built around. AuthStatus is
"idle" | "loading" | "authenticated" | "unauthenticated". AuthAdapter<TUser, TCredentials>
requires fetchSession(): Promise<TUser | null>, login(credentials): Promise<TUser>, and
logout(): Promise<void> — implement it against whatever backend you use (Sanctum via
@alphinex/api-laravel, a different API, or a mock for tests).
See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.
