Replace Axios with native fetch; add global 401 session-expiry redirect
All API calls now go through a thin request() wrapper around native fetch. Removes the axios dependency entirely. The wrapper injects the JWT on every request and — the key fix — clears localStorage and redirects to /login on any 401 response, so expired sessions no longer leave users on broken pages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+21
-9
@@ -22,7 +22,7 @@ frontend/
|
||||
├── src/
|
||||
│ ├── main.tsx ← React root, QueryClientProvider, BrowserRouter
|
||||
│ ├── App.tsx ← Route tree, PrivateRoute, AdminRoute
|
||||
│ ├── api/client.ts ← Axios instance + ALL API functions (single source of truth)
|
||||
│ ├── api/client.ts ← Native fetch wrapper (`request()`) + ALL API functions (single source of truth); no Axios
|
||||
│ ├── hooks/
|
||||
│ │ ├── useAuth.ts ← Token state (localStorage), login/logout
|
||||
│ │ └── useTheme.ts ← Theme toggle
|
||||
@@ -86,21 +86,33 @@ frontend/
|
||||
|
||||
### API client (`src/api/client.ts`)
|
||||
|
||||
Single Axios instance — **all** API calls live here, nowhere else:
|
||||
**No Axios** — uses a thin native `fetch` wrapper. All API calls live here, nowhere else.
|
||||
|
||||
The core `request()` function handles:
|
||||
- Prepends `/api` base URL
|
||||
- Injects `Authorization: Bearer {token}` from `localStorage` on every request
|
||||
- **Global 401 handler**: clears `localStorage` token and redirects to `/login` via `window.location.href` — this is the expired-session redirect
|
||||
- Throws `ApiError(status, detail)` on non-2xx responses (detail parsed from JSON body)
|
||||
- Returns `undefined` on 204 No Content
|
||||
- Supports `blob: true` for file download/preview responses
|
||||
|
||||
```typescript
|
||||
const api = axios.create({ baseURL: "/api" });
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
// The internal api object — use these methods in exported functions:
|
||||
api.get<T>(path, params?) // GET with optional query params object
|
||||
api.post<T>(path, json?) // POST with JSON body
|
||||
api.postForm<T>(path, URLSearchParams) // POST with form-encoded body (login)
|
||||
api.postFile<T>(path, FormData) // POST with multipart body (file upload)
|
||||
api.patch<T>(path, json?) // PATCH with JSON body
|
||||
api.delete<T>(path) // DELETE
|
||||
api.getBlob(path) // GET → Blob (download / view)
|
||||
```
|
||||
|
||||
Adding a new API call:
|
||||
1. Define a TypeScript interface for the response if it's new.
|
||||
2. Add a named export function (`getX`, `createX`, `updateX`, `deleteX`).
|
||||
3. Use `api.get<T>(...)`, `api.post<T>(...)`, etc.; always `.then((r) => r.data)`.
|
||||
3. Use the appropriate `api.*` method — return the promise directly (no `.then((r) => r.data)`).
|
||||
|
||||
Error handling in components: catch blocks receive an `ApiError` instance with `.status` and `.message` (the detail string).
|
||||
|
||||
### TanStack Query conventions
|
||||
|
||||
|
||||
@@ -130,6 +130,8 @@ Display and edit personal information.
|
||||
|
||||
## API client (`src/api/client.ts`)
|
||||
|
||||
**No Axios** — replaced with a native `fetch` wrapper (`request()`). Global 401 handler clears the token and redirects to `/login`, fixing the expired-session blank-page bug. All exported function signatures are unchanged.
|
||||
|
||||
Key document-related functions:
|
||||
|
||||
| Function | Description |
|
||||
@@ -199,6 +201,7 @@ Key document-related functions:
|
||||
- [x] Document sharing UI (Sharing section + Shared with me view)
|
||||
- [x] AI suggestion confirm/reject UI (folder + filename)
|
||||
- [x] Groups admin UI
|
||||
- [x] Replace Axios with native fetch; add global 401 → `/login` redirect for expired sessions
|
||||
- [ ] Toast notification system
|
||||
- [ ] Loading skeletons
|
||||
- [ ] Cmd+K global search (`CommandDialog`)
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@tanstack/react-query": "^5.40.0",
|
||||
"axios": "^1.7.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-react": "^0.400.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
+219
-101
@@ -1,25 +1,126 @@
|
||||
import axios from "axios";
|
||||
const BASE = "/api";
|
||||
|
||||
const api = axios.create({ baseURL: "/api" });
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core fetch wrapper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
class ApiError extends Error {
|
||||
status: number;
|
||||
detail: string;
|
||||
|
||||
constructor(status: number, detail: string) {
|
||||
super(detail);
|
||||
this.status = status;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
options: {
|
||||
json?: unknown;
|
||||
form?: URLSearchParams;
|
||||
body?: FormData;
|
||||
params?: Record<string, string | number | undefined>;
|
||||
blob?: boolean;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const { json, form, body, params, blob: asBlob } = options;
|
||||
|
||||
// Build URL with optional query params
|
||||
let url = `${BASE}${path}`;
|
||||
if (params) {
|
||||
const qs = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v !== undefined && v !== "") qs.set(k, String(v));
|
||||
}
|
||||
const str = qs.toString();
|
||||
if (str) url += `?${str}`;
|
||||
}
|
||||
|
||||
// Build headers
|
||||
const headers: Record<string, string> = {};
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
if (json !== undefined) headers["Content-Type"] = "application/json";
|
||||
if (form !== undefined) headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
// FormData: intentionally omit Content-Type so browser sets multipart boundary
|
||||
|
||||
// Build body
|
||||
let requestBody: BodyInit | undefined;
|
||||
if (json !== undefined) requestBody = JSON.stringify(json);
|
||||
else if (form !== undefined) requestBody = form;
|
||||
else if (body !== undefined) requestBody = body;
|
||||
|
||||
const response = await fetch(url, { method, headers, body: requestBody });
|
||||
|
||||
// Global 401 handler — expired or invalid token
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
window.location.href = "/login";
|
||||
// Throw so callers don't try to process the response
|
||||
throw new ApiError(401, "Session expired");
|
||||
}
|
||||
|
||||
// Parse error responses
|
||||
if (!response.ok) {
|
||||
let detail = `HTTP ${response.status}`;
|
||||
try {
|
||||
const err = await response.json();
|
||||
if (typeof err.detail === "string") detail = err.detail;
|
||||
else if (Array.isArray(err.detail)) detail = err.detail.map((e: { msg: string }) => e.msg).join(", ");
|
||||
} catch {
|
||||
// non-JSON error body — keep default message
|
||||
}
|
||||
throw new ApiError(response.status, detail);
|
||||
}
|
||||
|
||||
// 204 No Content
|
||||
if (response.status === 204) return undefined as T;
|
||||
|
||||
if (asBlob) return response.blob() as Promise<T>;
|
||||
|
||||
const text = await response.text();
|
||||
return text ? (JSON.parse(text) as T) : (undefined as T);
|
||||
}
|
||||
|
||||
const api = {
|
||||
get: <T>(path: string, params?: Record<string, string | number | undefined>) =>
|
||||
request<T>("GET", path, { params }),
|
||||
post: <T>(path: string, json?: unknown) =>
|
||||
request<T>("POST", path, { json }),
|
||||
postForm: <T>(path: string, form: URLSearchParams) =>
|
||||
request<T>("POST", path, { form }),
|
||||
postFile: <T>(path: string, body: FormData) =>
|
||||
request<T>("POST", path, { body }),
|
||||
patch: <T>(path: string, json?: unknown) =>
|
||||
request<T>("PATCH", path, { json }),
|
||||
delete: <T>(path: string) =>
|
||||
request<T>("DELETE", path),
|
||||
getBlob: (path: string) =>
|
||||
request<Blob>("GET", path, { blob: true }),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
// --- Auth ---
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const login = (email: string, password: string) =>
|
||||
api
|
||||
.post<{ access_token: string }>("/auth/login", new URLSearchParams({ username: email, password }))
|
||||
.then((r) => r.data.access_token);
|
||||
api.postForm<{ access_token: string }>(
|
||||
"/auth/login",
|
||||
new URLSearchParams({ username: email, password })
|
||||
).then((r) => r.access_token);
|
||||
|
||||
export const register = (email: string, password: string, full_name?: string) =>
|
||||
api.post("/auth/register", { email, password, full_name }).then((r) => r.data);
|
||||
api.post("/auth/register", { email, password, full_name });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Users
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Users ---
|
||||
export interface UserData {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -29,19 +130,21 @@ export interface UserData {
|
||||
color_mode: string | null;
|
||||
}
|
||||
|
||||
export const getMe = () => api.get<UserData>("/users/me").then((r) => r.data);
|
||||
export const getMe = () => api.get<UserData>("/users/me");
|
||||
|
||||
export interface DashboardPrefs {
|
||||
app_ids: string[];
|
||||
}
|
||||
|
||||
export const getDashboardPrefs = () =>
|
||||
api.get<DashboardPrefs>("/users/me/preferences").then((r) => r.data);
|
||||
export const getDashboardPrefs = () => api.get<DashboardPrefs>("/users/me/preferences");
|
||||
|
||||
export const updateDashboardPrefs = (app_ids: string[]) =>
|
||||
api.patch<DashboardPrefs>("/users/me/preferences", { app_ids }).then((r) => r.data);
|
||||
api.patch<DashboardPrefs>("/users/me/preferences", { app_ids });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin — Users
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Admin ---
|
||||
export interface AdminUserCreate {
|
||||
email: string;
|
||||
password: string;
|
||||
@@ -49,19 +152,21 @@ export interface AdminUserCreate {
|
||||
is_admin?: boolean;
|
||||
}
|
||||
|
||||
export const adminGetUsers = () =>
|
||||
api.get<UserData[]>("/admin/users").then((r) => r.data);
|
||||
export const adminGetUsers = () => api.get<UserData[]>("/admin/users");
|
||||
|
||||
export const adminCreateUser = (data: AdminUserCreate) =>
|
||||
api.post<UserData>("/admin/users", data).then((r) => r.data);
|
||||
api.post<UserData>("/admin/users", data);
|
||||
|
||||
export const adminDeleteUser = (userId: string) =>
|
||||
api.delete(`/admin/users/${userId}`);
|
||||
|
||||
export const adminToggleActive = (userId: string) =>
|
||||
api.patch<UserData>(`/admin/users/${userId}/active`).then((r) => r.data);
|
||||
api.patch<UserData>(`/admin/users/${userId}/active`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Profile
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Profile ---
|
||||
export interface ProfileData {
|
||||
id: string;
|
||||
user_id: string;
|
||||
@@ -79,13 +184,15 @@ export interface ProfileUpdate {
|
||||
address?: string | null;
|
||||
}
|
||||
|
||||
export const getProfile = () =>
|
||||
api.get<ProfileData>("/profile/me").then((r) => r.data);
|
||||
export const getProfile = () => api.get<ProfileData>("/profile/me");
|
||||
|
||||
export const updateProfile = (data: ProfileUpdate) =>
|
||||
api.put<ProfileData>("/profile/me", data).then((r) => r.data);
|
||||
api.patch<ProfileData>("/profile/me", data);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Documents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Documents ---
|
||||
export type DocumentStatus = "pending" | "processing" | "done" | "failed";
|
||||
|
||||
export interface CategoryOut {
|
||||
@@ -154,41 +261,40 @@ export interface DocumentStatusOut {
|
||||
}
|
||||
|
||||
export const listDocuments = (params: DocumentListParams = {}) =>
|
||||
api.get<DocumentPage>("/documents", { params }).then((r) => r.data);
|
||||
api.get<DocumentPage>("/documents", params as Record<string, string | number | undefined>);
|
||||
|
||||
export const listSharedWithMe = (params: DocumentListParams = {}) =>
|
||||
api.get<DocumentPage>("/documents/shared-with-me", { params }).then((r) => r.data);
|
||||
api.get<DocumentPage>("/documents/shared-with-me", params as Record<string, string | number | undefined>);
|
||||
|
||||
export const getDocumentShares = (docId: string) =>
|
||||
api.get<DocumentShareOut[]>(`/documents/${docId}/shares`).then((r) => r.data);
|
||||
api.get<DocumentShareOut[]>(`/documents/${docId}/shares`);
|
||||
|
||||
export const addDocumentShare = (docId: string, groupId: string) =>
|
||||
api.post<DocumentShareOut>(`/documents/${docId}/shares`, { group_id: groupId }).then((r) => r.data);
|
||||
api.post<DocumentShareOut>(`/documents/${docId}/shares`, { group_id: groupId });
|
||||
|
||||
export const removeDocumentShare = (docId: string, groupId: string) =>
|
||||
api.delete(`/documents/${docId}/shares/${groupId}`);
|
||||
|
||||
export const getDocument = (id: string) =>
|
||||
api.get<DocumentOut>(`/documents/${id}`).then((r) => r.data);
|
||||
api.get<DocumentOut>(`/documents/${id}`);
|
||||
|
||||
export const getDocumentStatus = (id: string) =>
|
||||
api.get<DocumentStatusOut>(`/documents/${id}/status`).then((r) => r.data);
|
||||
api.get<DocumentStatusOut>(`/documents/${id}/status`);
|
||||
|
||||
export const uploadDocument = (file: File) => {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
return api.post<DocumentOut>("/documents/upload", form).then((r) => r.data);
|
||||
return api.postFile<DocumentOut>("/documents/upload", form);
|
||||
};
|
||||
|
||||
export const updateDocumentType = (id: string, document_type: string) =>
|
||||
api.patch<DocumentOut>(`/documents/${id}/type`, { document_type }).then((r) => r.data);
|
||||
api.patch<DocumentOut>(`/documents/${id}/type`, { document_type });
|
||||
|
||||
export const deleteDocument = (id: string) =>
|
||||
api.delete(`/documents/${id}`);
|
||||
export const deleteDocument = (id: string) => api.delete(`/documents/${id}`);
|
||||
|
||||
export const downloadDocument = async (id: string, filename: string) => {
|
||||
const response = await api.get(`/documents/${id}/file`, { responseType: "blob" });
|
||||
const url = URL.createObjectURL(response.data);
|
||||
const blob = await api.getBlob(`/documents/${id}/file`);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
@@ -197,22 +303,21 @@ export const downloadDocument = async (id: string, filename: string) => {
|
||||
};
|
||||
|
||||
export const viewDocument = async (id: string): Promise<void> => {
|
||||
const response = await api.get(`/documents/${id}/file`, { responseType: "blob" });
|
||||
const url = URL.createObjectURL(response.data);
|
||||
const blob = await api.getBlob(`/documents/${id}/file`);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const win = window.open(url, "_blank");
|
||||
// Revoke after a generous delay — the new tab needs time to load the blob
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
if (!win) alert("Pop-up blocked. Please allow pop-ups for this site to preview PDFs.");
|
||||
};
|
||||
|
||||
export const updateDocumentTags = (id: string, tags: string[]) =>
|
||||
api.patch<DocumentOut>(`/documents/${id}/tags`, { tags }).then((r) => r.data);
|
||||
api.patch<DocumentOut>(`/documents/${id}/tags`, { tags });
|
||||
|
||||
export const updateDocumentTitle = (id: string, title: string) =>
|
||||
api.patch<DocumentOut>(`/documents/${id}/title`, { title }).then((r) => r.data);
|
||||
api.patch<DocumentOut>(`/documents/${id}/title`, { title });
|
||||
|
||||
export const reprocessDocument = (id: string) =>
|
||||
api.post<DocumentOut>(`/documents/${id}/reprocess`).then((r) => r.data);
|
||||
api.post<DocumentOut>(`/documents/${id}/reprocess`);
|
||||
|
||||
export const assignCategory = (docId: string, catId: string) =>
|
||||
api.post(`/documents/${docId}/categories/${catId}`);
|
||||
@@ -220,20 +325,24 @@ export const assignCategory = (docId: string, catId: string) =>
|
||||
export const removeCategory = (docId: string, catId: string) =>
|
||||
api.delete(`/documents/${docId}/categories/${catId}`);
|
||||
|
||||
// --- Categories ---
|
||||
export const listCategories = () =>
|
||||
api.get<CategoryOut[]>("/documents/categories").then((r) => r.data);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Categories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const listCategories = () => api.get<CategoryOut[]>("/documents/categories");
|
||||
|
||||
export const createCategory = (name: string) =>
|
||||
api.post<CategoryOut>("/documents/categories", { name }).then((r) => r.data);
|
||||
api.post<CategoryOut>("/documents/categories", { name });
|
||||
|
||||
export const renameCategory = (id: string, name: string) =>
|
||||
api.patch<CategoryOut>(`/documents/categories/${id}`, { name }).then((r) => r.data);
|
||||
api.patch<CategoryOut>(`/documents/categories/${id}`, { name });
|
||||
|
||||
export const deleteCategory = (id: string) =>
|
||||
api.delete(`/documents/categories/${id}`);
|
||||
export const deleteCategory = (id: string) => api.delete(`/documents/categories/${id}`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Appearance & Themes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Appearance & Themes ---
|
||||
export interface ThemeColors {
|
||||
primary: string;
|
||||
primary_hover: string;
|
||||
@@ -259,31 +368,31 @@ export interface AppearanceSettings {
|
||||
default_mode: string;
|
||||
}
|
||||
|
||||
export const getAppearanceSettings = (): Promise<AppearanceSettings> =>
|
||||
api.get<AppearanceSettings>("/settings/appearance").then((r) => r.data);
|
||||
export const getAppearanceSettings = () =>
|
||||
api.get<AppearanceSettings>("/settings/appearance");
|
||||
|
||||
export const updateAppearanceSettings = (data: AppearanceSettings): Promise<AppearanceSettings> =>
|
||||
api.patch<AppearanceSettings>("/settings/appearance", data).then((r) => r.data);
|
||||
export const updateAppearanceSettings = (data: AppearanceSettings) =>
|
||||
api.patch<AppearanceSettings>("/settings/appearance", data);
|
||||
|
||||
export const getThemes = (): Promise<ThemeDefinition[]> =>
|
||||
api.get<ThemeDefinition[]>("/settings/themes").then((r) => r.data);
|
||||
export const getThemes = () => api.get<ThemeDefinition[]>("/settings/themes");
|
||||
|
||||
export const createTheme = (data: Omit<ThemeDefinition, "builtin">): Promise<ThemeDefinition> =>
|
||||
api.post<ThemeDefinition>("/settings/themes", data).then((r) => r.data);
|
||||
export const createTheme = (data: Omit<ThemeDefinition, "builtin">) =>
|
||||
api.post<ThemeDefinition>("/settings/themes", data);
|
||||
|
||||
export const updateTheme = (
|
||||
id: string,
|
||||
data: { label?: string; light?: ThemeColors; dark?: ThemeColors }
|
||||
): Promise<ThemeDefinition> =>
|
||||
api.patch<ThemeDefinition>(`/settings/themes/${id}`, data).then((r) => r.data);
|
||||
) => api.patch<ThemeDefinition>(`/settings/themes/${id}`, data);
|
||||
|
||||
export const deleteTheme = (id: string): Promise<void> =>
|
||||
api.delete(`/settings/themes/${id}`).then((r) => r.data);
|
||||
export const deleteTheme = (id: string) => api.delete(`/settings/themes/${id}`);
|
||||
|
||||
export const updateColorMode = (color_mode: string): Promise<UserData> =>
|
||||
api.patch<UserData>("/users/me/color-mode", { color_mode }).then((r) => r.data);
|
||||
export const updateColorMode = (color_mode: string) =>
|
||||
api.patch<UserData>("/users/me/color-mode", { color_mode });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings (admin only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Settings (admin only) ---
|
||||
export interface AIProviderUpdate {
|
||||
provider: string;
|
||||
anthropic_api_key?: string;
|
||||
@@ -297,35 +406,38 @@ export interface AIProviderUpdate {
|
||||
}
|
||||
|
||||
export const getAISettings = () =>
|
||||
api.get<Record<string, unknown>>("/settings/ai").then((r) => r.data);
|
||||
api.get<Record<string, unknown>>("/settings/ai");
|
||||
|
||||
export const updateAISettings = (data: AIProviderUpdate) =>
|
||||
api.patch<Record<string, unknown>>("/settings/ai", data).then((r) => r.data);
|
||||
api.patch<Record<string, unknown>>("/settings/ai", data);
|
||||
|
||||
export const testAIConnection = () =>
|
||||
api.post<{ ok: boolean; provider: string; response?: string; error?: string }>(
|
||||
"/settings/ai/test"
|
||||
).then((r) => r.data);
|
||||
|
||||
export const updateDocumentLimits = (max_pdf_mb: number) =>
|
||||
api.patch<Record<string, unknown>>("/settings/documents/limits", { max_pdf_mb }).then(
|
||||
(r) => r.data
|
||||
);
|
||||
|
||||
export const getDocumentLimits = () =>
|
||||
api.get<Record<string, unknown>>("/settings/documents/limits").then((r) => r.data);
|
||||
export const updateDocumentLimits = (max_pdf_mb: number) =>
|
||||
api.patch<Record<string, unknown>>("/settings/documents/limits", { max_pdf_mb });
|
||||
|
||||
export const getDocumentLimits = () =>
|
||||
api.get<Record<string, unknown>>("/settings/documents/limits");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User groups (current user's own memberships)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- User groups (current user's own memberships) ---
|
||||
export interface UserGroupOut {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export const getMyGroups = () =>
|
||||
api.get<UserGroupOut[]>("/users/me/groups").then((r) => r.data);
|
||||
export const getMyGroups = () => api.get<UserGroupOut[]>("/users/me/groups");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Groups (admin only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Groups (admin only) ---
|
||||
export interface GroupOut {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -356,17 +468,16 @@ export interface GroupUpdate {
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export const adminListGroups = () =>
|
||||
api.get<GroupOut[]>("/admin/groups").then((r) => r.data);
|
||||
export const adminListGroups = () => api.get<GroupOut[]>("/admin/groups");
|
||||
|
||||
export const adminCreateGroup = (data: GroupCreate) =>
|
||||
api.post<GroupOut>("/admin/groups", data).then((r) => r.data);
|
||||
api.post<GroupOut>("/admin/groups", data);
|
||||
|
||||
export const adminGetGroup = (groupId: string) =>
|
||||
api.get<GroupDetailOut>(`/admin/groups/${groupId}`).then((r) => r.data);
|
||||
api.get<GroupDetailOut>(`/admin/groups/${groupId}`);
|
||||
|
||||
export const adminUpdateGroup = (groupId: string, data: GroupUpdate) =>
|
||||
api.patch<GroupOut>(`/admin/groups/${groupId}`, data).then((r) => r.data);
|
||||
api.patch<GroupOut>(`/admin/groups/${groupId}`, data);
|
||||
|
||||
export const adminDeleteGroup = (groupId: string) =>
|
||||
api.delete(`/admin/groups/${groupId}`);
|
||||
@@ -377,7 +488,10 @@ export const adminAddGroupMember = (groupId: string, userId: string) =>
|
||||
export const adminRemoveGroupMember = (groupId: string, userId: string) =>
|
||||
api.delete(`/admin/groups/${groupId}/members/${userId}`);
|
||||
|
||||
// --- Services ---
|
||||
// ---------------------------------------------------------------------------
|
||||
// Services
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ServiceStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -387,10 +501,12 @@ export interface ServiceStatus {
|
||||
settings_path: string;
|
||||
}
|
||||
|
||||
export const getServices = () =>
|
||||
api.get<ServiceStatus[]>("/services").then((r) => r.data);
|
||||
export const getServices = () => api.get<ServiceStatus[]>("/services");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System Prompts (admin only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- System Prompts (admin only) ---
|
||||
export interface ServiceSystemPrompt {
|
||||
label: string;
|
||||
system: string;
|
||||
@@ -402,17 +518,17 @@ export interface ServiceSystemPrompt {
|
||||
export type SystemPromptsData = Record<string, ServiceSystemPrompt>;
|
||||
|
||||
export const getSystemPrompts = () =>
|
||||
api.get<SystemPromptsData>("/settings/system-prompts").then((r) => r.data);
|
||||
api.get<SystemPromptsData>("/settings/system-prompts");
|
||||
|
||||
export const updateSystemPrompt = (
|
||||
serviceId: string,
|
||||
data: { system: string; user_template: string }
|
||||
) =>
|
||||
api
|
||||
.patch<SystemPromptsData>(`/settings/system-prompts/${serviceId}`, data)
|
||||
.then((r) => r.data);
|
||||
) => api.patch<SystemPromptsData>(`/settings/system-prompts/${serviceId}`, data);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document suggestions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- Document suggestions (watch-ingested documents) ---
|
||||
export const confirmFolderSuggestion = (docId: string) =>
|
||||
api.post(`/documents/${docId}/suggestions/folder/confirm`);
|
||||
|
||||
@@ -425,7 +541,10 @@ export const confirmFilenameSuggestion = (docId: string) =>
|
||||
export const rejectFilenameSuggestion = (docId: string) =>
|
||||
api.post(`/documents/${docId}/suggestions/filename/reject`);
|
||||
|
||||
// --- Plugins ---
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PluginOut {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -456,14 +575,13 @@ export interface PluginManifest {
|
||||
};
|
||||
}
|
||||
|
||||
export const getPlugins = () =>
|
||||
api.get<PluginOut[]>("/plugins").then((r) => r.data);
|
||||
export const getPlugins = () => api.get<PluginOut[]>("/plugins");
|
||||
|
||||
export const getPluginManifest = (id: string) =>
|
||||
api.get<PluginManifest>(`/plugins/${id}/manifest`).then((r) => r.data);
|
||||
api.get<PluginManifest>(`/plugins/${id}/manifest`);
|
||||
|
||||
export const getPluginSettings = (id: string) =>
|
||||
api.get<Record<string, unknown>>(`/plugins/${id}/settings`).then((r) => r.data);
|
||||
api.get<Record<string, unknown>>(`/plugins/${id}/settings`);
|
||||
|
||||
export const updatePluginSettings = (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<Record<string, unknown>>(`/plugins/${id}/settings`, data).then((r) => r.data);
|
||||
api.patch<Record<string, unknown>>(`/plugins/${id}/settings`, data);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { Briefcase, LayoutDashboard } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -22,12 +21,8 @@ export default function LoginPage() {
|
||||
try {
|
||||
await login(email, password);
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const detail = err.response?.data?.detail;
|
||||
setError(typeof detail === "string" ? detail : "Login failed.");
|
||||
} else {
|
||||
setError("Login failed.");
|
||||
}
|
||||
const detail = err instanceof Error ? err.message : "Login failed.";
|
||||
setError(detail);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user