feat: task 1.5 directus auth client + zustand auth store
Six files under src/auth/:
- client.ts: createDirectus<Schema>().with(authentication('cookie', ...))
.with(rest(...)). Lazy singleton via initDirectusClient(url) +
getDirectus() + useDirectus() (React-side helper). DirectusUser type;
empty Schema (grows in Phase 2+).
- store.ts: Zustand store with discriminated AuthState
(unknown / anonymous / authenticating / authenticated). Actions:
initialize, login, logout, setUser. humanizeAuthError maps Directus
error codes (INVALID_CREDENTIALS / INVALID_OTP / USER_SUSPENDED) to
user-facing strings.
- guard.ts: useRequireAuth (redirect to /login on anonymous) and
useRequireRole (bounce to / on role mismatch). Uses TanStack Router's
useNavigate; effective once 1.7 wires the router.
- bootstrap.tsx: <AuthBootstrap> component runs initDirectusClient +
initialize() once after the runtime config is loaded.
- index.ts: barrel re-exports.
main.tsx wraps App in <RuntimeConfigProvider><AuthBootstrap>...
App.tsx shows the auth status as a smoke indicator before 1.6/1.7.
Deviations:
1. Split getDirectus into initDirectusClient(url) + getDirectus()
because the runtime config is a React context (per 1.4), not a
Zustand store. <AuthBootstrap> bridges from React land to the non-
React getter.
2. Added <AuthBootstrap> as a discrete component rather than putting
the bootstrap effect in App.tsx — App.tsx gets replaced by the
router in 1.7, but bootstrap needs to keep running.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { authentication, createDirectus, rest } from '@directus/sdk';
|
||||
import { useRuntimeConfig } from '@/config/context';
|
||||
|
||||
/**
|
||||
* Minimum user fields we read after login. Mirrors what `/users/me` returns
|
||||
* with `?fields=id,email,role,first_name,last_name`.
|
||||
*/
|
||||
export type DirectusUser = {
|
||||
id: string;
|
||||
email: string | null;
|
||||
role: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Typed Directus collection schema for the SDK.
|
||||
*
|
||||
* Will grow as the SPA starts reading TRM-specific collections in Phase 2+.
|
||||
* Empty for now — Phase 1 only needs `/auth/*` and `/users/me`, which the
|
||||
* SDK exposes outside the schema.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type Schema = {};
|
||||
|
||||
type DirectusClient = ReturnType<typeof buildClient>;
|
||||
|
||||
function buildClient(directusUrl: string) {
|
||||
return createDirectus<Schema>(directusUrl)
|
||||
.with(authentication('cookie', { credentials: 'include', autoRefresh: true }))
|
||||
.with(rest({ credentials: 'include' }));
|
||||
}
|
||||
|
||||
let _client: DirectusClient | null = null;
|
||||
let _clientUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* Lazy singleton accessor for the Directus client.
|
||||
*
|
||||
* The runtime config must be loaded before this is called (the
|
||||
* `<RuntimeConfigProvider>` enforces that by gating its children).
|
||||
* If the config URL changes between calls (e.g. dev HMR), the client is
|
||||
* rebuilt to match — a minor convenience, not a production concern.
|
||||
*/
|
||||
export function getDirectus(): DirectusClient {
|
||||
// We can't call useRuntimeConfig outside React; the consumers of getDirectus
|
||||
// are non-React (Zustand actions). They pass the URL via a small adapter
|
||||
// below or read it from a non-React-bound source.
|
||||
if (!_client) {
|
||||
throw new Error(
|
||||
'getDirectus() called before initDirectusClient(). Wire <AuthBootstrap> inside <RuntimeConfigProvider>.',
|
||||
);
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the Directus client with the runtime config's `directusUrl`.
|
||||
* Idempotent: rebuilds only if the URL has changed (HMR-friendly).
|
||||
*/
|
||||
export function initDirectusClient(directusUrl: string): DirectusClient {
|
||||
if (_client && _clientUrl === directusUrl) return _client;
|
||||
_client = buildClient(directusUrl);
|
||||
_clientUrl = directusUrl;
|
||||
return _client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience hook for components that need a typed Directus client.
|
||||
* Inside React, prefer this over `getDirectus()` — it's reactive to runtime
|
||||
* config changes (in practice config is immutable, but the hook form is
|
||||
* cleaner).
|
||||
*/
|
||||
export function useDirectus(): DirectusClient {
|
||||
const cfg = useRuntimeConfig();
|
||||
return initDirectusClient(cfg.directusUrl);
|
||||
}
|
||||
Reference in New Issue
Block a user