frontend: apply bearer token to centralized API requests

This commit is contained in:
2026-02-21 15:03:13 -03:00
parent b25e508a00
commit c1a7011d71
4 changed files with 59 additions and 19 deletions

View File

@@ -101,6 +101,11 @@ Selected defaults from `Settings` (`backend/app/core/config.py`):
Frontend runtime API target: Frontend runtime API target:
- `VITE_API_BASE` in `docker-compose.yml` frontend service - `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: Frontend container runtime behavior:
- the container runs as non-root `node` - the container runs as non-root `node`

View File

@@ -112,6 +112,7 @@ services:
context: ./frontend context: ./frontend
environment: environment:
VITE_API_BASE: ${VITE_API_BASE:-http://localhost:8000/api/v1} VITE_API_BASE: ${VITE_API_BASE:-http://localhost:8000/api/v1}
VITE_API_TOKEN: ${VITE_API_TOKEN:-${USER_API_TOKEN:-}}
ports: ports:
- "${HOST_BIND_IP:-127.0.0.1}:5173:5173" - "${HOST_BIND_IP:-127.0.0.1}:5173:5173"
volumes: volumes:

View File

@@ -18,6 +18,39 @@ import type {
*/ */
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api/v1'; 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. * Encodes query parameters while skipping undefined and null values.
*/ */
@@ -72,7 +105,7 @@ export async function listDocuments(options?: {
processed_from: options?.processedFrom, processed_from: options?.processedFrom,
processed_to: options?.processedTo, processed_to: options?.processedTo,
}); });
const response = await fetch(`${API_BASE}/documents${query}`); const response = await apiRequest(`${API_BASE}/documents${query}`);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to load documents'); throw new Error('Failed to load documents');
} }
@@ -108,7 +141,7 @@ export async function searchDocuments(
processed_from: options?.processedFrom, processed_from: options?.processedFrom,
processed_to: options?.processedTo, processed_to: options?.processedTo,
}); });
const response = await fetch(`${API_BASE}/search${query}`); const response = await apiRequest(`${API_BASE}/search${query}`);
if (!response.ok) { if (!response.ok) {
throw new Error('Search failed'); throw new Error('Search failed');
} }
@@ -128,7 +161,7 @@ export async function listProcessingLogs(options?: {
offset: options?.offset ?? 0, offset: options?.offset ?? 0,
document_id: options?.documentId, 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) { if (!response.ok) {
throw new Error('Failed to load processing logs'); throw new Error('Failed to load processing logs');
} }
@@ -146,7 +179,7 @@ export async function trimProcessingLogs(options?: {
keep_document_sessions: options?.keepDocumentSessions ?? 2, keep_document_sessions: options?.keepDocumentSessions ?? 2,
keep_unbound_entries: options?.keepUnboundEntries ?? 80, 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', method: 'POST',
}); });
if (!response.ok) { if (!response.ok) {
@@ -159,7 +192,7 @@ export async function trimProcessingLogs(options?: {
* Clears all persisted processing logs. * Clears all persisted processing logs.
*/ */
export async function clearProcessingLogs(): Promise<{ deleted_entries: number }> { 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', method: 'POST',
}); });
if (!response.ok) { if (!response.ok) {
@@ -173,7 +206,7 @@ export async function clearProcessingLogs(): Promise<{ deleted_entries: number }
*/ */
export async function listTags(includeTrashed = false): Promise<string[]> { export async function listTags(includeTrashed = false): Promise<string[]> {
const query = buildQuery({ include_trashed: includeTrashed }); 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) { if (!response.ok) {
throw new Error('Failed to load tags'); 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[]> { export async function listPaths(includeTrashed = false): Promise<string[]> {
const query = buildQuery({ include_trashed: includeTrashed }); 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) { if (!response.ok) {
throw new Error('Failed to load paths'); 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[]> { export async function listTypes(includeTrashed = false): Promise<string[]> {
const query = buildQuery({ include_trashed: includeTrashed }); 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) { if (!response.ok) {
throw new Error('Failed to load document types'); throw new Error('Failed to load document types');
} }
@@ -228,7 +261,7 @@ export async function uploadDocuments(
formData.append('tags', options.tags); formData.append('tags', options.tags);
formData.append('conflict_mode', options.conflictMode); formData.append('conflict_mode', options.conflictMode);
const response = await fetch(`${API_BASE}/documents/upload`, { const response = await apiRequest(`${API_BASE}/documents/upload`, {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
@@ -245,7 +278,7 @@ export async function updateDocumentMetadata(
documentId: string, documentId: string,
payload: { original_filename?: string; logical_path?: string; tags?: string[] }, payload: { original_filename?: string; logical_path?: string; tags?: string[] },
): Promise<DmsDocument> { ): Promise<DmsDocument> {
const response = await fetch(`${API_BASE}/documents/${documentId}`, { const response = await apiRequest(`${API_BASE}/documents/${documentId}`, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -262,7 +295,7 @@ export async function updateDocumentMetadata(
* Moves a document to trash state without removing stored files. * Moves a document to trash state without removing stored files.
*/ */
export async function trashDocument(documentId: string): Promise<DmsDocument> { 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) { if (!response.ok) {
throw new Error('Failed to trash document'); 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. * Restores a document from trash to active state.
*/ */
export async function restoreDocument(documentId: string): Promise<DmsDocument> { 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) { if (!response.ok) {
throw new Error('Failed to restore document'); 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. * Permanently deletes a document record and associated stored files.
*/ */
export async function deleteDocument(documentId: string): Promise<{ deleted_documents: number; deleted_files: number }> { 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) { if (!response.ok) {
throw new Error('Failed to delete document'); 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. * Loads full details for one document, including extracted text content.
*/ */
export async function getDocumentDetails(documentId: string): Promise<DmsDocumentDetail> { 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) { if (!response.ok) {
throw new Error('Failed to load document details'); 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. * Re-enqueues one document for extraction and classification processing.
*/ */
export async function reprocessDocument(documentId: string): Promise<DmsDocument> { 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', method: 'POST',
}); });
if (!response.ok) { if (!response.ok) {
@@ -352,7 +385,7 @@ export async function exportContentsMarkdown(payload: {
include_trashed?: boolean; include_trashed?: boolean;
only_trashed?: boolean; only_trashed?: boolean;
}): Promise<{ blob: Blob; filename: string }> { }): 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -373,7 +406,7 @@ export async function exportContentsMarkdown(payload: {
* Retrieves persisted application settings from backend. * Retrieves persisted application settings from backend.
*/ */
export async function getAppSettings(): Promise<AppSettings> { export async function getAppSettings(): Promise<AppSettings> {
const response = await fetch(`${API_BASE}/settings`); const response = await apiRequest(`${API_BASE}/settings`);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to load application settings'); 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. * Updates provider and task settings for OpenAI-compatible model execution.
*/ */
export async function updateAppSettings(payload: AppSettingsUpdate): Promise<AppSettings> { export async function updateAppSettings(payload: AppSettingsUpdate): Promise<AppSettings> {
const response = await fetch(`${API_BASE}/settings`, { const response = await apiRequest(`${API_BASE}/settings`, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json', '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. * Resets persisted provider and task settings to backend defaults.
*/ */
export async function resetAppSettings(): Promise<AppSettings> { export async function resetAppSettings(): Promise<AppSettings> {
const response = await fetch(`${API_BASE}/settings/reset`, { const response = await apiRequest(`${API_BASE}/settings/reset`, {
method: 'POST', method: 'POST',
}); });
if (!response.ok) { if (!response.ok) {

View 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"}