@djangocfg/api
Type-safe API client and auth system for DjangoCFG applications.
Install
pnpm add @djangocfg/api
Entry Points
| Entry | Import | Description |
|---|---|---|
| Main | @djangocfg/api | API classes, types, fetchers, schemas |
| Auth | @djangocfg/api/auth | Auth providers, hooks, utilities |
| Auth Server | @djangocfg/api/auth/server | Next.js proxy middleware |
| Storage | @djangocfg/api/storage | Cookie/storage keys; single sign-in helpers (safeNext, deriveAuthHost, buildSsoLoginUrl) |
| Clients | @djangocfg/api/clients | Pre-configured API instances |
Generated API Classes
One class per OpenAPI tag. Import directly and call static methods:
import {
CfgAccounts, // OTP login
CfgAccountsProfile, // Profile CRUD
CfgAccountsOauth, // GitHub OAuth
CfgAccountsAuth, // Token refresh
CfgAccountsApiKey, // API key management
CfgTotp, // TOTP devices / disable
CfgTotpSetup, // TOTP setup / confirm
CfgTotpVerify, // TOTP verify / backup
CfgTotpBackupCodes, // Backup codes
} from '@djangocfg/api';
// OTP login
const result = await CfgAccounts.cfgAccountsOtpVerifyCreate({
body: { identifier, otp },
});
// Profile
const profile = await CfgAccountsProfile.cfgAccountsProfileRetrieve();
// Token refresh
const tokens = await CfgAccountsAuth.cfgAccountsTokenRefreshCreate({
body: { refresh },
});
Auth Module
import { AuthProvider, useAuth } from '@djangocfg/api/auth';
// Provider
<AuthProvider config={{ routes: { auth: '/auth', defaultCallback: '/private' } }}>
{children}
</AuthProvider>
// Hook
const { user, isAuthenticated, requestOTP, verifyOTP, logout } = useAuth();
Auth Hooks
import {
useAuth, // Main auth context
useAuthGuard, // Route protection
useAuthForm, // OTP form state machine
useOAuthLogin, // GitHub / Google OAuth
useOAuthProviders, // providers enabled on the server
useTwoFactor, // 2FA verification
useTwoFactorSetup, // 2FA setup flow
useTwoFactorStatus, // 2FA status / disable
useDeleteAccount, // Account deletion
} from '@djangocfg/api/auth';
Guard resolvers
The "spinner vs redirect vs render" decision is pure and separately testable.
useAuthGuard (and @djangocfg/layouts' PrivateLayout) just wire React state
into these:
import {
resolveGuardIsLoading, // show the preloader?
shouldRedirectToAuth, // no session → send to the auth form
isForbidden, // signed in, but lacks the required privilege
resolveGuardIsAuthenticated,
type GuardInput,
} from '@djangocfg/api/auth';
GuardInput takes two optional fields for the role gate: requireStaff (the
route needs staff/admin) and isAdminUser (from useAuth() — the
is_staff || is_superuser fold, never one flag).
isForbidden is deliberately not a redirect condition: the session is
valid, so the auth form cannot fix it, and redirecting would loop on the saved
URL. Render a distinct "not authorised" state instead. resolveGuardIsLoading
returns false once forbidden, so the preloader never spins on it.
Single sign-in host
<AuthProvider config={{ sso: true }}> (or BaseApp auth={{ sso: true }}) makes auth.<apex> the only sign-in page for
every subdomain: followers redirect there with ?next=<url> and are sent back; sign-out clears the session on all of
them. The session is the shared cookie pair on .<apex>. Off unless set, and off on localhost (one console line).
Pure helpers for middleware and clients are in @djangocfg/api/storage: safeNext (the only gate for next),
deriveAuthHost, resolveSso, buildSsoLoginUrl. useAuth().sso reports the state. Details:
@docs/AUTHENTICATION.md.
Storage keys
Every browser-storage key is cfg.<area>.<name> (framework) or
<owner>.<area>.<name> (app). Use the registry, never a literal or raw localStorage:
import { CFG_KEYS, cfgStorage, createStorage } from '@djangocfg/api/storage';
cfgStorage.read(CFG_KEYS.email.key);
cfgStorage.clearAll(); // sign-out: removes personal keys
const storage = createStorage('kino');
storage.writeJSON(storage.key('player', 'volume'), 0.4);
Old key names are not migrated. Full rules, the key list and cross-tab behavior:
@docs/STORAGE_KEYS.md.
Token storage
The token lives in the shared auth store (localStorage by default, switchable
to cookie):
import { auth } from '@djangocfg/api';
auth.getToken(); // current access token
auth.setStorageMode('cookie'); // SSR-readable cookie instead of localStorage
Logging — role-aware verbosity
All @djangocfg/* consola loggers read one global runtime level so the console
is clean for end users and verbose for admins/devs — like big SaaS apps.
import { applyRoleLogPolicy, setLogLevel } from '@djangocfg/api';
// Usually automatic: AuthProvider calls this on login/logout.
// admin (is_staff/is_superuser) or dev → debug; prod regular user → errors only.
applyRoleLogPolicy({ isAdmin: user?.is_staff || user?.is_superuser });
// Manual override (0 silent · 1 error · 2 warn · 3 info · 4 debug · 5 trace):
setLogLevel(0);
Errors still flow to @djangocfg/devtools regardless of console level, so prod
incidents stay diagnosable.
Env flags
@djangocfg/api is the lowest shared package, so env flags live here (single
source of truth):
import { isDev, isProd, isBrowser, isStaticBuild } from '@djangocfg/api';
Type Namespaces
import type { AccountsTypes, TotpTypes } from '@djangocfg/api';
type User = AccountsTypes.User;
type Device = TotpTypes.TotpDevice;
Server Middleware
// middleware.ts
import { proxyMiddleware } from '@djangocfg/api/auth/server';
export function middleware(request: NextRequest) {
return proxyMiddleware(request);
}
export const config = {
matcher: ['/media/:path*', '/api/:path*'],
};
Environment
NEXT_PUBLIC_API_URL=http://localhost:8000
Regenerate Client
# From django solution directory
make api