You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
5.6 KiB
197 lines
5.6 KiB
import axios from 'axios'; |
|
|
|
const api = axios.create({ |
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api', |
|
withCredentials: true, |
|
headers: { 'Content-Type': 'application/json' } |
|
}); |
|
|
|
let isRefreshing = false; |
|
let failedQueue: any[] = []; |
|
|
|
const processQueue = (error: any, token: string | null = null) => { |
|
failedQueue.forEach(prom => { |
|
if (error) { |
|
prom.reject(error); |
|
} else { |
|
prom.resolve(token); |
|
} |
|
}); |
|
failedQueue = []; |
|
}; |
|
|
|
// Request interceptor: attach access token |
|
api.interceptors.request.use( |
|
config => { |
|
const token = localStorage.getItem('access_token'); |
|
if (token) { |
|
config.headers.Authorization = `Bearer ${token}`; |
|
} |
|
return config; |
|
}, |
|
error => Promise.reject(error) |
|
); |
|
|
|
// Response interceptor: handle token refresh |
|
api.interceptors.response.use( |
|
response => response.data, |
|
async error => { |
|
const originalRequest = error.config; |
|
|
|
// If 401 and not already retrying |
|
if (error.response?.status === 401 && !originalRequest._retry) { |
|
if (isRefreshing) { |
|
// Queue this request |
|
return new Promise((resolve, reject) => { |
|
failedQueue.push({ resolve, reject }); |
|
}).then(token => { |
|
originalRequest.headers.Authorization = `Bearer ${token}`; |
|
return api(originalRequest); |
|
}); |
|
} |
|
|
|
originalRequest._retry = true; |
|
isRefreshing = true; |
|
|
|
try { |
|
// Try to refresh token |
|
const response = await axios.post( |
|
`${api.defaults.baseURL}/auth/refresh`, |
|
{}, |
|
{ withCredentials: true } |
|
); |
|
|
|
const { accessToken } = response.data.data; |
|
localStorage.setItem('access_token', accessToken); |
|
|
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`; |
|
processQueue(null, accessToken); |
|
|
|
return api(originalRequest); |
|
} catch (refreshError) { |
|
processQueue(refreshError, null); |
|
localStorage.removeItem('access_token'); |
|
// Only redirect if we're not already on login page |
|
if (window.location.pathname !== '/login') { |
|
window.location.href = '/login'; |
|
} |
|
return Promise.reject(refreshError); |
|
} finally { |
|
isRefreshing = false; |
|
} |
|
} |
|
|
|
return Promise.reject(error.response?.data || error); |
|
} |
|
); |
|
|
|
// Auth API |
|
export const authApi = { |
|
login: (username: string, password: string) => |
|
api.post('/auth/login', { username, password }), |
|
|
|
logout: () => api.post('/auth/logout'), |
|
|
|
getCurrentUser: () => api.get('/auth/me'), |
|
|
|
refresh: () => api.post('/auth/refresh') |
|
}; |
|
|
|
// Channels API |
|
export const channelsApi = { |
|
getAll: () => api.get('/channels'), |
|
|
|
add: (channelInput: string) => |
|
api.post('/channels', { channelInput }), |
|
|
|
remove: (channelId: string) => |
|
api.delete(`/channels/${channelId}`), |
|
|
|
refresh: (channelId: string) => |
|
api.put(`/channels/${channelId}/refresh`) |
|
}; |
|
|
|
// Videos API |
|
export const videosApi = { |
|
getAll: (params?: any) => api.get('/videos', { params }), |
|
|
|
search: (query: string, params?: any) => |
|
api.get('/videos/search', { params: { q: query, ...params } }), |
|
|
|
refresh: (channelIds?: string[]) => |
|
api.post('/videos/refresh', { channelIds }) |
|
}; |
|
|
|
// Settings API |
|
export const settingsApi = { |
|
heartbeat: (sessionId: string, route: string, video?: { title: string; channelName: string }) => api.post('/settings/heartbeat', { sessionId, route, videoTitle: video?.title, videoChannel: video?.channelName }), |
|
|
|
getConnectionStats: () => api.get('/settings/connection-stats') |
|
}; |
|
|
|
// Word Groups API |
|
export const wordGroupsApi = { |
|
getAll: () => api.get('/word-groups'), |
|
|
|
create: (name: string) => |
|
api.post('/word-groups', { name }), |
|
|
|
update: (id: number, name: string) => |
|
api.put(`/word-groups/${id}`, { name }), |
|
|
|
delete: (id: number) => |
|
api.delete(`/word-groups/${id}`), |
|
|
|
addWord: (groupId: number, word: string) => |
|
api.post(`/word-groups/${groupId}/words`, { word }), |
|
|
|
deleteWord: (wordId: number) => |
|
api.delete(`/word-groups/words/${wordId}`) |
|
}; |
|
|
|
// Users API (admin only) |
|
export const usersApi = { |
|
getAll: () => api.get('/users'), |
|
|
|
create: (userData: { username: string; password: string; role?: 'admin' | 'user' }) => |
|
api.post('/users', userData), |
|
|
|
update: (id: number, userData: { username?: string; role?: 'admin' | 'user' }) => |
|
api.put(`/users/${id}`, userData), |
|
|
|
delete: (id: number) => |
|
api.delete(`/users/${id}`), |
|
|
|
changePassword: (id: number, password: string) => |
|
api.put(`/users/${id}/password`, { password }) |
|
}; |
|
|
|
// Settings Profiles API (admin only) |
|
export const settingsProfilesApi = { |
|
getAll: () => api.get('/settings-profiles'), |
|
|
|
getById: (id: number) => api.get(`/settings-profiles/${id}`), |
|
|
|
create: (data: { name: string; description?: string; enabledApps?: string[] }) => |
|
api.post('/settings-profiles', data), |
|
|
|
update: (id: number, data: { name?: string; description?: string; isActive?: boolean }) => |
|
api.put(`/settings-profiles/${id}`, data), |
|
|
|
delete: (id: number) => api.delete(`/settings-profiles/${id}`), |
|
|
|
updateSettings: (id: number, settings: { enabledApps?: string[] }) => |
|
api.put(`/settings-profiles/${id}/settings`, settings), |
|
|
|
regenerateCode: (id: number) => api.post(`/settings-profiles/${id}/regenerate-code`) |
|
}; |
|
|
|
// Magic Code API (public) |
|
export const magicCodeApi = { |
|
getSettingsByCode: (code: string) => api.get(`/magic-code/${code}`) |
|
}; |
|
|
|
// Speech Sounds API (admin only) |
|
export const speechSoundsApi = { |
|
clearPronunciationsCache: () => api.delete('/speech-sounds/cache') |
|
};
|
|
|