frontend: apply bearer token to centralized API requests
This commit is contained in:
@@ -101,6 +101,11 @@ Selected defaults from `Settings` (`backend/app/core/config.py`):
|
||||
|
||||
Frontend runtime API target:
|
||||
- `VITE_API_BASE` in `docker-compose.yml` frontend service
|
||||
- `VITE_API_TOKEN` in `docker-compose.yml` frontend service (defaults to `USER_API_TOKEN` in compose, override to `ADMIN_API_TOKEN` when admin-only routes are needed)
|
||||
|
||||
Frontend API authentication behavior:
|
||||
- `frontend/src/lib/api.ts` adds `Authorization: Bearer <VITE_API_TOKEN>` for all API requests only when `VITE_API_TOKEN` is non-empty
|
||||
- requests are still sent without authorization when `VITE_API_TOKEN` is unset, which keeps unauthenticated endpoints such as `/api/v1/health` backward-compatible
|
||||
|
||||
Frontend container runtime behavior:
|
||||
- the container runs as non-root `node`
|
||||
|
||||
@@ -112,6 +112,7 @@ services:
|
||||
context: ./frontend
|
||||
environment:
|
||||
VITE_API_BASE: ${VITE_API_BASE:-http://localhost:8000/api/v1}
|
||||
VITE_API_TOKEN: ${VITE_API_TOKEN:-${USER_API_TOKEN:-}}
|
||||
ports:
|
||||
- "${HOST_BIND_IP:-127.0.0.1}:5173:5173"
|
||||
volumes:
|
||||
|
||||
@@ -18,6 +18,39 @@ import type {
|
||||
*/
|
||||
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api/v1';
|
||||
|
||||
/**
|
||||
* Optional bearer token used for authenticated backend routes.
|
||||
*/
|
||||
const API_TOKEN = import.meta.env.VITE_API_TOKEN?.trim();
|
||||
|
||||
type ApiRequestInit = Omit<RequestInit, 'headers'> & { headers?: HeadersInit };
|
||||
|
||||
/**
|
||||
* Merges request headers and appends bearer authorization when configured.
|
||||
*/
|
||||
function buildRequestHeaders(headers?: HeadersInit): Headers | undefined {
|
||||
if (!API_TOKEN && !headers) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const requestHeaders = new Headers(headers);
|
||||
if (API_TOKEN) {
|
||||
requestHeaders.set('Authorization', `Bearer ${API_TOKEN}`);
|
||||
}
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an API request with centralized auth-header handling.
|
||||
*/
|
||||
function apiRequest(input: string, init: ApiRequestInit = {}): Promise<Response> {
|
||||
const headers = buildRequestHeaders(init.headers);
|
||||
return fetch(input, {
|
||||
...init,
|
||||
...(headers ? { headers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes query parameters while skipping undefined and null values.
|
||||
*/
|
||||
@@ -72,7 +105,7 @@ export async function listDocuments(options?: {
|
||||
processed_from: options?.processedFrom,
|
||||
processed_to: options?.processedTo,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/documents${query}`);
|
||||
const response = await apiRequest(`${API_BASE}/documents${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load documents');
|
||||
}
|
||||
@@ -108,7 +141,7 @@ export async function searchDocuments(
|
||||
processed_from: options?.processedFrom,
|
||||
processed_to: options?.processedTo,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/search${query}`);
|
||||
const response = await apiRequest(`${API_BASE}/search${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Search failed');
|
||||
}
|
||||
@@ -128,7 +161,7 @@ export async function listProcessingLogs(options?: {
|
||||
offset: options?.offset ?? 0,
|
||||
document_id: options?.documentId,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/processing/logs${query}`);
|
||||
const response = await apiRequest(`${API_BASE}/processing/logs${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load processing logs');
|
||||
}
|
||||
@@ -146,7 +179,7 @@ export async function trimProcessingLogs(options?: {
|
||||
keep_document_sessions: options?.keepDocumentSessions ?? 2,
|
||||
keep_unbound_entries: options?.keepUnboundEntries ?? 80,
|
||||
});
|
||||
const response = await fetch(`${API_BASE}/processing/logs/trim${query}`, {
|
||||
const response = await apiRequest(`${API_BASE}/processing/logs/trim${query}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -159,7 +192,7 @@ export async function trimProcessingLogs(options?: {
|
||||
* Clears all persisted processing logs.
|
||||
*/
|
||||
export async function clearProcessingLogs(): Promise<{ deleted_entries: number }> {
|
||||
const response = await fetch(`${API_BASE}/processing/logs/clear`, {
|
||||
const response = await apiRequest(`${API_BASE}/processing/logs/clear`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -173,7 +206,7 @@ export async function clearProcessingLogs(): Promise<{ deleted_entries: number }
|
||||
*/
|
||||
export async function listTags(includeTrashed = false): Promise<string[]> {
|
||||
const query = buildQuery({ include_trashed: includeTrashed });
|
||||
const response = await fetch(`${API_BASE}/documents/tags${query}`);
|
||||
const response = await apiRequest(`${API_BASE}/documents/tags${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load tags');
|
||||
}
|
||||
@@ -186,7 +219,7 @@ export async function listTags(includeTrashed = false): Promise<string[]> {
|
||||
*/
|
||||
export async function listPaths(includeTrashed = false): Promise<string[]> {
|
||||
const query = buildQuery({ include_trashed: includeTrashed });
|
||||
const response = await fetch(`${API_BASE}/documents/paths${query}`);
|
||||
const response = await apiRequest(`${API_BASE}/documents/paths${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load paths');
|
||||
}
|
||||
@@ -199,7 +232,7 @@ export async function listPaths(includeTrashed = false): Promise<string[]> {
|
||||
*/
|
||||
export async function listTypes(includeTrashed = false): Promise<string[]> {
|
||||
const query = buildQuery({ include_trashed: includeTrashed });
|
||||
const response = await fetch(`${API_BASE}/documents/types${query}`);
|
||||
const response = await apiRequest(`${API_BASE}/documents/types${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load document types');
|
||||
}
|
||||
@@ -228,7 +261,7 @@ export async function uploadDocuments(
|
||||
formData.append('tags', options.tags);
|
||||
formData.append('conflict_mode', options.conflictMode);
|
||||
|
||||
const response = await fetch(`${API_BASE}/documents/upload`, {
|
||||
const response = await apiRequest(`${API_BASE}/documents/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
@@ -245,7 +278,7 @@ export async function updateDocumentMetadata(
|
||||
documentId: string,
|
||||
payload: { original_filename?: string; logical_path?: string; tags?: string[] },
|
||||
): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}`, {
|
||||
const response = await apiRequest(`${API_BASE}/documents/${documentId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -262,7 +295,7 @@ export async function updateDocumentMetadata(
|
||||
* Moves a document to trash state without removing stored files.
|
||||
*/
|
||||
export async function trashDocument(documentId: string): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}/trash`, { method: 'POST' });
|
||||
const response = await apiRequest(`${API_BASE}/documents/${documentId}/trash`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to trash document');
|
||||
}
|
||||
@@ -273,7 +306,7 @@ export async function trashDocument(documentId: string): Promise<DmsDocument> {
|
||||
* Restores a document from trash to active state.
|
||||
*/
|
||||
export async function restoreDocument(documentId: string): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}/restore`, { method: 'POST' });
|
||||
const response = await apiRequest(`${API_BASE}/documents/${documentId}/restore`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to restore document');
|
||||
}
|
||||
@@ -284,7 +317,7 @@ export async function restoreDocument(documentId: string): Promise<DmsDocument>
|
||||
* Permanently deletes a document record and associated stored files.
|
||||
*/
|
||||
export async function deleteDocument(documentId: string): Promise<{ deleted_documents: number; deleted_files: number }> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}`, { method: 'DELETE' });
|
||||
const response = await apiRequest(`${API_BASE}/documents/${documentId}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete document');
|
||||
}
|
||||
@@ -295,7 +328,7 @@ export async function deleteDocument(documentId: string): Promise<{ deleted_docu
|
||||
* Loads full details for one document, including extracted text content.
|
||||
*/
|
||||
export async function getDocumentDetails(documentId: string): Promise<DmsDocumentDetail> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}`);
|
||||
const response = await apiRequest(`${API_BASE}/documents/${documentId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load document details');
|
||||
}
|
||||
@@ -306,7 +339,7 @@ export async function getDocumentDetails(documentId: string): Promise<DmsDocumen
|
||||
* Re-enqueues one document for extraction and classification processing.
|
||||
*/
|
||||
export async function reprocessDocument(documentId: string): Promise<DmsDocument> {
|
||||
const response = await fetch(`${API_BASE}/documents/${documentId}/reprocess`, {
|
||||
const response = await apiRequest(`${API_BASE}/documents/${documentId}/reprocess`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -352,7 +385,7 @@ export async function exportContentsMarkdown(payload: {
|
||||
include_trashed?: boolean;
|
||||
only_trashed?: boolean;
|
||||
}): Promise<{ blob: Blob; filename: string }> {
|
||||
const response = await fetch(`${API_BASE}/documents/content-md/export`, {
|
||||
const response = await apiRequest(`${API_BASE}/documents/content-md/export`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -373,7 +406,7 @@ export async function exportContentsMarkdown(payload: {
|
||||
* Retrieves persisted application settings from backend.
|
||||
*/
|
||||
export async function getAppSettings(): Promise<AppSettings> {
|
||||
const response = await fetch(`${API_BASE}/settings`);
|
||||
const response = await apiRequest(`${API_BASE}/settings`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load application settings');
|
||||
}
|
||||
@@ -384,7 +417,7 @@ export async function getAppSettings(): Promise<AppSettings> {
|
||||
* Updates provider and task settings for OpenAI-compatible model execution.
|
||||
*/
|
||||
export async function updateAppSettings(payload: AppSettingsUpdate): Promise<AppSettings> {
|
||||
const response = await fetch(`${API_BASE}/settings`, {
|
||||
const response = await apiRequest(`${API_BASE}/settings`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -401,7 +434,7 @@ export async function updateAppSettings(payload: AppSettingsUpdate): Promise<App
|
||||
* Resets persisted provider and task settings to backend defaults.
|
||||
*/
|
||||
export async function resetAppSettings(): Promise<AppSettings> {
|
||||
const response = await fetch(`${API_BASE}/settings/reset`, {
|
||||
const response = await apiRequest(`${API_BASE}/settings/reset`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
1
frontend/tsconfig.tsbuildinfo
Normal file
1
frontend/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/actionmodal.tsx","./src/components/documentcard.tsx","./src/components/documentgrid.tsx","./src/components/documentviewer.tsx","./src/components/pathinput.tsx","./src/components/processinglogpanel.tsx","./src/components/searchfiltersbar.tsx","./src/components/settingsscreen.tsx","./src/components/taginput.tsx","./src/components/uploadsurface.tsx","./src/lib/api.ts"],"version":"5.9.2"}
|
||||
Reference in New Issue
Block a user