- Added /users/auth-mode public endpoint to detect LDAP status - Updated frontend to call getAuthMode() instead of hardcoded false - Login page now automatically switches to enterprise mode when LDAP is enabled - Fixes missing LDAP user login UI that was previously disabled with TODO comment This restores the functionality that was commented out earlier where the login page would auto-detect whether to show local user list or enterprise username input.
304 lines
9.6 KiB
TypeScript
304 lines
9.6 KiB
TypeScript
import axios from 'axios';
|
|
import { getToken, clearAuth } from './auth';
|
|
|
|
// Cached config to avoid repeated fetches
|
|
let cachedConfig: any = null;
|
|
|
|
/**
|
|
* Fetches the network configuration from the public/network.json file.
|
|
* This file is generated at startup by start_server.sh or docker-compose.
|
|
*/
|
|
export const getNetworkConfig = async () => {
|
|
if (cachedConfig) return cachedConfig;
|
|
|
|
if (typeof window === 'undefined') {
|
|
return { SERVER_IP: 'localhost', BACKEND_PORT: 8000, BACKEND_SSL_PORT: 8908 };
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/network.json');
|
|
if (!response.ok) throw new Error("Config not found");
|
|
cachedConfig = await response.json();
|
|
return cachedConfig;
|
|
} catch (e) {
|
|
console.warn("Network config not found, using compiled defaults.");
|
|
// Use actual hostname from browser + default ports
|
|
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
|
|
return { SERVER_IP: hostname, BACKEND_PORT: 8916, BACKEND_SSL_PORT: 8918 };
|
|
}
|
|
};
|
|
|
|
export const getBackendUrl = async () => {
|
|
const config = await getNetworkConfig();
|
|
|
|
if (typeof window === 'undefined') return `http://localhost:${config.BACKEND_PORT}`;
|
|
|
|
// Use SERVER_IP from config (set during startup) instead of window.location.hostname
|
|
// This ensures VPN/remote clients connect to the correct server IP, not their access IP
|
|
const host = config.SERVER_IP || window.location.hostname;
|
|
|
|
// If we are on HTTPS (Proxy/Mobile mode), we use the SSL port for the backend
|
|
if (window.location.protocol === 'https:') {
|
|
if (host.includes('.loca.lt')) {
|
|
return 'https://inventory-ai-api.loca.lt';
|
|
}
|
|
return `https://${host}:${config.BACKEND_SSL_PORT}`;
|
|
}
|
|
|
|
return `http://${host}:${config.BACKEND_PORT}`;
|
|
};
|
|
|
|
export const buildPhotoUrl = (backendUrl: string, photoPath: string): string => {
|
|
if (!photoPath) return '';
|
|
return `${backendUrl}${photoPath}`;
|
|
};
|
|
|
|
/**
|
|
* [C-01] Axios instance cu JWT Bearer token în header
|
|
* și interceptor pentru 401 Unauthorized (token expired)
|
|
*/
|
|
export const axiosInstance = axios.create({});
|
|
|
|
axiosInstance.interceptors.request.use(async (config) => {
|
|
if (!config.baseURL) {
|
|
config.baseURL = await getBackendUrl(); // called at request time — always correct
|
|
}
|
|
const token = getToken();
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
}, (error) => Promise.reject(error));
|
|
|
|
axiosInstance.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
// [L-01] Handle 401 Unauthorized — token expired
|
|
if (error.response?.status === 401) {
|
|
clearAuth();
|
|
if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
export const inventoryApi = {
|
|
getAuthMode: async () => {
|
|
const baseUrl = await getBackendUrl();
|
|
const res = await axios.get(`${baseUrl}/users/auth-mode`);
|
|
return res.data;
|
|
},
|
|
|
|
getItems: async () => {
|
|
const res = await axiosInstance.get('/items/');
|
|
return res.data;
|
|
},
|
|
|
|
getStats: async () => {
|
|
const res = await axiosInstance.get('/items/stats');
|
|
return res.data;
|
|
},
|
|
|
|
syncBulkOperations: async (userId: number, operations: any[]) => {
|
|
try {
|
|
const res = await axiosInstance.post('/operations/bulk-sync', {
|
|
user_id: userId,
|
|
operations: operations
|
|
});
|
|
return res.data;
|
|
} catch (err: any) {
|
|
console.error("Sync API Error:", err);
|
|
if (err.response?.status === 404) {
|
|
throw new Error(`404: Endpoint not found`);
|
|
}
|
|
throw err;
|
|
}
|
|
},
|
|
|
|
analyzeLabel: async (formData: FormData, mode: string = "item") => {
|
|
const res = await axiosInstance.post(`/items/extract-label?mode=${mode}`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
});
|
|
return res.data;
|
|
},
|
|
|
|
createItem: async (userId: number, itemData: any) => {
|
|
const res = await axiosInstance.post('/items/', itemData);
|
|
return res.data;
|
|
},
|
|
|
|
updateItem: async (itemId: number, itemData: any) => {
|
|
const res = await axiosInstance.put(`/items/${itemId}`, itemData);
|
|
return res.data;
|
|
},
|
|
|
|
adjustStock: async (endpoint: string, data: any) => {
|
|
const res = await axiosInstance.post(`/operations/${endpoint}`, data);
|
|
return res.data;
|
|
},
|
|
|
|
deleteItem: async (itemId: number) => {
|
|
const res = await axiosInstance.delete(`/items/${itemId}`);
|
|
return res.data;
|
|
},
|
|
|
|
getAuditLogs: async (limit: number = 50) => {
|
|
const res = await axiosInstance.get('/operations/logs', { params: { limit } });
|
|
return res.data;
|
|
},
|
|
|
|
// Users
|
|
getUsers: async () => {
|
|
// [C-01] Public endpoint — use plain axios to avoid JWT interceptor
|
|
const baseUrl = await getBackendUrl();
|
|
const res = await axios.get(`${baseUrl}/users/`);
|
|
return res.data;
|
|
},
|
|
|
|
createUser: async (userData: any) => {
|
|
const res = await axiosInstance.post('/users/', userData);
|
|
return res.data;
|
|
},
|
|
|
|
login: async (credentials: any) => {
|
|
// [C-01] Login endpoint — NU adaug token header (login e public)
|
|
const baseUrl = await getBackendUrl();
|
|
const res = await axios.post(`${baseUrl}/users/login`, credentials);
|
|
return res.data;
|
|
},
|
|
|
|
deleteUser: async (userId: number) => {
|
|
const res = await axiosInstance.delete(`/users/${userId}`);
|
|
return res.data;
|
|
},
|
|
|
|
updateUser: async (userId: number, data: any) => {
|
|
const res = await axiosInstance.put(`/users/${userId}`, data);
|
|
return res.data;
|
|
},
|
|
|
|
getLdapConfig: async () => {
|
|
const res = await axiosInstance.get('/users/ldap-config');
|
|
return res.data;
|
|
},
|
|
updateLdapConfig: async (config: any) => {
|
|
const res = await axiosInstance.post('/users/ldap-config', config);
|
|
return res.data;
|
|
},
|
|
testLdapConnection: async (config: any) => {
|
|
const res = await axiosInstance.post('/users/test-ldap', config);
|
|
return res.data;
|
|
},
|
|
|
|
// Categories
|
|
getCategories: async () => {
|
|
const res = await axiosInstance.get('/categories/');
|
|
return res.data;
|
|
},
|
|
createCategory: async (data: any) => {
|
|
const res = await axiosInstance.post('/categories/', data);
|
|
return res.data;
|
|
},
|
|
updateCategory: async (id: number, data: any) => {
|
|
const res = await axiosInstance.put(`/categories/${id}`, data);
|
|
return res.data;
|
|
},
|
|
deleteCategory: async (id: number) => {
|
|
const res = await axiosInstance.delete(`/categories/${id}`);
|
|
return res.data;
|
|
},
|
|
|
|
// Database Management
|
|
getDbBackups: async () => {
|
|
const res = await axiosInstance.get('/admin/db/backups');
|
|
return res.data;
|
|
},
|
|
getDbStats: async () => {
|
|
const res = await axiosInstance.get('/admin/db/stats');
|
|
return res.data;
|
|
},
|
|
triggerBackup: async () => {
|
|
const res = await axiosInstance.post('/admin/db/backup');
|
|
return res.data;
|
|
},
|
|
restoreDatabase: async (filename: string) => {
|
|
const res = await axiosInstance.post('/admin/db/restore', { filename, confirm: true });
|
|
return res.data;
|
|
},
|
|
getDbSettings: async () => {
|
|
const res = await axiosInstance.get('/admin/db/settings');
|
|
return res.data;
|
|
},
|
|
updateDbSettings: async (settings: any) => {
|
|
const res = await axiosInstance.patch('/admin/db/settings', settings);
|
|
return res.data;
|
|
},
|
|
exportDb: async () => {
|
|
const res = await axiosInstance.get('/admin/db/export', { responseType: 'blob' });
|
|
return res.data;
|
|
},
|
|
importDb: async (formData: FormData) => {
|
|
const res = await axiosInstance.post('/admin/db/import', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
});
|
|
return res.data;
|
|
},
|
|
// System Settings
|
|
getSystemSettings: async () => {
|
|
// Note: Reusing the existing settings router pattern but adding AI Prompt
|
|
const res = await axiosInstance.get('/admin/db/settings');
|
|
// We need another endpoint for general settings or expand the DB one
|
|
// For now, I'll add a specific fetch for the prompt
|
|
const promptRes = await axiosInstance.get('/admin/ai/settings/prompt');
|
|
return { ...res.data, ai_extraction_prompt: promptRes.data.value };
|
|
},
|
|
getAiPrompt: async () => {
|
|
const res = await axiosInstance.get('/admin/ai/settings/prompt');
|
|
return res.data;
|
|
},
|
|
updateAiPrompt: async (prompt: string) => {
|
|
const res = await axiosInstance.post('/admin/ai/settings/prompt', { value: prompt });
|
|
return res.data;
|
|
},
|
|
getAiConfig: async () => {
|
|
const res = await axiosInstance.get('/admin/ai/settings');
|
|
return res.data;
|
|
},
|
|
updateAiProvider: async (provider: string) => {
|
|
const res = await axiosInstance.post('/admin/ai/settings', { provider });
|
|
return res.data;
|
|
},
|
|
updateAiKeys: async (keys: { gemini_api_key?: string, claude_api_key?: string }) => {
|
|
const res = await axiosInstance.post('/admin/ai/settings/keys', keys);
|
|
return res.data;
|
|
},
|
|
testAiKey: async (provider: string, key: string) => {
|
|
const res = await axiosInstance.post('/admin/ai/settings/test-key', { provider, key });
|
|
return res.data;
|
|
},
|
|
|
|
// Photo Upload
|
|
uploadItemPhoto: async (itemId: number, formData: FormData) => {
|
|
const res = await axiosInstance.post(`/items/${itemId}/photo`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
});
|
|
return res.data;
|
|
},
|
|
|
|
// Photo Replacement
|
|
replaceItemPhoto: async (itemId: number, formData: FormData) => {
|
|
const res = await axiosInstance.put(`/items/${itemId}/photo`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
});
|
|
return res.data;
|
|
},
|
|
|
|
// Photo Deletion
|
|
deleteItemPhoto: async (itemId: number) => {
|
|
const res = await axiosInstance.delete(`/items/${itemId}/photo`);
|
|
return res.data;
|
|
},
|
|
};
|