mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
feat: merge conflict resolution, site management, UI fixes
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
// API 配置 API 模块
|
||||
import { api } from './client'
|
||||
import type { ApiConfig, ApiProvider } from '../types'
|
||||
|
||||
// 获取所有 API 配置
|
||||
export async function listApiConfigs(): Promise<ApiConfig[]> {
|
||||
const resp = await api.get<ApiConfig[]>('/api-config')
|
||||
return resp.data as unknown as ApiConfig[]
|
||||
}
|
||||
|
||||
// 获取提供者列表
|
||||
export async function getProviders(): Promise<ApiProvider[]> {
|
||||
const resp = await api.get<ApiProvider[]>('/api-config/providers/list')
|
||||
return resp.data as unknown as ApiProvider[]
|
||||
}
|
||||
|
||||
// 获取指定提供者的配置
|
||||
export async function getApiConfig(provider: string): Promise<ApiConfig> {
|
||||
const resp = await api.get<ApiConfig>(`/api-config/${provider}`)
|
||||
return resp.data as unknown as ApiConfig
|
||||
}
|
||||
|
||||
// 获取生效的配置
|
||||
export async function getEffectiveConfig(provider: string): Promise<ApiConfig> {
|
||||
const resp = await api.get<ApiConfig>(`/api-config/${provider}/effective`)
|
||||
return resp.data as unknown as ApiConfig
|
||||
}
|
||||
|
||||
// 更新 API 配置
|
||||
export interface UpdateApiConfigRequest {
|
||||
api_key?: string
|
||||
base_url?: string
|
||||
extra?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export async function updateApiConfig(
|
||||
provider: string,
|
||||
data: UpdateApiConfigRequest
|
||||
): Promise<ApiConfig> {
|
||||
const resp = await api.post<ApiConfig>(`/api-config/${provider}`, data)
|
||||
return resp.data as unknown as ApiConfig
|
||||
}
|
||||
|
||||
// 删除 API 配置
|
||||
export async function deleteApiConfig(provider: string): Promise<void> {
|
||||
await api.delete(`/api-config/${provider}`)
|
||||
}
|
||||
|
||||
// 测试 API 连接
|
||||
export interface TestApiConfigResponse {
|
||||
result: 'success' | 'error' | 'invalid' | 'unknown'
|
||||
}
|
||||
|
||||
export async function testApiConfig(provider: string): Promise<TestApiConfigResponse> {
|
||||
const resp = await api.post<TestApiConfigResponse>(`/api-config/${provider}/test`)
|
||||
return resp.data as unknown as TestApiConfigResponse
|
||||
}
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { api } from './client'
|
||||
import type { User } from '../types'
|
||||
import type { User, TokenPair } from '../types'
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: User
|
||||
tokens: TokenPair
|
||||
}
|
||||
|
||||
export const authAPI = {
|
||||
@@ -11,7 +11,7 @@ export const authAPI = {
|
||||
api.post<LoginResponse>('/auth/login', { username, password }).then((r) => r.data),
|
||||
|
||||
register: (username: string, password: string) =>
|
||||
api.post<User>('/auth/register', { username, password }).then((r) => r.data),
|
||||
api.post<LoginResponse>('/auth/register', { username, password }).then((r) => r.data),
|
||||
|
||||
me: () => api.get<User>('/me').then((r) => r.data),
|
||||
|
||||
|
||||
+68
-3
@@ -1,4 +1,4 @@
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import axios, { AxiosError, type InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
@@ -9,6 +9,22 @@ export const api = axios.create({
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
// Flag to prevent multiple simultaneous refresh attempts
|
||||
let isRefreshing = false
|
||||
let refreshSubscribers: Array<(token: string) => void> = []
|
||||
|
||||
// Subscribe to token refresh
|
||||
function subscribeTokenRefresh(callback: (token: string) => void) {
|
||||
refreshSubscribers.push(callback)
|
||||
}
|
||||
|
||||
// Notify all subscribers about new token
|
||||
function onTokenRefreshed(newToken: string) {
|
||||
refreshSubscribers.forEach(callback => callback(newToken))
|
||||
refreshSubscribers = []
|
||||
}
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().token
|
||||
if (token) {
|
||||
@@ -18,15 +34,54 @@ api.interceptors.request.use((config) => {
|
||||
return config
|
||||
})
|
||||
|
||||
// Handle 401 errors with token refresh
|
||||
api.interceptors.response.use(
|
||||
(resp) => resp,
|
||||
(err: AxiosError) => {
|
||||
if (err.response?.status === 401) {
|
||||
async (err: AxiosError) => {
|
||||
const originalRequest = err.config as InternalAxiosRequestConfig & { _retry?: boolean }
|
||||
|
||||
// If 401 and not already retried
|
||||
if (err.response?.status === 401 && originalRequest && !originalRequest._retry) {
|
||||
if (isRefreshing) {
|
||||
// Wait for token refresh to complete
|
||||
return new Promise((resolve) => {
|
||||
subscribeTokenRefresh((token: string) => {
|
||||
if (originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
resolve(api(originalRequest))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
const refreshed = await useAuthStore.getState().tokenRefresh()
|
||||
if (refreshed) {
|
||||
const newToken = useAuthStore.getState().token
|
||||
if (newToken && originalRequest.headers) {
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
}
|
||||
onTokenRefreshed(newToken || '')
|
||||
isRefreshing = false
|
||||
return api(originalRequest)
|
||||
}
|
||||
} catch (refreshError) {
|
||||
isRefreshing = false
|
||||
refreshSubscribers = []
|
||||
}
|
||||
|
||||
// Refresh failed, logout
|
||||
useAuthStore.getState().logout()
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
// For other errors, just reject
|
||||
return Promise.reject(err)
|
||||
},
|
||||
)
|
||||
@@ -55,3 +110,13 @@ export function imageURL(remote?: string): string {
|
||||
if (remote.startsWith('/api/img')) return remote
|
||||
return `/api/img?url=${encodeURIComponent(remote)}&${tokenQuery()}`
|
||||
}
|
||||
|
||||
// getToken returns the current auth token
|
||||
export function getToken(): string | null {
|
||||
return useAuthStore.getState().token
|
||||
}
|
||||
|
||||
// getRefreshToken returns the current refresh token
|
||||
export function getRefreshToken(): string | null {
|
||||
return useAuthStore.getState().refreshToken
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { api } from './client'
|
||||
import type { DownloadClient } from '../types'
|
||||
|
||||
export interface DownloadClientCreateParams {
|
||||
name: string
|
||||
type: 'qbittorrent' | 'transmission' | 'aria2'
|
||||
host: string
|
||||
username?: string
|
||||
password?: string
|
||||
is_default?: boolean
|
||||
extra?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface DownloadClientUpdateParams {
|
||||
name?: string
|
||||
type?: 'qbittorrent' | 'transmission' | 'aria2'
|
||||
host?: string
|
||||
username?: string
|
||||
password?: string
|
||||
is_default?: boolean
|
||||
enabled?: boolean
|
||||
extra?: Record<string, string>
|
||||
}
|
||||
|
||||
export const downloadClientAPI = {
|
||||
list: () =>
|
||||
api
|
||||
.get<{ code: number; data: DownloadClient[] }>('/download-clients')
|
||||
.then((r) => r.data.data),
|
||||
|
||||
get: (id: string) =>
|
||||
api
|
||||
.get<{ code: number; data: DownloadClient }>(`/download-clients/${id}`)
|
||||
.then((r) => r.data.data),
|
||||
|
||||
create: (params: DownloadClientCreateParams) =>
|
||||
api
|
||||
.post<{ code: number; data: DownloadClient }>('/download-clients', params)
|
||||
.then((r) => r.data.data),
|
||||
|
||||
update: (id: string, params: DownloadClientUpdateParams) =>
|
||||
api
|
||||
.put<{ code: number; data: DownloadClient }>(`/download-clients/${id}`, params)
|
||||
.then((r) => r.data.data),
|
||||
|
||||
delete: (id: string) =>
|
||||
api.delete(`/download-clients/${id}`).then((r) => r.data),
|
||||
|
||||
test: (id: string) =>
|
||||
api.post(`/download-clients/${id}/test`).then((r) => r.data),
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { api } from './client'
|
||||
import type { NotifyChannel, NotifyProviderInfo } from '../types'
|
||||
|
||||
export interface NotifyChannelCreateParams {
|
||||
name: string
|
||||
type: 'telegram' | 'wechat' | 'bark' | 'webhook' | 'email'
|
||||
enabled?: boolean
|
||||
config: Record<string, string>
|
||||
events?: string[]
|
||||
}
|
||||
|
||||
export interface NotifyChannelUpdateParams {
|
||||
name?: string
|
||||
enabled?: boolean
|
||||
config?: Record<string, string>
|
||||
events?: string[]
|
||||
}
|
||||
|
||||
export const notifyAPI = {
|
||||
list: () =>
|
||||
api
|
||||
.get<{ code: number; data: NotifyChannel[] }>('/notify-channels')
|
||||
.then((r) => r.data.data),
|
||||
|
||||
get: (id: string) =>
|
||||
api
|
||||
.get<{ code: number; data: NotifyChannel }>(`/notify-channels/${id}`)
|
||||
.then((r) => r.data.data),
|
||||
|
||||
getTypes: () =>
|
||||
api
|
||||
.get<{ code: number; data: NotifyProviderInfo[] }>('/notify-channels/types')
|
||||
.then((r) => r.data.data),
|
||||
|
||||
create: (params: NotifyChannelCreateParams) =>
|
||||
api
|
||||
.post<{ code: number; data: NotifyChannel }>('/notify-channels', params)
|
||||
.then((r) => r.data.data),
|
||||
|
||||
update: (id: string, params: NotifyChannelUpdateParams) =>
|
||||
api
|
||||
.put<{ code: number; data: NotifyChannel }>(`/notify-channels/${id}`, params)
|
||||
.then((r) => r.data.data),
|
||||
|
||||
delete: (id: string) =>
|
||||
api.delete(`/notify-channels/${id}`).then((r) => r.data),
|
||||
|
||||
test: (id: string) =>
|
||||
api.post(`/notify-channels/${id}/test`).then((r) => r.data),
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 权限 API 模块
|
||||
import { api } from './client'
|
||||
import type { UserPermission } from '../types'
|
||||
|
||||
// 获取用户权限
|
||||
export async function getUserPermissions(userId: string): Promise<UserPermission> {
|
||||
const resp = await api.get<UserPermission>(`/admin/users/${userId}/permissions`)
|
||||
return resp.data as unknown as UserPermission
|
||||
}
|
||||
|
||||
// 更新用户权限
|
||||
export async function updateUserPermissions(
|
||||
userId: string,
|
||||
permissions: Record<string, boolean>
|
||||
): Promise<void> {
|
||||
await api.put(`/admin/users/${userId}/permissions`, { permissions })
|
||||
}
|
||||
|
||||
// 重置用户权限为默认值
|
||||
export async function resetUserPermissions(userId: string): Promise<void> {
|
||||
await api.post(`/admin/users/${userId}/permissions/reset`)
|
||||
}
|
||||
|
||||
// 获取当前用户权限
|
||||
export async function getMyPermissions(): Promise<{
|
||||
permissions: Record<string, boolean>
|
||||
role: string
|
||||
tier: string
|
||||
is_super: boolean
|
||||
}> {
|
||||
const resp = await api.get('/auth/permissions')
|
||||
return resp.data as unknown as {
|
||||
permissions: Record<string, boolean>
|
||||
role: string
|
||||
tier: string
|
||||
is_super: boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 令牌刷新 API 模块
|
||||
import { api } from './client'
|
||||
|
||||
// 刷新令牌请求/响应
|
||||
export interface RefreshTokenRequest {
|
||||
refresh_token: string
|
||||
}
|
||||
|
||||
export interface RefreshTokenResponse {
|
||||
token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
// 刷新访问令牌
|
||||
export async function refreshToken(refreshToken: string): Promise<RefreshTokenResponse> {
|
||||
const resp = await api.post<RefreshTokenResponse>('/auth/refresh', {
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
return resp.data as unknown as RefreshTokenResponse
|
||||
}
|
||||
|
||||
// 登出
|
||||
export async function logout(): Promise<void> {
|
||||
await api.post('/me/logout')
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { api } from './client'
|
||||
|
||||
// Site management API - matches Go backend /api/sites endpoints
|
||||
export const sitesAPI = {
|
||||
// List all sites
|
||||
list: () => api.get('/sites').then((r) => r.data),
|
||||
|
||||
// Get single site with decrypted fields
|
||||
get: (id: string | number) => api.get(`/sites/${id}`).then((r) => r.data),
|
||||
|
||||
// Create a new site
|
||||
create: (data: Record<string, unknown>) =>
|
||||
api.post('/sites', data).then((r) => r.data),
|
||||
|
||||
// Update existing site
|
||||
update: (id: string | number, data: Record<string, unknown>) =>
|
||||
api.put(`/sites/${id}`, data).then((r) => r.data),
|
||||
|
||||
// Delete a site
|
||||
remove: (id: string | number) =>
|
||||
api.delete(`/sites/${id}`).then((r) => r.data),
|
||||
|
||||
// Test site connectivity
|
||||
test: (id: string | number) =>
|
||||
api.post(`/sites/${id}/test`).then((r) => r.data),
|
||||
|
||||
// Get supported site types
|
||||
types: () => api.get('/sites/types').then((r) => r.data),
|
||||
|
||||
// Get supported auth types
|
||||
authTypes: () => api.get('/sites/auth-types').then((r) => r.data),
|
||||
}
|
||||
Reference in New Issue
Block a user