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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom'; import { Navigate, useLocation } from 'react-router-dom';
import LoginView from '../login/LoginView'; import LoginView from '../login/LoginView';
@@ -9,7 +10,15 @@ import {
serializeAuthenticationCredential, serializeAuthenticationCredential,
serializeRegistrationCredential, serializeRegistrationCredential,
} from '../utils/webauthn'; } 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'; type StatusVariant = 'info' | 'success' | 'error';
@@ -28,6 +37,11 @@ interface TenantSelectionState {
tenants?: TenantOption[]; tenants?: TenantOption[];
} }
type AuthResponse = {
access_token?: string;
tenant?: TenantOption | null;
tenants?: TenantOption[];
};
const LoginRoute: React.FC = () => { const LoginRoute: React.FC = () => {
const appState = useAppState(); const appState = useAppState();
@@ -161,23 +175,19 @@ const LoginRoute: React.FC = () => {
try { try {
setSelectingTenantId(tenant.id); setSelectingTenantId(tenant.id);
const { data } = await api.post( const data = await selectTenant(
'/auth/select-tenant',
{ tenant_id: tenant.id }, { tenant_id: tenant.id },
{ tenantSelection.selectionToken,
headers: { ) as AuthResponse;
Authorization: `Bearer ${tenantSelection.selectionToken}`,
},
},
);
if (!data?.access_token) { const accessToken = data?.access_token;
if (!accessToken) {
throw new Error('Invalid tenant selection response.'); throw new Error('Invalid tenant selection response.');
} }
appDispatch({ appDispatch({
type: 'LOGIN_SUCCESS', type: 'LOGIN_SUCCESS',
token: data.access_token, token: accessToken,
tenant: data.tenant || null, tenant: data.tenant || null,
}); });
setStatusMessage('Login successful.', 'success'); setStatusMessage('Login successful.', 'success');
@@ -211,9 +221,9 @@ const LoginRoute: React.FC = () => {
setPasskeyLoading(true); setPasskeyLoading(true);
appDispatch({ type: 'LOGIN_REQUEST' }); appDispatch({ type: 'LOGIN_REQUEST' });
try { try {
const { data: startData } = await api.post('/auth/passkeys/login/start', { username }); const startData = await startPasskeyLogin(username);
const challengeId = startData.challengeId; const challengeId = (startData as { challengeId?: string })?.challengeId;
const publicKeyOptions = startData.publicKey; const publicKeyOptions = (startData as { publicKey?: PublicKeyCredentialRequestOptions })?.publicKey;
if (!challengeId || !publicKeyOptions) { if (!challengeId || !publicKeyOptions) {
throw new Error('Invalid passkey challenge response.'); throw new Error('Invalid passkey challenge response.');
@@ -239,13 +249,13 @@ const LoginRoute: React.FC = () => {
credential: serialized, 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({ appDispatch({
type: 'TENANT_SELECTION_REQUIRED', type: 'TENANT_SELECTION_REQUIRED',
selectionToken: finishData.access_token, selectionToken: finishData.access_token,
tenants: finishData.tenants, tenants: finishData.tenants || [],
}); });
setStatusMessage('Select a tenant to continue.', 'info'); setStatusMessage('Select a tenant to continue.', 'info');
return; return;
@@ -322,16 +332,16 @@ const LoginRoute: React.FC = () => {
payload.preferred_tenant_id = magicPreferredTenantId; payload.preferred_tenant_id = magicPreferredTenantId;
} }
const { data } = await api.post('/auth/login', payload); const data = await performLogin(payload) as AuthResponse;
if (cancelled) { if (cancelled) {
return; return;
} }
if (data?.access_token && Array.isArray(data?.tenants)) { if (data?.access_token && Array.isArray(data.tenants)) {
appDispatch({ appDispatch({
type: 'TENANT_SELECTION_REQUIRED', type: 'TENANT_SELECTION_REQUIRED',
selectionToken: data.access_token, selectionToken: data.access_token,
tenants: data.tenants, tenants: data.tenants || [],
}); });
setStatusMessage('Select a tenant to continue.', 'info'); setStatusMessage('Select a tenant to continue.', 'info');
return; return;
@@ -416,7 +426,10 @@ const LoginRoute: React.FC = () => {
setSignupLoading(true); setSignupLoading(true);
try { 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 signupToken = startData.signup_token;
const challengePayload = startData.challenge; const challengePayload = startData.challenge;
const challengeId = challengePayload?.challengeId; const challengeId = challengePayload?.challengeId;
@@ -446,13 +459,13 @@ const LoginRoute: React.FC = () => {
credential: serialized, 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({ appDispatch({
type: 'TENANT_SELECTION_REQUIRED', type: 'TENANT_SELECTION_REQUIRED',
selectionToken: finishData.access_token, selectionToken: finishData.access_token,
tenants: finishData.tenants, tenants: finishData.tenants || [],
}); });
setStatusMessage('Select a tenant to continue.', 'info'); setStatusMessage('Select a tenant to continue.', 'info');
return; return;
+2 -3
View File
@@ -4,7 +4,6 @@ import { useAppShell } from '../appShellContext';
import useApiTokens from '../settings/useApiTokens'; import useApiTokens from '../settings/useApiTokens';
import useCapabilitySets from '../settings/useCapabilitySets'; import useCapabilitySets from '../settings/useCapabilitySets';
import useCapabilities from '../settings/useCapabilities'; import useCapabilities from '../settings/useCapabilities';
import { api } from './appState';
interface SettingsRouteProps { interface SettingsRouteProps {
open?: boolean; open?: boolean;
@@ -38,7 +37,7 @@ const SettingsRoute: React.FC<SettingsRouteProps> = ({ open = true, onClose }) =
revoke: revokeToken, revoke: revokeToken,
regenerate: regenerateToken, regenerate: regenerateToken,
dismissSecret, dismissSecret,
} = useApiTokens({ api, token, notifyApiError, setStatusMessage }); } = useApiTokens({ token, notifyApiError, setStatusMessage });
const { const {
capabilitySets, capabilitySets,
@@ -51,7 +50,7 @@ const SettingsRoute: React.FC<SettingsRouteProps> = ({ open = true, onClose }) =
createCapabilitySet, createCapabilitySet,
updateCapabilitySet, updateCapabilitySet,
deleteCapabilitySet, deleteCapabilitySet,
} = useCapabilitySets({ api, token, notifyApiError, setStatusMessage }); } = useCapabilitySets({ token, notifyApiError, setStatusMessage });
const { const {
capabilities, capabilities,
+3 -2
View File
@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useMemo, useReducer } from 'react'; import React, { useContext, useEffect, useMemo, useReducer } from 'react';
import api from '../lib/api'; import api from '../lib/api';
import { listTenants } from '../lib/apiClient';
type Tenant = Record<string, unknown> | null; type Tenant = Record<string, unknown> | null;
@@ -217,11 +218,11 @@ const AppStateProvider: React.FC<{ children?: React.ReactNode }> = ({ children }
} }
try { try {
const { data } = await api.get('/tenants');
if (!abort) { if (!abort) {
const tenants = await listTenants();
dispatch({ dispatch({
type: 'SET_TENANTS', type: 'SET_TENANTS',
tenants: Array.isArray(data?.tenants) ? data.tenants : [], tenants,
}); });
} }
} catch (error) { } catch (error) {
@@ -1,12 +1,8 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { assignCorrespondentsBulk } from '../../lib/apiClient';
export type Identifier = string | number; export type Identifier = string | number;
type ApiClient = {
post: <T = { data: unknown }>(url: string, payload: unknown) => Promise<{ data: T } | T>;
delete: (url: string) => Promise<unknown>;
};
type BulkAssignmentResponse = { type BulkAssignmentResponse = {
assigned?: number; assigned?: number;
removed?: number; removed?: number;
@@ -17,7 +13,6 @@ type CorrespondentAssignment = {
}; };
interface UseBulkDocumentActionsArgs { interface UseBulkDocumentActionsArgs {
api: ApiClient;
resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[]; resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[];
correspondentLookupByName: Map<string, { id?: Identifier }>; correspondentLookupByName: Map<string, { id?: Identifier }>;
handleCorrespondentCreate: (payload: { name: string }) => Promise<{ id?: Identifier } | null>; handleCorrespondentCreate: (payload: { name: string }) => Promise<{ id?: Identifier } | null>;
@@ -32,7 +27,6 @@ interface UseBulkDocumentActionsArgs {
} }
const useBulkDocumentActions = ({ const useBulkDocumentActions = ({
api,
resolveTargetDocumentIds, resolveTargetDocumentIds,
correspondentLookupByName, correspondentLookupByName,
handleCorrespondentCreate, handleCorrespondentCreate,
@@ -72,7 +66,7 @@ const useBulkDocumentActions = ({
return; return;
} }
const response = await api.post<BulkAssignmentResponse>('/documents/bulk/correspondents', { const response: BulkAssignmentResponse = await assignCorrespondentsBulk<BulkAssignmentResponse>({
document_ids: targets, document_ids: targets,
assignments: [ assignments: [
{ {
@@ -82,7 +76,7 @@ const useBulkDocumentActions = ({
action: 'add', action: 'add',
}); });
const { assigned = 0, removed = 0 } = 'data' in response ? response.data : response; const { assigned = 0, removed = 0 } = response;
if (updateDocumentCaches && target.id) { if (updateDocumentCaches && target.id) {
targets.forEach((docId) => { targets.forEach((docId) => {
@@ -118,7 +112,6 @@ const useBulkDocumentActions = ({
} }
}, },
[ [
api,
correspondentLookupByName, correspondentLookupByName,
handleCorrespondentCreate, handleCorrespondentCreate,
resolveTargetDocumentIds, resolveTargetDocumentIds,
@@ -145,13 +138,13 @@ const useBulkDocumentActions = ({
correspondent_id: entry.correspondent_id, correspondent_id: entry.correspondent_id,
})); }));
const response = await api.post<BulkAssignmentResponse>('/documents/bulk/correspondents', { const response: BulkAssignmentResponse = await assignCorrespondentsBulk<BulkAssignmentResponse>({
document_ids: targets, document_ids: targets,
assignments: normalizedAssignments, assignments: normalizedAssignments,
action: 'remove', action: 'remove',
}); });
const { assigned = 0, removed = 0 } = 'data' in response ? response.data : response; const { assigned = 0, removed = 0 } = response;
if (updateDocumentCaches) { if (updateDocumentCaches) {
targets.forEach((docId) => { targets.forEach((docId) => {
updateDocumentCaches(docId, (doc) => { updateDocumentCaches(docId, (doc) => {
@@ -182,7 +175,7 @@ const useBulkDocumentActions = ({
setStatusMessage('No correspondents changed.', 'info'); setStatusMessage('No correspondents changed.', 'info');
} }
}, },
[api, resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches], [resolveTargetDocumentIds, setStatusMessage, updateDocumentCaches],
); );
const handleDeleteSelection = useCallback(async () => { const handleDeleteSelection = useCallback(async () => {
@@ -2,6 +2,17 @@ import { useCallback } from 'react';
import { isPlainObject } from '../../utils/typeGuards'; import { isPlainObject } from '../../utils/typeGuards';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils';
import {
addDocumentTags,
createTag,
deleteDocumentTag,
deleteFolder,
moveDocumentsBulk,
moveDocumentToFolder,
queueDocumentReanalysis,
trashDocument,
updateDocument,
} from '../../lib/apiClient';
type DocumentId = string | number; type DocumentId = string | number;
type FolderId = DocumentId | 'root'; type FolderId = DocumentId | 'root';
@@ -35,12 +46,6 @@ type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type SetStatusMessage = (message: string, level?: StatusLevel) => void; type SetStatusMessage = (message: string, level?: StatusLevel) => void;
interface ApiClient {
post<T = unknown>(url: string, data?: unknown, config?: Record<string, unknown>): Promise<{ data: T }>;
patch<T = unknown>(url: string, data?: unknown, config?: Record<string, unknown>): Promise<{ data: T }>;
delete<T = unknown>(url: string, config?: Record<string, unknown>): Promise<{ data: T }>;
}
interface Tag { interface Tag {
id: DocumentId; id: DocumentId;
label: string; label: string;
@@ -105,7 +110,6 @@ interface FolderDeleteOptions {
} }
interface UseDocumentMutationsArgs { interface UseDocumentMutationsArgs {
api: ApiClient;
token?: string | null; token?: string | null;
documentLookup: Map<DocumentId, DocumentLike>; documentLookup: Map<DocumentId, DocumentLike>;
folderLabelMap: Map<FolderId, string>; folderLabelMap: Map<FolderId, string>;
@@ -181,7 +185,6 @@ const normalizeDocumentId = (value: unknown): DocumentId | null => {
}; };
const useDocumentMutations = ({ const useDocumentMutations = ({
api,
token, token,
documentLookup, documentLookup,
folderLabelMap, folderLabelMap,
@@ -286,12 +289,9 @@ const useDocumentMutations = ({
setLoading(true); setLoading(true);
try { try {
if (uniqueIds.length === 1) { if (uniqueIds.length === 1) {
await api.patch(`/documents/${uniqueIds[0]}/folder`, { folder_id: target }); await moveDocumentToFolder(uniqueIds[0], target);
} else { } else {
await api.post('/documents/bulk/move', { await moveDocumentsBulk(uniqueIds, target);
document_ids: uniqueIds,
folder_id: target,
});
} }
const count = uniqueIds.length; const count = uniqueIds.length;
@@ -382,7 +382,6 @@ const useDocumentMutations = ({
} }
}, },
[ [
api,
documentLookup, documentLookup,
folderLabelMap, folderLabelMap,
ensureFolderData, ensureFolderData,
@@ -413,9 +412,7 @@ const useDocumentMutations = ({
} }
setLoading(true); setLoading(true);
try { try {
await api.post(`/documents/${documentId}/assets`, null, { await queueDocumentReanalysis(documentId, { force: true });
params: { force: true },
});
setStatusMessage('Document re-analysis queued.', 'info'); setStatusMessage('Document re-analysis queued.', 'info');
await refreshCurrentFolder(); await refreshCurrentFolder();
} catch (error) { } catch (error) {
@@ -425,7 +422,7 @@ const useDocumentMutations = ({
setLoading(false); setLoading(false);
} }
}, },
[api, token, refreshCurrentFolder, notifyApiError, setStatusMessage, setLoading], [token, refreshCurrentFolder, notifyApiError, setStatusMessage, setLoading],
); );
const handleDocumentsDelete = useCallback( const handleDocumentsDelete = useCallback(
@@ -444,7 +441,7 @@ const useDocumentMutations = ({
} }
try { try {
await Promise.all(documentIds.map((documentId) => api.post(`/documents/${documentId}/trash`))); await Promise.all(documentIds.map((documentId) => trashDocument(documentId)));
removeDocumentsFromCaches(documentIds); removeDocumentsFromCaches(documentIds);
@@ -468,7 +465,6 @@ const useDocumentMutations = ({
} }
}, },
[ [
api,
token, token,
removeDocumentsFromCaches, removeDocumentsFromCaches,
previewDocumentId, previewDocumentId,
@@ -489,7 +485,7 @@ const useDocumentMutations = ({
setLoading(true); setLoading(true);
try { try {
const { data } = await api.patch(`/documents/${documentId}`, { title: trimmed }); const data = await updateDocument(documentId, { title: trimmed });
const updatedDocument = extractDocumentFromResponse?.(data); const updatedDocument = extractDocumentFromResponse?.(data);
if (updatedDocument && ingestDocuments) { if (updatedDocument && ingestDocuments) {
@@ -514,7 +510,6 @@ const useDocumentMutations = ({
} }
}, },
[ [
api,
extractDocumentFromResponse, extractDocumentFromResponse,
ingestDocuments, ingestDocuments,
notifyApiError, notifyApiError,
@@ -529,7 +524,7 @@ const useDocumentMutations = ({
setLoading(true); setLoading(true);
const payload = { issued_at: nextIssuedDate || null }; const payload = { issued_at: nextIssuedDate || null };
try { try {
const { data } = await api.patch(`/documents/${documentId}`, payload); const data = await updateDocument(documentId, payload);
const updatedDocument = extractDocumentFromResponse?.(data); const updatedDocument = extractDocumentFromResponse?.(data);
if (updatedDocument && ingestDocuments) { if (updatedDocument && ingestDocuments) {
@@ -555,7 +550,6 @@ const useDocumentMutations = ({
} }
}, },
[ [
api,
extractDocumentFromResponse, extractDocumentFromResponse,
ingestDocuments, ingestDocuments,
notifyApiError, notifyApiError,
@@ -584,7 +578,7 @@ const useDocumentMutations = ({
}; };
try { try {
await api.post(`/documents/${documentId}/tags`, { tag_ids: [cachedTag.id] }); await addDocumentTags(documentId, [cachedTag.id]);
updateDocumentCaches(documentId, (doc) => { updateDocumentCaches(documentId, (doc) => {
if (!doc) { if (!doc) {
return doc; return doc;
@@ -603,7 +597,7 @@ const useDocumentMutations = ({
return false; return false;
} }
}, },
[api, notifyApiError, setStatusMessage, updateDocumentCaches], [notifyApiError, setStatusMessage, updateDocumentCaches],
); );
const handleDocumentTagAdd = useCallback( const handleDocumentTagAdd = useCallback(
@@ -621,8 +615,8 @@ const useDocumentMutations = ({
} }
try { try {
if (!tag) { if (!tag) {
const payload = tagManager.buildPayload({ label: normalizedLabel }); const payload = tagManager.buildPayload({ label: normalizedLabel }) as { label: string; color?: string | null };
const { data } = await api.post('/tags', payload); const data = await createTag(payload);
tag = data as Tag; tag = data as Tag;
await refreshTags(); await refreshTags();
} }
@@ -637,7 +631,7 @@ const useDocumentMutations = ({
notifyApiError(error, 'Failed to assign tag.'); notifyApiError(error, 'Failed to assign tag.');
} }
}, },
[api, tags, refreshTags, attachTagToDocument, notifyApiError, tagManager], [tags, refreshTags, attachTagToDocument, notifyApiError, tagManager],
); );
const handleDocumentTagAttach = useCallback( const handleDocumentTagAttach = useCallback(
@@ -706,7 +700,7 @@ const useDocumentMutations = ({
} }
try { try {
await api.delete(`/documents/${documentId}/tags/${tagId}`); await deleteDocumentTag(documentId, tagId);
applyTagRemovalToCaches(documentId, tagId); applyTagRemovalToCaches(documentId, tagId);
if (refreshTagList) { if (refreshTagList) {
await refreshTags(); await refreshTags();
@@ -721,7 +715,7 @@ const useDocumentMutations = ({
return false; return false;
} }
}, },
[api, applyTagRemovalToCaches, notifyApiError, refreshTags, setStatusMessage], [applyTagRemovalToCaches, notifyApiError, refreshTags, setStatusMessage],
); );
const handleFolderDelete = useCallback( const handleFolderDelete = useCallback(
@@ -757,7 +751,7 @@ const useDocumentMutations = ({
return false; return false;
} }
await api.delete(`/folders/${folderId}`); await deleteFolder(folderId);
setFolderNodes((prev: Map<FolderId, FolderNode>) => { setFolderNodes((prev: Map<FolderId, FolderNode>) => {
const next = new Map<FolderId, FolderNode>(prev); const next = new Map<FolderId, FolderNode>(prev);
@@ -815,7 +809,6 @@ const useDocumentMutations = ({
} }
}, },
[ [
api,
token, token,
ensureFolderData, ensureFolderData,
selectedFolder, selectedFolder,
@@ -566,7 +566,6 @@ const useDocumentsWorkspace = ({
registerPasskey, registerPasskey,
revokePasskey, revokePasskey,
} = usePasskeys({ } = usePasskeys({
api,
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
token, token,
@@ -815,7 +814,6 @@ const useDocumentsWorkspace = ({
handleDocumentIssuedUpdate, handleDocumentIssuedUpdate,
handleTagRemove, handleTagRemove,
} = useDocumentMutations({ } = useDocumentMutations({
api,
token, token,
documentLookup, documentLookup,
folderLabelMap, folderLabelMap,
@@ -861,7 +859,6 @@ const useDocumentsWorkspace = ({
handleFolderDelete, handleFolderDelete,
folderClickHandlers, folderClickHandlers,
} = useFolderTreeActions({ } = useFolderTreeActions({
api,
token, token,
folderNodes, folderNodes,
setFolderNodes, setFolderNodes,
@@ -996,7 +993,6 @@ const useDocumentsWorkspace = ({
handleBulkCorrespondentRemove, handleBulkCorrespondentRemove,
handleDeleteSelection, handleDeleteSelection,
} = useBulkDocumentActions({ } = useBulkDocumentActions({
api,
resolveTargetDocumentIds, resolveTargetDocumentIds,
correspondentLookupByName, correspondentLookupByName,
handleCorrespondentCreate, handleCorrespondentCreate,
@@ -1,6 +1,12 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils';
import {
createFolder,
deleteFolder,
moveFolder as moveFolderRequest,
renameFolder as renameFolderRequest,
} from '../../lib/apiClient';
type FolderId = string | number; type FolderId = string | number;
type FolderKey = FolderId | 'root'; type FolderKey = FolderId | 'root';
@@ -22,12 +28,6 @@ interface FolderContentsState {
[key: string]: unknown; [key: string]: unknown;
} }
interface ApiClient {
patch: (url: string, data?: unknown) => Promise<any>;
post: (url: string, data?: unknown) => Promise<{ data: any }>;
delete: (url: string) => Promise<any>;
}
interface EnsureFolderOptions { interface EnsureFolderOptions {
force?: boolean; force?: boolean;
includeDocuments?: boolean; includeDocuments?: boolean;
@@ -53,7 +53,6 @@ interface FolderClickHandlers {
} }
interface UseFolderTreeActionsOptions { interface UseFolderTreeActionsOptions {
api: ApiClient;
token?: string | null; token?: string | null;
folderNodes: Map<FolderKey, FolderNode>; folderNodes: Map<FolderKey, FolderNode>;
setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void; setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void;
@@ -84,7 +83,6 @@ interface UseFolderTreeActionsOptions {
} }
const useFolderTreeActions = ({ const useFolderTreeActions = ({
api,
token, token,
folderNodes, folderNodes,
setFolderNodes, setFolderNodes,
@@ -129,7 +127,7 @@ const useFolderTreeActions = ({
const parent_id = targetKey === 'root' ? null : targetKey; const parent_id = targetKey === 'root' ? null : targetKey;
try { try {
await api.patch(`/folders/${folderId}`, { parent_id }); await moveFolderRequest(folderId, parent_id);
setFolderNodes((prev) => { setFolderNodes((prev) => {
const next = new Map(prev); const next = new Map(prev);
@@ -202,7 +200,6 @@ const useFolderTreeActions = ({
} }
}, },
[ [
api,
ensureFolderData, ensureFolderData,
folderNodes, folderNodes,
notifyApiError, notifyApiError,
@@ -295,7 +292,7 @@ const useFolderTreeActions = ({
setLoading(true); setLoading(true);
try { try {
await api.patch(`/folders/${folderId}`, { name: trimmed }); await renameFolderRequest(folderId, trimmed);
setFolderNodes((prev) => { setFolderNodes((prev) => {
const next = new Map(prev); const next = new Map(prev);
@@ -331,7 +328,6 @@ const useFolderTreeActions = ({
} }
}, },
[ [
api,
notifyApiError, notifyApiError,
setCurrentFolder, setCurrentFolder,
setFolderContents, setFolderContents,
@@ -359,33 +355,37 @@ const useFolderTreeActions = ({
setCreatingFolder(true); setCreatingFolder(true);
let succeeded = false; let succeeded = false;
try { try {
const { data } = await api.post('/folders', payload); const data = await createFolder(payload);
const folderData = (data as { folder?: { id?: FolderKey; name?: string; parent_id?: FolderKey | null; children?: FolderKey[] } }).folder;
if (!folderData?.id) {
throw new Error('Folder creation failed.');
}
setStatusMessage('Folder created.', 'success'); setStatusMessage('Folder created.', 'success');
setFolderNodes((prev) => { setFolderNodes((prev) => {
const next = new Map(prev); const next = new Map(prev);
const parentId = payload.parent_id || 'root'; const parentId = folderData.parent_id ?? payload.parent_id ?? 'root';
const parentNode = next.get(parentId); const parentNode = next.get(parentId);
if (parentNode) { if (parentNode) {
next.set(parentId, { next.set(parentId, {
...parentNode, ...parentNode,
children: parentNode.children.concat([data.folder.id]), children: parentNode.children.concat([folderData.id]),
loaded: true, loaded: true,
hasChildren: true, hasChildren: true,
}); });
} }
next.set(data.folder.id, { next.set(folderData.id, {
id: data.folder.id, id: folderData.id,
name: data.folder.name, name: folderData.name ?? payload.name,
parentId: parentId, parentId: parentId,
children: [], children: folderData.children || [],
expanded: false, expanded: false,
loaded: false, loaded: false,
hasChildren: false, hasChildren: Array.isArray(folderData.children) ? folderData.children.length > 0 : false,
}); });
return next; return next;
}); });
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 }); await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
await selectFolder(data.folder.id, { immediate: true }); await selectFolder(folderData.id, { immediate: true });
succeeded = true; succeeded = true;
return true; return true;
} catch (error) { } catch (error) {
@@ -400,7 +400,6 @@ const useFolderTreeActions = ({
} }
}, },
[ [
api,
ensureFolderData, ensureFolderData,
notifyApiError, notifyApiError,
selectFolder, selectFolder,
@@ -445,7 +444,7 @@ const useFolderTreeActions = ({
return false; return false;
} }
await api.delete(`/folders/${folderId}`); await deleteFolder(folderId);
setFolderNodes((prev) => { setFolderNodes((prev) => {
const next = new Map(prev); const next = new Map(prev);
@@ -503,7 +502,6 @@ const useFolderTreeActions = ({
} }
}, },
[ [
api,
token, token,
applySelectedFolder, applySelectedFolder,
ensureFolderData, ensureFolderData,
+173
View File
@@ -9,6 +9,8 @@ import type {
FolderTreeNode, FolderTreeNode,
Identifier, Identifier,
PasskeySummary, PasskeySummary,
TenantSnippet,
TagResponse,
} from './apiTypes'; } from './apiTypes';
const normalizeNumber = (value: unknown): number | undefined => { const normalizeNumber = (value: unknown): number | undefined => {
@@ -75,6 +77,176 @@ export const listPasskeys = async (): Promise<PasskeySummary[]> => {
return Array.isArray(data) ? data : []; return Array.isArray(data) ? data : [];
}; };
export const moveDocumentsBulk = async (documentIds: Identifier[], folderId: Identifier | null): Promise<void> => {
await api.post('/documents/bulk/move', {
document_ids: documentIds,
folder_id: folderId,
});
};
export const queueDocumentReanalysis = async (
documentId: Identifier,
options: { force?: boolean } = {},
): Promise<void> => {
const { force = false } = options;
await api.post(`/documents/${documentId}/assets`, null, { params: { force } });
};
export const trashDocument = async (documentId: Identifier): Promise<void> => {
await api.post(`/documents/${documentId}/trash`);
};
export const addDocumentTags = async (documentId: Identifier, tagIds: Identifier[]): Promise<void> => {
await api.post(`/documents/${documentId}/tags`, { tag_ids: tagIds });
};
export const createTag = async (payload: { label: string; color?: string | null }): Promise<TagResponse> => {
const { data } = await api.post<TagResponse>('/tags', payload);
return data;
};
export const createFolder = async (payload: { name: string; parent_id?: Identifier | null }): Promise<unknown> => {
const { data } = await api.post('/folders', payload);
return data;
};
export const assignCorrespondentsBulk = async <T = unknown>(
payload: Record<string, unknown>,
): Promise<T> => {
const { data } = await api.post<T>('/documents/bulk/correspondents', payload);
return data;
};
export const createApiToken = async (payload: {
capability_set_id: Identifier;
label?: string;
expires_at?: string;
}): Promise<{ token_info?: ApiTokenRecord; token?: string }> => {
const { data } = await api.post('/profile/api-tokens', payload);
return data as { token_info?: ApiTokenRecord; token?: string };
};
export const regenerateApiToken = async (
tokenId: Identifier,
): Promise<{ token_info?: ApiTokenRecord; token?: string }> => {
const { data } = await api.post(`/profile/api-tokens/${tokenId}/regenerate`);
return data as { token_info?: ApiTokenRecord; token?: string };
};
export const startPasskeyRegistration = async (): Promise<unknown> => {
const { data } = await api.post('/auth/passkeys/register/start', {});
return data;
};
export const finishPasskeyRegistration = async (payload: unknown): Promise<unknown> => {
const { data } = await api.post('/auth/passkeys/register/finish', payload);
return data;
};
export const startPasskeyLogin = async (username: string): Promise<unknown> => {
const { data } = await api.post('/auth/passkeys/login/start', { username });
return data;
};
export const finishPasskeyLogin = async (payload: unknown): Promise<unknown> => {
const { data } = await api.post('/auth/passkeys/login/finish', payload);
return data;
};
export const performLogin = async (payload: Record<string, unknown>): Promise<unknown> => {
const { data } = await api.post('/auth/login', payload);
return data;
};
export const selectTenant = async (
payload: { tenant_id: Identifier },
selectionToken: string,
): Promise<unknown> => {
const { data } = await api.post('/auth/select-tenant', payload, {
headers: {
Authorization: `Bearer ${selectionToken}`,
},
});
return data;
};
export const startSignup = async (username: string): Promise<unknown> => {
const { data } = await api.post('/auth/signup/start', { username });
return data;
};
export const finishSignup = async (payload: unknown): Promise<unknown> => {
const { data } = await api.post('/auth/signup/finish', payload);
return data;
};
export const updateDocument = async (
id: Identifier,
payload: Record<string, unknown>,
): Promise<DocumentResponse> => {
const { data } = await api.patch<DocumentResponse>(`/documents/${id}`, payload);
return data;
};
export const moveDocumentToFolder = async (id: Identifier, folderId: Identifier | null): Promise<void> => {
await api.patch(`/documents/${id}/folder`, { folder_id: folderId });
};
export const deleteDocumentTag = async (documentId: Identifier, tagId: Identifier): Promise<void> => {
await api.delete(`/documents/${documentId}/tags/${tagId}`);
};
export const deleteFolder = async (folderId: Identifier): Promise<void> => {
await api.delete(`/folders/${folderId}`);
};
export const moveFolder = async (folderId: Identifier, parentId: Identifier | null): Promise<void> => {
await api.patch(`/folders/${folderId}`, { parent_id: parentId });
};
export const renameFolder = async (folderId: Identifier, name: string): Promise<void> => {
await api.patch(`/folders/${folderId}`, { name });
};
export const createCapabilitySet = async (
payload: { slug?: string; label?: string; capabilities: string[] },
): Promise<CapabilitySetResponse & { label?: string }> => {
const { data } = await api.post<CapabilitySetResponse & { label?: string }>('/capability-sets', payload);
return data;
};
export const updateCapabilitySet = async (
id: Identifier,
payload: { slug?: string; label?: string; capabilities?: string[] },
): Promise<CapabilitySetResponse & { label?: string }> => {
const { data } = await api.patch<CapabilitySetResponse & { label?: string }>(`/capability-sets/${id}`, payload);
return data;
};
export const deleteCapabilitySet = async (id: Identifier): Promise<void> => {
await api.delete(`/capability-sets/${id}`);
};
export const deleteApiToken = async (tokenId: Identifier): Promise<void> => {
await api.delete(`/profile/api-tokens/${tokenId}`);
};
export const deletePasskey = async (
passkeyId: Identifier,
options: { reason?: string } = {},
): Promise<void> => {
const query = options.reason ? `?reason=${encodeURIComponent(options.reason)}` : '';
await api.delete(`/profile/passkeys/${passkeyId}${query}`);
};
export const listTenants = async (): Promise<TenantSnippet[]> => {
const { data } = await api.get<{ tenants?: TenantSnippet[] } | TenantSnippet[]>('/tenants');
if (Array.isArray(data)) {
return data;
}
return Array.isArray(data?.tenants) ? data.tenants : [];
};
export type { export type {
DownloadLink, DownloadLink,
DocumentResponse, DocumentResponse,
@@ -85,4 +257,5 @@ export type {
ApiTokenRecord, ApiTokenRecord,
PasskeySummary, PasskeySummary,
Identifier, Identifier,
TenantSnippet,
} from './apiTypes'; } from './apiTypes';
+17 -25
View File
@@ -1,14 +1,11 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import {
interface ApiTokenRecord { createApiToken,
id?: string | number; deleteApiToken,
label?: string; listApiTokens,
expires_at?: string; regenerateApiToken,
created_at?: string; type ApiTokenRecord,
last_used_at?: string; } from '../lib/apiClient';
capability_set_id?: string | number;
[key: string]: unknown;
}
interface ApiTokensResponse { interface ApiTokensResponse {
token_info?: ApiTokenRecord; token_info?: ApiTokenRecord;
@@ -22,11 +19,6 @@ interface CreateTokenArgs {
} }
interface UseApiTokensArgs { interface UseApiTokensArgs {
api: {
get: <T = unknown>(url: string) => Promise<{ data: T }>;
post: <T = unknown>(url: string, payload?: unknown) => Promise<{ data: T }>;
delete: (url: string) => Promise<unknown>;
};
notifyApiError?: (error: unknown, message: string) => void; notifyApiError?: (error: unknown, message: string) => void;
setStatusMessage?: (message: string, variant?: string) => void; setStatusMessage?: (message: string, variant?: string) => void;
token?: string | null; token?: string | null;
@@ -46,7 +38,7 @@ interface UseApiTokensResult {
dismissSecret: () => void; dismissSecret: () => void;
} }
const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTokensArgs): UseApiTokensResult => { const useApiTokens = ({ notifyApiError, setStatusMessage, token }: UseApiTokensArgs): UseApiTokensResult => {
const [tokens, setTokens] = useState<ApiTokenRecord[]>([]); const [tokens, setTokens] = useState<ApiTokenRecord[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
@@ -60,14 +52,14 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
} }
setLoading(true); setLoading(true);
try { try {
const { data } = await api.get<ApiTokenRecord[]>('/profile/api-tokens'); const data = await listApiTokens();
setTokens(Array.isArray(data) ? data : []); setTokens(Array.isArray(data) ? data : []);
} catch (error) { } catch (error) {
notifyApiError?.(error, 'Failed to load API tokens.'); notifyApiError?.(error, 'Failed to load API tokens.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [api, notifyApiError, token]); }, [notifyApiError, token]);
const create = useCallback( const create = useCallback(
async ({ label, expires_at, capability_set_id }: CreateTokenArgs = {}) => { async ({ label, expires_at, capability_set_id }: CreateTokenArgs = {}) => {
@@ -76,7 +68,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
} }
setCreating(true); setCreating(true);
try { try {
const payload: Record<string, unknown> = { capability_set_id }; const payload: { capability_set_id: string | number; label?: string; expires_at?: string } = { capability_set_id };
if (label) { if (label) {
payload.label = label; payload.label = label;
} }
@@ -84,7 +76,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
payload.expires_at = expires_at; payload.expires_at = expires_at;
} }
const { data } = await api.post<ApiTokensResponse>('/profile/api-tokens', payload); const data = await createApiToken(payload) as ApiTokensResponse;
if (data?.token_info) { if (data?.token_info) {
setTokens((previous) => { setTokens((previous) => {
const filtered = previous.filter((entry) => entry.id !== data.token_info?.id); const filtered = previous.filter((entry) => entry.id !== data.token_info?.id);
@@ -107,7 +99,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
setCreating(false); setCreating(false);
} }
}, },
[api, creating, notifyApiError, refresh, setStatusMessage], [creating, notifyApiError, refresh, setStatusMessage],
); );
const revoke = useCallback( const revoke = useCallback(
@@ -117,7 +109,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
} }
setDeletingId(tokenId); setDeletingId(tokenId);
try { try {
await api.delete(`/profile/api-tokens/${tokenId}`); await deleteApiToken(tokenId);
await refresh(); await refresh();
setStatusMessage?.('API token revoked.', 'success'); setStatusMessage?.('API token revoked.', 'success');
return true; return true;
@@ -128,7 +120,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
setDeletingId(null); setDeletingId(null);
} }
}, },
[api, notifyApiError, refresh, setStatusMessage], [notifyApiError, refresh, setStatusMessage],
); );
const regenerate = useCallback( const regenerate = useCallback(
@@ -138,7 +130,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
} }
setRegeneratingId(tokenId); setRegeneratingId(tokenId);
try { try {
const { data } = await api.post<ApiTokensResponse>(`/profile/api-tokens/${tokenId}/regenerate`); const data = await regenerateApiToken(tokenId) as ApiTokensResponse;
if (data?.token_info) { if (data?.token_info) {
setTokens((previous) => { setTokens((previous) => {
let found = false; let found = false;
@@ -171,7 +163,7 @@ const useApiTokens = ({ api, notifyApiError, setStatusMessage, token }: UseApiTo
setRegeneratingId(null); setRegeneratingId(null);
} }
}, },
[api, notifyApiError, refresh, setStatusMessage], [notifyApiError, refresh, setStatusMessage],
); );
const dismissSecret = useCallback(() => { const dismissSecret = useCallback(() => {
+11 -16
View File
@@ -1,5 +1,10 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { listCapabilitySets } from '../lib/apiClient'; import {
createCapabilitySet as createCapabilitySetRequest,
deleteCapabilitySet as deleteCapabilitySetRequest,
listCapabilitySets,
updateCapabilitySet as updateCapabilitySetRequest,
} from '../lib/apiClient';
type Identifier = string | number; type Identifier = string | number;
@@ -11,21 +16,13 @@ interface CapabilitySet {
[key: string]: unknown; [key: string]: unknown;
} }
interface ApiClient {
get<T = CapabilitySet[]>(path: string): Promise<{ data: T }>;
post<T = CapabilitySet>(path: string, payload?: unknown): Promise<{ data: T }>;
patch<T = CapabilitySet>(path: string, payload?: unknown): Promise<{ data: T }>;
delete: (path: string) => Promise<void>;
}
interface UseCapabilitySetsOptions { interface UseCapabilitySetsOptions {
api: ApiClient;
notifyApiError?: (error: unknown, message: string) => void; notifyApiError?: (error: unknown, message: string) => void;
setStatusMessage?: (message: string, level?: string) => void; setStatusMessage?: (message: string, level?: string) => void;
token?: string | null; token?: string | null;
} }
const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: UseCapabilitySetsOptions) => { const useCapabilitySets = ({ notifyApiError, setStatusMessage, token }: UseCapabilitySetsOptions) => {
const [capabilitySets, setCapabilitySets] = useState<CapabilitySet[]>([]); const [capabilitySets, setCapabilitySets] = useState<CapabilitySet[]>([]);
const [capabilitySetsLoading, setCapabilitySetsLoading] = useState(false); const [capabilitySetsLoading, setCapabilitySetsLoading] = useState(false);
const [creatingCapabilitySet, setCreatingCapabilitySet] = useState(false); const [creatingCapabilitySet, setCreatingCapabilitySet] = useState(false);
@@ -92,7 +89,7 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
payload.label = trimmedLabel; payload.label = trimmedLabel;
} }
const { data } = await api.post<CapabilitySet>('/capability-sets', payload); const data = await createCapabilitySetRequest(payload);
if (data) { if (data) {
applyCapabilitySets((previous) => { applyCapabilitySets((previous) => {
const next = previous.filter((entry) => entry?.id !== data.id); const next = previous.filter((entry) => entry?.id !== data.id);
@@ -113,7 +110,6 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
} }
}, },
[ [
api,
applyCapabilitySets, applyCapabilitySets,
creatingCapabilitySet, creatingCapabilitySet,
notifyApiError, notifyApiError,
@@ -154,7 +150,7 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
payload.capabilities = capabilities; payload.capabilities = capabilities;
} }
const { data } = await api.patch<CapabilitySet>(`/capability-sets/${capabilitySetId}`, payload); const data = await updateCapabilitySetRequest(capabilitySetId, payload);
if (data) { if (data) {
applyCapabilitySets((previous) => { applyCapabilitySets((previous) => {
let found = false; let found = false;
@@ -184,7 +180,6 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
} }
}, },
[ [
api,
applyCapabilitySets, applyCapabilitySets,
notifyApiError, notifyApiError,
refreshCapabilitySets, refreshCapabilitySets,
@@ -200,7 +195,7 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
} }
setDeletingCapabilitySetId(capabilitySetId); setDeletingCapabilitySetId(capabilitySetId);
try { try {
await api.delete(`/capability-sets/${capabilitySetId}`); await deleteCapabilitySetRequest(capabilitySetId);
applyCapabilitySets((previous) => previous.filter((entry) => entry?.id !== capabilitySetId)); applyCapabilitySets((previous) => previous.filter((entry) => entry?.id !== capabilitySetId));
setStatusMessage?.('Capability set deleted.', 'success'); setStatusMessage?.('Capability set deleted.', 'success');
return true; return true;
@@ -211,7 +206,7 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
setDeletingCapabilitySetId(null); setDeletingCapabilitySetId(null);
} }
}, },
[api, applyCapabilitySets, notifyApiError, setStatusMessage], [applyCapabilitySets, notifyApiError, setStatusMessage],
); );
return { return {
+14 -17
View File
@@ -6,12 +6,12 @@ import {
preparePublicKeyCreationOptions, preparePublicKeyCreationOptions,
serializeRegistrationCredential, serializeRegistrationCredential,
} from '../utils/webauthn'; } from '../utils/webauthn';
import {
type ApiClient = { deletePasskey,
get: (path: string) => Promise<{ data: unknown }>; finishPasskeyRegistration,
post: (path: string, body?: unknown) => Promise<{ data: unknown }>; listPasskeys,
delete: (path: string) => Promise<{ data: unknown }>; startPasskeyRegistration,
}; } from '../lib/apiClient';
type StatusMessageFn = (message: string, variant?: string) => void; type StatusMessageFn = (message: string, variant?: string) => void;
type NotifyApiErrorFn = (error: unknown, message: string) => void; type NotifyApiErrorFn = (error: unknown, message: string) => void;
@@ -69,7 +69,6 @@ export type RevokePasskeyResult =
| { ok: false; reason: RevokePasskeyFailureReason; message?: string }; | { ok: false; reason: RevokePasskeyFailureReason; message?: string };
interface UsePasskeysArgs { interface UsePasskeysArgs {
api: ApiClient;
notifyApiError: NotifyApiErrorFn; notifyApiError: NotifyApiErrorFn;
setStatusMessage: StatusMessageFn; setStatusMessage: StatusMessageFn;
token?: string | null; token?: string | null;
@@ -89,7 +88,7 @@ interface UsePasskeysResult {
) => Promise<RevokePasskeyResult>; ) => Promise<RevokePasskeyResult>;
} }
const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasskeysArgs): UsePasskeysResult => { const usePasskeys = ({ notifyApiError, setStatusMessage, token }: UsePasskeysArgs): UsePasskeysResult => {
const [passkeys, setPasskeys] = useState<PasskeyRecord[]>([]); const [passkeys, setPasskeys] = useState<PasskeyRecord[]>([]);
const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null); const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null);
const [passkeysLoading, setPasskeysLoading] = useState(false); const [passkeysLoading, setPasskeysLoading] = useState(false);
@@ -102,8 +101,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
} }
setPasskeysLoading(true); setPasskeysLoading(true);
try { try {
const { data } = await api.get('/profile/passkeys'); const passkeyData = await listPasskeys();
const passkeyData = Array.isArray(data) ? (data as PasskeyRecord[]) : [];
setPasskeys(passkeyData); setPasskeys(passkeyData);
setPasskeysSupported(true); setPasskeysSupported(true);
} catch (error) { } catch (error) {
@@ -117,7 +115,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
} finally { } finally {
setPasskeysLoading(false); setPasskeysLoading(false);
} }
}, [api, notifyApiError, token]); }, [notifyApiError, token]);
const registerPasskey = useCallback( const registerPasskey = useCallback(
async ({ nickname }: { nickname?: string } = {}): Promise<RegisterPasskeyResult> => { async ({ nickname }: { nickname?: string } = {}): Promise<RegisterPasskeyResult> => {
@@ -132,7 +130,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
setRegisteringPasskey(true); setRegisteringPasskey(true);
try { try {
const { data } = await api.post('/auth/passkeys/register/start', {}); const data = await startPasskeyRegistration();
const challengeData = data as PasskeyChallengeResponse | undefined; const challengeData = data as PasskeyChallengeResponse | undefined;
const challengeId = challengeData?.challengeId || challengeData?.challenge_id; const challengeId = challengeData?.challengeId || challengeData?.challenge_id;
const publicKeyOptions = const publicKeyOptions =
@@ -164,7 +162,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
payload.nickname = trimmedNickname; payload.nickname = trimmedNickname;
} }
await api.post('/auth/passkeys/register/finish', payload); await finishPasskeyRegistration(payload);
await refreshPasskeys(); await refreshPasskeys();
setPasskeysSupported(true); setPasskeysSupported(true);
setStatusMessage('Passkey registered.', 'success'); setStatusMessage('Passkey registered.', 'success');
@@ -188,7 +186,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
setRegisteringPasskey(false); setRegisteringPasskey(false);
} }
}, },
[api, notifyApiError, refreshPasskeys, registeringPasskey, setStatusMessage], [notifyApiError, refreshPasskeys, registeringPasskey, setStatusMessage],
); );
const revokePasskey = useCallback( const revokePasskey = useCallback(
@@ -201,8 +199,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
} }
setRevokingPasskeyId(passkeyId); setRevokingPasskeyId(passkeyId);
try { try {
const query = reason ? `?reason=${encodeURIComponent(reason)}` : ''; await deletePasskey(passkeyId, { reason });
await api.delete(`/profile/passkeys/${passkeyId}${query}`);
await refreshPasskeys(); await refreshPasskeys();
setStatusMessage('Passkey revoked.', 'success'); setStatusMessage('Passkey revoked.', 'success');
return { ok: true }; return { ok: true };
@@ -214,7 +211,7 @@ const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }: UsePasske
setRevokingPasskeyId(null); setRevokingPasskeyId(null);
} }
}, },
[api, notifyApiError, refreshPasskeys, setStatusMessage], [notifyApiError, refreshPasskeys, setStatusMessage],
); );
return { return {