apiClient

This commit is contained in:
2025-11-22 15:30:08 +01:00
parent 530218a4b8
commit d3c76bc303
11 changed files with 319 additions and 169 deletions
+37 -24
View File
@@ -1,3 +1,4 @@
/* global PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import LoginView from '../login/LoginView';
@@ -9,7 +10,15 @@ import {
serializeAuthenticationCredential,
serializeRegistrationCredential,
} from '../utils/webauthn';
import { api, useAppDispatch, useAppState } from './appState';
import { useAppDispatch, useAppState } from './appState';
import {
finishPasskeyLogin,
finishSignup,
performLogin,
selectTenant,
startPasskeyLogin,
startSignup,
} from '../lib/apiClient';
type StatusVariant = 'info' | 'success' | 'error';
@@ -28,6 +37,11 @@ interface TenantSelectionState {
tenants?: TenantOption[];
}
type AuthResponse = {
access_token?: string;
tenant?: TenantOption | null;
tenants?: TenantOption[];
};
const LoginRoute: React.FC = () => {
const appState = useAppState();
@@ -161,23 +175,19 @@ const LoginRoute: React.FC = () => {
try {
setSelectingTenantId(tenant.id);
const { data } = await api.post(
'/auth/select-tenant',
const data = await selectTenant(
{ tenant_id: tenant.id },
{
headers: {
Authorization: `Bearer ${tenantSelection.selectionToken}`,
},
},
);
tenantSelection.selectionToken,
) as AuthResponse;
if (!data?.access_token) {
const accessToken = data?.access_token;
if (!accessToken) {
throw new Error('Invalid tenant selection response.');
}
appDispatch({
type: 'LOGIN_SUCCESS',
token: data.access_token,
token: accessToken,
tenant: data.tenant || null,
});
setStatusMessage('Login successful.', 'success');
@@ -211,9 +221,9 @@ const LoginRoute: React.FC = () => {
setPasskeyLoading(true);
appDispatch({ type: 'LOGIN_REQUEST' });
try {
const { data: startData } = await api.post('/auth/passkeys/login/start', { username });
const challengeId = startData.challengeId;
const publicKeyOptions = startData.publicKey;
const startData = await startPasskeyLogin(username);
const challengeId = (startData as { challengeId?: string })?.challengeId;
const publicKeyOptions = (startData as { publicKey?: PublicKeyCredentialRequestOptions })?.publicKey;
if (!challengeId || !publicKeyOptions) {
throw new Error('Invalid passkey challenge response.');
@@ -239,13 +249,13 @@ const LoginRoute: React.FC = () => {
credential: serialized,
};
const { data: finishData } = await api.post('/auth/passkeys/login/finish', finishPayload);
const finishData = await finishPasskeyLogin(finishPayload) as AuthResponse;
if (finishData?.access_token && Array.isArray(finishData?.tenants)) {
if (finishData?.access_token && Array.isArray(finishData.tenants)) {
appDispatch({
type: 'TENANT_SELECTION_REQUIRED',
selectionToken: finishData.access_token,
tenants: finishData.tenants,
tenants: finishData.tenants || [],
});
setStatusMessage('Select a tenant to continue.', 'info');
return;
@@ -322,16 +332,16 @@ const LoginRoute: React.FC = () => {
payload.preferred_tenant_id = magicPreferredTenantId;
}
const { data } = await api.post('/auth/login', payload);
const data = await performLogin(payload) as AuthResponse;
if (cancelled) {
return;
}
if (data?.access_token && Array.isArray(data?.tenants)) {
if (data?.access_token && Array.isArray(data.tenants)) {
appDispatch({
type: 'TENANT_SELECTION_REQUIRED',
selectionToken: data.access_token,
tenants: data.tenants,
tenants: data.tenants || [],
});
setStatusMessage('Select a tenant to continue.', 'info');
return;
@@ -416,7 +426,10 @@ const LoginRoute: React.FC = () => {
setSignupLoading(true);
try {
const { data: startData } = await api.post('/auth/signup/start', { username });
const startData = await startSignup(username) as {
signup_token?: string;
challenge?: { challengeId?: string; publicKey?: PublicKeyCredentialCreationOptions };
};
const signupToken = startData.signup_token;
const challengePayload = startData.challenge;
const challengeId = challengePayload?.challengeId;
@@ -446,13 +459,13 @@ const LoginRoute: React.FC = () => {
credential: serialized,
};
const { data: finishData } = await api.post('/auth/signup/finish', finishPayload);
const finishData = await finishSignup(finishPayload) as AuthResponse;
if (finishData?.access_token && Array.isArray(finishData?.tenants)) {
if (finishData?.access_token && Array.isArray(finishData.tenants)) {
appDispatch({
type: 'TENANT_SELECTION_REQUIRED',
selectionToken: finishData.access_token,
tenants: finishData.tenants,
tenants: finishData.tenants || [],
});
setStatusMessage('Select a tenant to continue.', 'info');
return;
+2 -3
View File
@@ -4,7 +4,6 @@ import { useAppShell } from '../appShellContext';
import useApiTokens from '../settings/useApiTokens';
import useCapabilitySets from '../settings/useCapabilitySets';
import useCapabilities from '../settings/useCapabilities';
import { api } from './appState';
interface SettingsRouteProps {
open?: boolean;
@@ -38,7 +37,7 @@ const SettingsRoute: React.FC<SettingsRouteProps> = ({ open = true, onClose }) =
revoke: revokeToken,
regenerate: regenerateToken,
dismissSecret,
} = useApiTokens({ api, token, notifyApiError, setStatusMessage });
} = useApiTokens({ token, notifyApiError, setStatusMessage });
const {
capabilitySets,
@@ -51,7 +50,7 @@ const SettingsRoute: React.FC<SettingsRouteProps> = ({ open = true, onClose }) =
createCapabilitySet,
updateCapabilitySet,
deleteCapabilitySet,
} = useCapabilitySets({ api, token, notifyApiError, setStatusMessage });
} = useCapabilitySets({ token, notifyApiError, setStatusMessage });
const {
capabilities,
+3 -2
View File
@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useMemo, useReducer } from 'react';
import api from '../lib/api';
import { listTenants } from '../lib/apiClient';
type Tenant = Record<string, unknown> | null;
@@ -217,11 +218,11 @@ const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }
}
try {
const { data } = await api.get('/tenants');
if (!abort) {
const tenants = await listTenants();
dispatch({
type: 'SET_TENANTS',
tenants: Array.isArray(data?.tenants) ? data.tenants : [],
tenants,
});
}
} catch (error) {