allauth/ (44 files) is a django-allauth React UI — a separate concern from the Mizan protocol. Moved to legacy/ pending extraction into a standalone mizan-django-allauth package. Also moved to legacy/: - client/AuthContext.tsx — generic auth state from /me endpoint - client/RouterContext.tsx — framework-agnostic router adapter - client/routing.tsx — UserRoute/StaffRoute/AnonymousRoute guards - client/nextjs.tsx — Next.js router adapter for auth These are auth UI infrastructure, not Mizan protocol. The Mizan core only needs JWT for auth header selection (jwt/ stays — MizanProvider depends on useJWT() to decide between Bearer and session auth). Cleaned up re-exports in client/react.ts and vitest aliases. 33 React tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
310 lines
15 KiB
TypeScript
310 lines
15 KiB
TypeScript
import { OAuthProcess, apiURL } from './defines'
|
|
|
|
import {
|
|
type RegistrationResponseJSON,
|
|
type AuthenticationResponseJSON,
|
|
} from '@simplewebauthn/browser'
|
|
|
|
import type {
|
|
// Core types
|
|
AuthError,
|
|
User,
|
|
Flow,
|
|
Authenticated,
|
|
AuthenticationMeta,
|
|
// Request types
|
|
LoginRequest,
|
|
SignupRequest,
|
|
ProviderSignupRequest,
|
|
ReauthenticateRequest,
|
|
ChangePasswordRequest,
|
|
ResetPasswordRequest,
|
|
MFAAuthenticateRequest,
|
|
WebAuthnUpdateRequest,
|
|
// Response types
|
|
AllauthResponse,
|
|
AuthenticatedResponse,
|
|
AuthenticationRequiredResponse,
|
|
ReauthenticationRequiredResponse,
|
|
ConfigurationResponse,
|
|
EmailListResponse,
|
|
SessionListResponse,
|
|
AuthenticatorListResponse,
|
|
ProviderAccountListResponse,
|
|
TOTPStatusResponse,
|
|
RecoveryCodesResponse,
|
|
WebAuthnCreationOptionsResponse,
|
|
WebAuthnRequestOptionsResponse,
|
|
EmailVerificationInfoResponse,
|
|
ErrorResponse,
|
|
} from './types'
|
|
|
|
export type { AuthError } from './types'
|
|
|
|
// Registration = creating new credentials (signup, add)
|
|
// Authentication = verifying existing credentials (login, authenticate, reauthenticate)
|
|
type RegistrationCredential = RegistrationResponseJSON
|
|
type AuthenticationCredential = AuthenticationResponseJSON
|
|
|
|
/**
|
|
* Union of all possible auth responses
|
|
*/
|
|
export type AuthResponse =
|
|
| AuthenticatedResponse
|
|
| AuthenticationRequiredResponse
|
|
| ReauthenticationRequiredResponse
|
|
| ConfigurationResponse
|
|
| EmailListResponse
|
|
| SessionListResponse
|
|
| AuthenticatorListResponse
|
|
| ProviderAccountListResponse
|
|
| TOTPStatusResponse
|
|
| RecoveryCodesResponse
|
|
| WebAuthnCreationOptionsResponse
|
|
| WebAuthnRequestOptionsResponse
|
|
| EmailVerificationInfoResponse
|
|
| ErrorResponse
|
|
| AllauthResponse
|
|
|
|
export interface AuthDetails {
|
|
isAuthenticated: boolean
|
|
requiresReauthentication: boolean
|
|
user: User | null
|
|
pendingFlow: Flow | undefined
|
|
}
|
|
|
|
export const getAuthDetails = (auth: AllauthResponse | null | undefined): AuthDetails => {
|
|
const meta = auth?.meta as AuthenticationMeta | undefined
|
|
const isAuthenticated = !!auth && (auth?.status === 200 || (auth?.status === 401 && !!meta?.is_authenticated))
|
|
const requiresReauthentication = !!(isAuthenticated && auth?.status === 401)
|
|
const data = auth?.data as Authenticated | { flows?: Flow[]; user?: User } | undefined
|
|
const pendingFlow = (data as { flows?: Flow[] })?.flows?.find((flow: Flow) => flow.is_pending)
|
|
|
|
return {
|
|
isAuthenticated,
|
|
requiresReauthentication,
|
|
user: isAuthenticated ? (data as Authenticated)?.user ?? null : null,
|
|
pendingFlow
|
|
}
|
|
}
|
|
|
|
export type BrowserFormAction = (action: string, data: Record<string, string>) => void
|
|
|
|
type RequestFn = (method: string, path: string, data?: unknown, headers?: Record<string, string>) => Promise<AllauthResponse>
|
|
|
|
export const createAPI = (
|
|
request: RequestFn,
|
|
browserFormAction?: BrowserFormAction
|
|
) => {
|
|
return {
|
|
getConfig: async (): Promise<ConfigurationResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.CONFIG) as ConfigurationResponse | ErrorResponse,
|
|
|
|
session: {
|
|
getStatus: async (): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.SESSION) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
|
|
|
|
list: async (): Promise<SessionListResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.SESSIONS) as SessionListResponse | ErrorResponse,
|
|
|
|
logout: async (): Promise<AllauthResponse> =>
|
|
await request('DELETE', apiURL.SESSION),
|
|
|
|
remove: async (ids: number[]): Promise<AllauthResponse> =>
|
|
await request('DELETE', apiURL.SESSIONS, { sessions: ids }),
|
|
},
|
|
|
|
account: {
|
|
signup: async (data: SignupRequest): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.SIGNUP, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
|
|
|
|
login: async (data: LoginRequest): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.LOGIN, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
|
|
|
|
reauthenticate: async (data: ReauthenticateRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.REAUTHENTICATE, data) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
emails: {
|
|
list: async (): Promise<EmailListResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.EMAIL) as EmailListResponse | ErrorResponse,
|
|
|
|
add: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse,
|
|
|
|
remove: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
|
|
await request('DELETE', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse,
|
|
|
|
setPrimary: async (email: string): Promise<EmailListResponse | ErrorResponse> =>
|
|
await request('PATCH', apiURL.EMAIL, { email, primary: true }) as EmailListResponse | ErrorResponse,
|
|
|
|
verification: {
|
|
dispatch: async (email: string): Promise<AllauthResponse> =>
|
|
await request('PUT', apiURL.EMAIL, { email }),
|
|
|
|
checkKey: async (key: string): Promise<EmailVerificationInfoResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.VERIFY_EMAIL, undefined, { 'X-Email-Verification-Key': key }) as EmailVerificationInfoResponse | ErrorResponse,
|
|
|
|
confirmKey: async (key: string): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.VERIFY_EMAIL, { key }) as AuthenticatedResponse | ErrorResponse,
|
|
}
|
|
},
|
|
|
|
password: {
|
|
set: async (data: ResetPasswordRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.RESET_PASSWORD, data) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
change: async (data: ChangePasswordRequest): Promise<AllauthResponse> =>
|
|
await request('POST', apiURL.CHANGE_PASSWORD, data),
|
|
|
|
reset: {
|
|
dispatch: async (email: string): Promise<AllauthResponse> =>
|
|
await request('POST', apiURL.REQUEST_PASSWORD_RESET, { email }),
|
|
|
|
checkKey: async (key: string): Promise<AllauthResponse> =>
|
|
await request('GET', apiURL.RESET_PASSWORD, undefined, { 'X-Password-Reset-Key': key }),
|
|
}
|
|
}
|
|
},
|
|
|
|
loginCodes: {
|
|
request: async (email: string): Promise<AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.REQUEST_LOGIN_CODE, { email }) as AuthenticationRequiredResponse | ErrorResponse,
|
|
|
|
confirm: async (code: string): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.CONFIRM_LOGIN_CODE, { code }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
|
|
},
|
|
|
|
oauth: {
|
|
list: async (): Promise<ProviderAccountListResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.PROVIDERS) as ProviderAccountListResponse | ErrorResponse,
|
|
|
|
signup: async (data: ProviderSignupRequest): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.PROVIDER_SIGNUP, data) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
provider: (providerID: string) => {
|
|
const buildAuths = (processType: string) => {
|
|
return {
|
|
withToken: async (token: string): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request(
|
|
'POST',
|
|
apiURL.PROVIDER_TOKEN,
|
|
{
|
|
provider: providerID,
|
|
process: processType,
|
|
token: token,
|
|
}
|
|
) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
|
|
|
|
withRedirect: (endpoint: string): void => {
|
|
if (browserFormAction) {
|
|
if (!process.env.NEXT_PUBLIC_HOST_URL) {
|
|
throw new Error('NEXT_PUBLIC_HOST_URL environment variable is not set. OAuth redirects require this to be set at build time.')
|
|
}
|
|
browserFormAction(
|
|
apiURL.REDIRECT_TO_PROVIDER,
|
|
{
|
|
provider: providerID,
|
|
process: processType,
|
|
callback_url: new URL(`${process.env.NEXT_PUBLIC_HOST_URL}/${endpoint}`).toString(),
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
removeFrom: async (accountUID: string): Promise<ProviderAccountListResponse | ErrorResponse> =>
|
|
await request('DELETE', apiURL.PROVIDERS, { provider: providerID, account: accountUID }) as ProviderAccountListResponse | ErrorResponse,
|
|
|
|
login: buildAuths(OAuthProcess.LOGIN),
|
|
connect: buildAuths(OAuthProcess.CONNECT),
|
|
}
|
|
}
|
|
},
|
|
|
|
mfa: {
|
|
list: async (): Promise<AuthenticatorListResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.AUTHENTICATORS) as AuthenticatorListResponse | ErrorResponse,
|
|
|
|
authenticate: async (code: string): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.MFA_AUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
reauthenticate: async (code: string): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.MFA_REAUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
trust: async (trust: boolean): Promise<AllauthResponse> =>
|
|
await request('POST', apiURL.MFA_TRUST, { trust }),
|
|
|
|
totp: {
|
|
getStatus: async (): Promise<TOTPStatusResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.TOTP_AUTHENTICATOR) as TOTPStatusResponse | ErrorResponse,
|
|
|
|
activate: async (code: string): Promise<TOTPStatusResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.TOTP_AUTHENTICATOR, { code }) as TOTPStatusResponse | ErrorResponse,
|
|
|
|
deactivate: async (): Promise<AllauthResponse> =>
|
|
await request('DELETE', apiURL.TOTP_AUTHENTICATOR),
|
|
},
|
|
|
|
recoveryCodes: {
|
|
list: async (): Promise<RecoveryCodesResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse,
|
|
|
|
regenerate: async (): Promise<RecoveryCodesResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse,
|
|
}
|
|
},
|
|
|
|
webauthn: {
|
|
signup: async (name: string, credential: RegistrationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('PUT', apiURL.SIGNUP_WEBAUTHN, { name, credential }) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
add: async (name: string, credential: RegistrationCredential): Promise<AllauthResponse> =>
|
|
await request('POST', apiURL.WEBAUTHN_AUTHENTICATOR, { name, credential }),
|
|
|
|
login: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.LOGIN_WEBAUTHN, { credential }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse,
|
|
|
|
authenticate: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.AUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
reauthenticate: async (credential: AuthenticationCredential): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('POST', apiURL.REAUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse,
|
|
|
|
update: async (id: number, data: Omit<WebAuthnUpdateRequest, 'id'>): Promise<AllauthResponse> =>
|
|
await request('PUT', apiURL.WEBAUTHN_AUTHENTICATOR, { id, ...data }),
|
|
|
|
delete: async (ids: number[]): Promise<AllauthResponse> =>
|
|
await request('DELETE', apiURL.WEBAUTHN_AUTHENTICATOR, { authenticators: ids }),
|
|
|
|
passkey: {
|
|
signup: async (email: string): Promise<AllauthResponse> =>
|
|
await request('POST', apiURL.SIGNUP_WEBAUTHN, { email }),
|
|
|
|
confirm: async (): Promise<AuthenticatedResponse | ErrorResponse> =>
|
|
await request('PUT', apiURL.SIGNUP_WEBAUTHN) as AuthenticatedResponse | ErrorResponse,
|
|
},
|
|
|
|
requestOptions: {
|
|
creation: async (passwordless: boolean): Promise<WebAuthnCreationOptionsResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.WEBAUTHN_AUTHENTICATOR + (passwordless ? '?passwordless' : '')) as WebAuthnCreationOptionsResponse | ErrorResponse,
|
|
|
|
creationAtSignup: async (): Promise<WebAuthnCreationOptionsResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.SIGNUP_WEBAUTHN) as WebAuthnCreationOptionsResponse | ErrorResponse,
|
|
|
|
login: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.LOGIN_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
|
|
|
|
authentication: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.AUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
|
|
|
|
reauthentication: async (): Promise<WebAuthnRequestOptionsResponse | ErrorResponse> =>
|
|
await request('GET', apiURL.REAUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
export type AllauthAPI = ReturnType<typeof createAPI>
|