diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..3680d4f --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,57 @@ +import axios from 'axios' + +const api = axios.create({ + baseURL: '/api', + withCredentials: true, +}) + +api.interceptors.response.use( + (res) => res, + (err) => { + if (err.response?.status === 401 && window.location.pathname !== '/login') { + window.location.href = '/login' + } + return Promise.reject(err) + } +) + +export default api + +// --- Auth --- +export const authApi = { + login: (username: string, password: string) => + api.post('/auth/login', { username, password }), + logout: () => api.post('/auth/logout'), + me: () => api.get('/auth/me'), + register: (username: string, email: string, password: string) => + api.post('/auth/register', { username, email, password }), +} + +// --- Configs --- +export const configsApi = { + list: () => api.get('/configs'), + create: (data: object) => api.post('/configs', data), + get: (id: number) => api.get(`/configs/${id}`), + update: (id: number, data: object) => api.put(`/configs/${id}`, data), + delete: (id: number) => api.delete(`/configs/${id}`), + generate: (id: number, format: 'json' | 'zip' = 'json') => + api.post(`/configs/${id}/generate?format=${format}`, null, { + responseType: format === 'zip' ? 'blob' : 'json', + }), +} + +// --- Nested resources (zones, interfaces, policies, rules, masq) --- +const nestedApi = (resource: string) => ({ + list: (configId: number) => api.get(`/configs/${configId}/${resource}`), + create: (configId: number, data: object) => api.post(`/configs/${configId}/${resource}`, data), + update: (configId: number, id: number, data: object) => + api.put(`/configs/${configId}/${resource}/${id}`, data), + delete: (configId: number, id: number) => + api.delete(`/configs/${configId}/${resource}/${id}`), +}) + +export const zonesApi = nestedApi('zones') +export const interfacesApi = nestedApi('interfaces') +export const policiesApi = nestedApi('policies') +export const rulesApi = nestedApi('rules') +export const masqApi = nestedApi('masq') diff --git a/frontend/src/store/auth.ts b/frontend/src/store/auth.ts new file mode 100644 index 0000000..fdb4721 --- /dev/null +++ b/frontend/src/store/auth.ts @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react' +import { authApi } from '../api' + +export interface User { + id: number + username: string + email: string + is_active: boolean +} + +// Simple module-level state (no external lib needed) +let currentUser: User | null = null +const listeners = new Set<() => void>() + +export function useAuth() { + const [user, setUser] = useState(currentUser) + const [loading, setLoading] = useState(currentUser === null) + + useEffect(() => { + const update = () => setUser(currentUser) + listeners.add(update) + if (currentUser === null) { + authApi.me() + .then((res) => { currentUser = res.data; listeners.forEach((l) => l()) }) + .catch(() => { currentUser = null; listeners.forEach((l) => l()) }) + .finally(() => setLoading(false)) + } + return () => { listeners.delete(update) } + }, []) + + const logout = async () => { + await authApi.logout() + currentUser = null + listeners.forEach((l) => l()) + window.location.href = '/login' + } + + return { user, loading, logout } +}