@msflib/react-auth¶
Standalone authentication module for React applications. Built on TanStack Query v5. Supports real-time multi-tenant switching via
@msflib/core.
Table of Contents¶
- Installation
- Peer Dependencies
- Quick Start
- Configuration
- Application Config
- Custom Endpoints
- API Reference
- AuthProvider
- useAuth
- Multi-Tenancy Support
- TypeScript Types
- Endpoint Defaults
Installation¶
pnpm add @msflib/react-auth @msflib/core @msflib/react-shared
Peer Dependencies¶
| Package | Version |
|---|---|
react |
>= 18 |
react-dom |
>= 18 |
@tanstack/react-query |
>= 5 |
Quick Start¶
1. Configure the application once (e.g. in your app's entry point):
// app/config.ts
import { configureApplication } from '@msflib/core';
configureApplication({
baseURL: process.env.NEXT_PUBLIC_API_URL!,
accessTokenKey: 'access_token',
endpoints: {
auth: {
login: '/auth/login',
me: '/auth/me',
logout: '/auth/logout',
},
},
});
2. Wrap your app with QueryClientProvider and AuthProvider:
// app/providers.tsx
'use client'; // Next.js App Router
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@msflib/react-auth';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
);
}
3. Use the useAuth hook in any child component:
import { useAuth } from '@msflib/react-auth';
export function LoginForm() {
const { login, loading, status } = useAuth();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
await login(
{
email: form.get('email') as string,
password: form.get('password') as string,
},
{ onSuccess: () => console.log('Logged in!') },
);
};
if (status === 'authenticated') return <p>Already logged in.</p>;
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit" disabled={loading.login}>
{loading.login ? 'Logging in…' : 'Login'}
</button>
</form>
);
}
Configuration¶
Application Config¶
Call configureApplication once before rendering your app. It is idempotent — subsequent calls after the first are silently ignored.
import { configureApplication } from '@msflib/core';
configureApplication({
baseURL: 'https://api.example.com',
accessTokenKey: 'access_token', // localStorage key for the JWT
workspace: 'acme', // optional: initial active workspace
endpoints: {
auth: {
/* override defaults here */
},
},
});
Custom Endpoints¶
All endpoint keys are optional. Omitted keys fall back to the defaults listed in Endpoint Defaults.
configureApplication({
baseURL: 'https://api.example.com',
accessTokenKey: 'my_token',
endpoints: {
auth: {
login: '/v2/auth/login',
register: '/v2/auth/register',
me: '/v2/auth/me',
logout: '/v2/auth/logout',
recoverPassword: '/v2/auth/recover',
verifyToken: '/v2/auth/verify',
resetPassword: '/v2/auth/reset',
resendCode: '/v2/auth/resend',
verifyAvailability: '/v2/auth/verify-availability',
authenticate: '/v2/auth/authenticate',
verifyOtp: '/v2/auth/verify-otp',
ssoLogin: '/v2/auth/sso-login',
ssoRedirect: '/v2/auth/sso-redirect',
ssoRetrieve: '/v2/auth/sso-retrieve',
},
},
});
API Reference¶
AuthProvider¶
A React context provider that manages authentication state. Must be rendered inside a QueryClientProvider.
import { AuthProvider } from '@msflib/react-auth';
<QueryClientProvider client={queryClient}>
<AuthProvider>{/* your app */}</AuthProvider>
</QueryClientProvider>;
Props
| Prop | Type | Required | Description |
|---|---|---|---|
children |
React.ReactNode |
Yes | Child component tree. |
options |
object |
No | Configuration options for the provider. See below for details. |
options
| Key | Type | Default | Description |
|---|---|---|---|
isWorkspaceScoped |
boolean |
true |
If true, the session is isolated per workspace. |
requireAuth |
boolean |
false |
If true, the /me query only runs when isAuthenticated is true. |
isAuthenticated |
boolean |
false |
The authentication status of the user, used when requireAuth is true. |
Behaviour
- On mount,
AuthProviderautomatically callsGET /meto restore the user session. - The result is cached for 60 seconds via TanStack Query (
staleTime: 60_000). - When
login()succeeds, the access token is stored via@msflib/core'sstorageand the query cache is populated immediately — no second round-trip to/merequired. - When
logout()succeeds, the token is removed from storage, the query cache is cleared, andstatustransitions to'unauthenticated'.
useAuth¶
Returns the authentication context. Must be called inside a component tree wrapped by AuthProvider.
import { useAuth } from '@msflib/react-auth';
const {
status,
me,
login,
register,
recoverPassword,
verifyToken,
resetPassword,
resendCode,
logout,
updateMe,
verifyAvailability,
authenticate,
verifyOtp,
ssoLogin,
ssoRedirect,
ssoRetrieve,
loading,
} = useAuth();
status¶
| Value | Meaning |
|---|---|
'loading' |
The initial /me fetch is in progress. |
'authenticated' |
A user session is active. |
'unauthenticated' |
No session — user is logged out or /me returned an error. |
me¶
Me | null — The current user object. Is null when unauthenticated.
login(data, options?)¶
Authenticates the user. Sends x-www-form-urlencoded — maps email → username automatically.
await login({ email: 'user@example.com', password: 'secret' });
Returns Promise<AuthUser>.
register(data, options?)¶
Creates a new user account.
await register({
username: 'john',
email: 'john@example.com',
password: 'secret',
phone: '555-1234',
});
Returns Promise<RegisterResponse>.
recoverPassword(data, options?)¶
Sends a password recovery email.
await recoverPassword({ email: 'user@example.com' });
Returns Promise<{ msg: string }>.
verifyToken(data, options?)¶
Verifies an OTP or recovery token.
await verifyToken({ email: 'user@example.com', token: '123456' });
Returns Promise<{ msg: string }>.
resetPassword(data, options?)¶
Resets the user's password using a valid token.
await resetPassword({
token: 'abc123',
email: 'user@example.com',
new_password: 'newSecret',
});
Returns Promise<{ msg: string }>.
resendCode(data, options?)¶
Resends the verification code.
await resendCode({ email: 'user@example.com' });
Returns Promise<{ sent: boolean }>.
logout(options?)¶
Calls POST /logout, removes the stored access token, and clears the cached user.
await logout();
Returns Promise<void>.
updateMe(data, options?)¶
Updates the current user's account fields.
await updateMe({ username: 'new_name' });
Returns Promise<User>.
verifyAvailability(data, options?)¶
Checks whether an email, username, or phone is already taken — useful for inline signup-form validation before submitting.
await verifyAvailability({ field: 'email', value: 'user@example.com' });
Returns Promise<{ msg: string }>.
authenticate(data, options?)¶
Step 1 of the OTP login flow: verifies the email/password and sends a one-time code to the user's email. Does not establish a session by itself — follow up with verifyOtp.
await authenticate({ email: 'user@example.com', password: 'secret' });
Returns Promise<{ msg: string }>.
verifyOtp(data, options?)¶
Step 2 of the OTP login flow: verifies the code sent by authenticate and completes the login. On success, stores the access token and refreshes me — same as login.
await verifyOtp({ code: '123456', email: 'user@example.com' });
Returns Promise<AuthUser>.
ssoLogin(data, options?)¶
Exchanges an SSO token (typically read from a redirect query param) for a session in one call. On success, stores the access token and refreshes me.
await ssoLogin({ token: ssoTokenFromQueryParam });
Returns Promise<AuthUser>.
ssoRedirect(data, options?)¶
Hands an SSO token off to the backend (application/x-www-form-urlencoded) as part of the SSO handshake, optionally carrying a path_location to return to afterwards. Does not itself change auth state — pair with ssoRetrieve to pull the resulting session.
await ssoRedirect({ token: ssoToken, path_location: '/dashboard' });
Returns Promise<unknown> (the backend's response shape isn't fixed).
ssoRetrieve(options?)¶
Retrieves the session established by a prior ssoRedirect (no arguments — the backend resolves it from the request itself, e.g. a cookie). On success, stores the access token and refreshes me.
const session = await ssoRetrieve();
router.push(session.path_location || '/');
Returns Promise<AuthUser & { path_location?: string }>.
loading¶
An object of booleans indicating in-flight mutations:
loading.login;
loading.register;
loading.recoverPassword;
loading.verifyToken;
loading.resendCode;
loading.resetPassword;
loading.logout;
loading.updateMe;
loading.verifyAvailability;
loading.authenticate;
loading.verifyOtp;
loading.ssoLogin;
loading.ssoRedirect;
loading.ssoRetrieve;
Multi-Tenancy Support¶
@msflib/react-auth integrates with the workspace runtime in @msflib/core, but unlike most other msflib modules, AuthProvider does not subscribe to workspace changes itself.
How it works¶
- Endpoint paths — The API layer is created via
createAuthApi(options?.isWorkspaceScoped), which usesconfiguredApiClient()from@msflib/core. Scoped requests are prefixed/headered for the active workspace at request time, same as every other module. - Query cache — The
mequery uses a fixed key (authQueryKeys.me()), not one derived from the active workspace.AuthProviderdoes not calluseActiveWorkspace(), so changing the active workspace viasetActiveWorkspace()does not by itself trigger a re-fetch of/me.
If your app needs the session to refresh when the user switches workspace, invalidate it explicitly:
import { useQueryClient } from '@tanstack/react-query';
import { setActiveWorkspace } from '@msflib/core';
import { authQueryKeys } from '@msflib/react-auth';
const queryClient = useQueryClient();
setActiveWorkspace('new-workspace');
queryClient.invalidateQueries({ queryKey: authQueryKeys.me() });
Setting the initial workspace¶
configureApplication({
baseURL: 'https://api.example.com',
accessTokenKey: 'access_token',
workspace: 'acme-corp', // seeds the active workspace on startup
});
Clearing the workspace¶
setActiveWorkspace(null); // or setActiveWorkspace('')
TypeScript Types¶
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
export type Me = {
id: number;
created_at: string;
updated_at: string;
username: string;
email: string;
phone?: string;
status: string;
role: string;
data?: Record<string, unknown>;
[k: string]: unknown;
};
export type UpdateMePayload = Partial<Me> & {
current_password?: string;
new_password?: string;
};
export type Profile = {
firstname: string;
lastname: string;
date_of_birth: string;
gender: string;
marital_status: string;
[k: string]: unknown;
};
export type AuthUser = {
access_token: string;
token_type: string;
expires: string;
account?: Me;
};
export type LoginPayload = {
email: string;
password: string;
};
export type RegisterPayload = {
username?: string;
email?: string;
password: string;
phone?: string;
profile?: Omit<Profile, 'id' | 'email'>;
[key: string]: unknown;
};
export interface RegisterResponse extends Me {
profile: Profile;
}
export interface RecoverPasswordPayload {
email: string;
}
export interface VerifyTokenPayload {
email: string;
token: string;
}
export interface ResendCodePayload {
email: string | null;
}
export interface ResetPasswordPayload {
token: string;
email: string;
new_password: string;
}
export type VerifyAvailabilityField = 'email' | 'username' | 'phone';
export interface VerifyAvailabilityPayload {
field: VerifyAvailabilityField;
value: string;
}
export interface AuthenticatePayload {
email: string;
password: string;
}
export interface VerifyOtpPayload {
code: string;
email: string;
}
export interface SsoLoginParams {
token: string;
}
export interface SsoRedirectPayload {
token: string;
path_location?: string;
}
export type SsoRetrieveResponse = AuthUser & {
path_location?: string;
};
Endpoint Defaults¶
| Key | Default | HTTP Method |
|---|---|---|
login |
/login |
POST (form) |
register |
/open |
POST |
recoverPassword |
/password-recovery |
POST |
verifyToken |
/verify-token |
POST |
resendCode |
/resend-code |
POST |
resetPassword |
/reset-password |
POST |
me |
/me |
GET |
logout |
/logout |
POST |
verifyAvailability |
/verify-availability |
POST |
authenticate |
/authenticate |
POST |
verifyOtp |
/verify-otp |
POST |
ssoLogin |
/sso-login |
GET |
ssoRedirect |
/sso-redirect |
POST (form) |
ssoRetrieve |
/sso-retrieve |
GET |
Override any key via configureApplication({ endpoints: { auth: { ... } } }).
Notes:
- verifyAvailability, authenticate, verifyOtp are the pre-login OTP flow (accounts/otp_accounts on the backend) and don't require an access token.
- ssoLogin, ssoRedirect, ssoRetrieve cover the SSO handshake (sso-auth on the backend) and don't require an access token either.
- ssoRedirect sends application/x-www-form-urlencoded (not JSON), matching the backend contract.