refactor(repo): consolidate openflare-server to root and move subprojects to internal/apps

- Merge all files inside openflare-server to the repository root directory.
- Relocate agent, relay, and flared subprojects from internal/ to internal/apps/.
- Combine docker-compose files and update build context paths to root.
- Update GitHub workflows and Dockerfiles to refer to new directories and package names.
- Rewrite Go package imports across all files.
- Resolve database renew test race condition and clean up docs.
This commit is contained in:
ryan
2026-06-19 14:23:29 +08:00
parent 19d476ed7f
commit 63cd906cfc
1064 changed files with 366 additions and 1397 deletions
@@ -0,0 +1,26 @@
import {BaseService} from '@/lib/services/core';
import type {AuthSource, AuthSourceRequest, ToggleAuthSourceRequest} from './types';
export class AdminAuthSourceService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listAuthSources(): Promise<AuthSource[]> {
return this.get<AuthSource[]>('/auth-sources');
}
static async createAuthSource(request: AuthSourceRequest): Promise<AuthSource> {
return this.post<AuthSource>('/auth-sources', request);
}
static async updateAuthSource(id: string, request: AuthSourceRequest): Promise<AuthSource> {
return this.put<AuthSource>(`/auth-sources/${id}`, request);
}
static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise<void> {
return this.put<void>(`/auth-sources/${id}/toggle`, request);
}
static async deleteAuthSource(id: string): Promise<void> {
return this.delete<void>(`/auth-sources/${id}`);
}
}
@@ -0,0 +1,18 @@
import {BaseService} from '@/lib/services/core';
import type {CacheConfig, CacheStatus} from './types';
export class AdminCacheService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getCacheStatus(): Promise<CacheStatus> {
return this.get<CacheStatus>('/cache/status');
}
static async updateCacheConfig(config: CacheConfig): Promise<void> {
return this.post<void>('/cache/config', config);
}
static async clearCache(): Promise<void> {
return this.post<void>('/cache/clear');
}
}
+43
View File
@@ -0,0 +1,43 @@
export { AdminSystemConfigService } from './system-config.service';
export { AdminAuthSourceService } from './auth-source.service';
export { AdminTaskService } from './task.service';
export { AdminUserService } from './user.service';
export { AdminStatusService } from './status.service';
export { AdminLogService } from './log.service';
export { AdminTemplateService } from './template.service';
export { AdminCacheService } from './cache.service';
export type {
SystemConfig,
CreateSystemConfigRequest,
CreateUserRequest,
UpdateSystemConfigRequest,
AuthSource,
AuthSourceRequest,
ToggleAuthSourceRequest,
TaskMeta,
TaskParam,
TaskParamType,
TaskExecution,
TaskExecutionStatus,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
DispatchTaskRequest,
AdminUser,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
SystemStatus,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
Template,
CreateTemplateRequest,
UpdateTemplateRequest,
StorageDriver,
StorageConfig,
ObjectStorageConfig,
} from './types';
@@ -0,0 +1,53 @@
import {BaseService} from '@/lib/services/core';
export class AdminLogService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getLogs(cursor: number = 0, limit: number = 200): Promise<{
lines: Array<{ index: number; data: string }>;
has_more: boolean;
next_cursor: number;
}> {
return this.get('/logs', { cursor, limit });
}
static async getAccessLogs(params: {
page: number;
page_size: number;
username?: string;
path?: string;
start_time?: string;
end_time?: string;
}): Promise<{
total: number;
list: Array<{
id: string;
user_id: string;
username: string;
nickname: string;
path: string;
method: string;
ip: string;
user_agent: string;
headers: string;
status: number;
latency: number;
created_at: string;
}>;
}> {
return this.get('/logs/access', params as Record<string, unknown>);
}
static async getLogsAnalytics(): Promise<{
trend: Array<{ date: string; count: number }>;
browsers: Array<{ browser: string; count: number }>;
top_users: Array<{
user_id: string;
username: string;
nickname: string;
count: number;
}>;
}> {
return this.get('/logs/analytics');
}
}
@@ -0,0 +1,18 @@
import {BaseService} from '@/lib/services/core';
import type {AppUpdateStatus, SystemStatus} from './types';
export class AdminStatusService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getSystemStatus(): Promise<SystemStatus> {
return this.get<SystemStatus>('/status');
}
static async getUpdateStatus(): Promise<AppUpdateStatus> {
return this.get<AppUpdateStatus>('/update');
}
static async applyUpdate(): Promise<void> {
return this.post<void>('/update/apply');
}
}
@@ -0,0 +1,40 @@
import {BaseService} from '@/lib/services/core';
import type {CreateSystemConfigRequest, SystemConfig, UpdateSystemConfigRequest,} from './types';
export class AdminSystemConfigService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async createSystemConfig(request: CreateSystemConfigRequest): Promise<void> {
return this.post<void>('/system-configs', request);
}
static async listSystemConfigs(type?: 'system' | 'business'): Promise<SystemConfig[]> {
const query = type ? `?type=${type}` : '';
return this.get<SystemConfig[]>(`/system-configs${query}`);
}
static async getSystemConfig(key: string): Promise<SystemConfig> {
return this.get<SystemConfig>(`/system-configs/${key}`);
}
static async updateSystemConfig(key: string, request: UpdateSystemConfigRequest): Promise<void> {
return this.put<void>(`/system-configs/${key}`, request);
}
static async testSMTP(request: {
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
to: string;
}): Promise<{ success: boolean; log: string; error: string }> {
return this.post<{ success: boolean; log: string; error: string }>(
'/system-configs/smtp/test',
request,
);
}
static async listUploadTypes(): Promise<string[]> {
return this.get<string[]>('/uploads/types');
}
}
@@ -0,0 +1,75 @@
import {BaseService} from '@/lib/services/core';
import type {
CreateScheduleRequest,
DispatchTaskRequest,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
Schedule,
TaskExecution,
TaskMeta,
TaskParamType,
TaskTypeResponse,
UpdateScheduleRequest,
} from './types';
export class AdminTaskService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getTaskTypes(): Promise<TaskMeta[]> {
const response = await this.get<TaskTypeResponse[]>('/tasks/types');
return response.map((item) => ({
type: item.Type || item.type || '',
asynq_task: item.AsynqTask || item.asynq_task || '',
name: item.Name || item.name || '',
description: item.Description || item.description || '',
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map((p) => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
type: (p.Type || p.type || 'string') as TaskParamType,
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
static async dispatchTask(request: DispatchTaskRequest): Promise<string> {
return this.post<string>('/tasks/dispatch', request);
}
static async listTaskExecutions(
request: ListTaskExecutionsRequest = {},
): Promise<ListTaskExecutionsResponse> {
return this.get<ListTaskExecutionsResponse>(
'/tasks/executions',
request as unknown as Record<string, unknown>,
);
}
static async getTaskExecution(id: string): Promise<TaskExecution> {
return this.get<TaskExecution>(`/tasks/executions/${id}`);
}
static async retryTaskExecution(id: string): Promise<string> {
return this.post<string>(`/tasks/executions/${id}/retry`);
}
static async listSchedules(): Promise<Schedule[]> {
return this.get<Schedule[]>('/tasks/schedules');
}
static async createSchedule(request: CreateScheduleRequest): Promise<Schedule> {
return this.post<Schedule>('/tasks/schedules', request);
}
static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise<Schedule> {
return this.put<Schedule>(`/tasks/schedules/${id}`, request);
}
static async deleteSchedule(id: string): Promise<void> {
return this.delete<void>(`/tasks/schedules/${id}`);
}
}
@@ -0,0 +1,26 @@
import {BaseService} from '@/lib/services/core';
import type {CreateTemplateRequest, Template, UpdateTemplateRequest} from './types';
export class AdminTemplateService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listTemplates(): Promise<Template[]> {
return this.get<Template[]>('/templates');
}
static async getTemplate(key: string): Promise<Template> {
return this.get<Template>(`/templates/${key}`);
}
static async createTemplate(request: CreateTemplateRequest): Promise<Template> {
return this.post<Template>('/templates', request);
}
static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise<Template> {
return this.put<Template>(`/templates/${key}`, request);
}
static async deleteTemplate(key: string): Promise<void> {
return this.delete<void>(`/templates/${key}`);
}
}
+508
View File
@@ -0,0 +1,508 @@
/**
* 系统配置信息
*/
export interface SystemConfig {
/** 配置键 */
key: string;
/** 配置值 */
value: string;
/** 配置类型:'system' | 'business' */
type: 'system' | 'business';
/** 是否对公共配置接口可见:0 不可见,1 可见 */
visibility: 0 | 1;
/** 配置描述 */
description: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/**
* 创建系统配置请求参数
*/
export interface CreateSystemConfigRequest {
/** 配置键(最大64字符) */
key: string;
/** 配置值 */
value: string;
/** 配置类型:'system' | 'business' */
type: 'system' | 'business';
/** 是否对公共配置接口可见:0 不可见,1 可见 */
visibility?: 0 | 1;
/** 配置描述(最大255字符,可选) */
description?: string;
}
/**
* 更新系统配置请求参数
*/
export interface UpdateSystemConfigRequest {
/** 配置值 */
value: string;
/** 是否对公共配置接口可见:0 不可见,1 可见 */
visibility?: 0 | 1;
/** 配置描述(最大255字符,可选) */
description?: string;
}
export type StorageDriver = 'local' | 's3' | 'r2' | 'minio' | 'oss' | 'webdav';
export interface ObjectStorageConfig {
endpoint: string;
region: string;
bucket: string;
access_key_id: string;
secret_access_key: string;
account_id?: string;
path_style: boolean;
key_prefix: string;
cdn_url: string;
}
export interface StorageConfig {
driver: StorageDriver;
local: { root: string };
s3: ObjectStorageConfig;
r2: ObjectStorageConfig;
minio: ObjectStorageConfig;
oss: ObjectStorageConfig;
webdav: {
endpoint: string;
username: string;
password: string;
base_path: string;
};
}
// ==================== 任务管理 ====================
/**
* 任务参数响应
*/
export interface TaskParamResponse {
Name?: string;
name?: string;
Label?: string;
label?: string;
Type?: string;
type?: string;
Required?: boolean;
required?: boolean;
Placeholder?: string;
placeholder?: string;
Description?: string;
description?: string;
}
/**
* 任务类型响应
*/
export interface TaskTypeResponse {
Type?: string;
type?: string;
AsynqTask?: string;
asynq_task?: string;
Name?: string;
name?: string;
Description?: string;
description?: string;
SupportsTime?: boolean;
supports_time?: boolean;
MaxRetry?: number;
max_retry?: number;
Queue?: string;
queue?: string;
Params?: TaskParamResponse[];
params?: TaskParamResponse[];
}
/**
* 任务参数的数据类型
* - string → 单行文本输入,JSON 中序列化为 string
* - text → 多行文本输入,JSON 中序列化为 string
* - number → 数字输入,JSON 中序列化为 number(而非 string)
* - boolean → 开关输入,JSON 中序列化为 boolean(而非 string)
*/
export type TaskParamType = 'string' | 'text' | 'number' | 'boolean';
/**
* 任务参数定义
*/
export interface TaskParam {
name: string;
label: string;
type: TaskParamType;
required: boolean;
placeholder: string;
description: string;
}
/**
* 任务元数据
*/
export interface TaskMeta {
/** 任务类型标识 */
type: string;
/** Asynq 任务名称 */
asynq_task: string;
/** 任务名称 */
name: string;
/** 任务描述 */
description: string;
/** 是否支持时间范围参数 */
supports_time: boolean;
/** 最大重试次数 */
max_retry: number;
/** 队列名称 */
queue: string;
/** 任务所需的自定义参数定义 */
params?: TaskParam[];
}
/**
* 下发任务请求参数
*/
export interface DispatchTaskRequest {
/** 任务类型 */
task_type: string;
/** 开始时间(可选,仅部分任务支持) */
start_time?: string;
/** 结束时间(可选,仅部分任务支持) */
end_time?: string;
/** 用户 ID(可选,仅部分任务需要) */
user_id?: string;
/** 任务自定义参数 JSON 字符串 */
payload?: string;
}
export type TaskExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed';
/**
* 任务执行记录
*/
export interface TaskExecution {
id: string;
task_id: string;
task_type: string;
task_name: string;
status: TaskExecutionStatus;
retryable: boolean;
max_retry: number;
retry_count: number;
log: string;
error_message: string;
result: string;
started_at?: string | null;
finished_at?: string | null;
duration: number;
payload: string;
triggered_by: string;
created_at: string;
updated_at: string;
}
/**
* 查询任务执行记录请求参数
*/
export interface ListTaskExecutionsRequest {
status?: TaskExecutionStatus;
task_type?: string;
page?: number;
page_size?: number;
}
/**
* 查询任务执行记录响应
*/
export interface ListTaskExecutionsResponse {
items: TaskExecution[];
total: number;
page: number;
page_size: number;
}
/**
* 定时任务配置信息
*/
export interface Schedule {
id: string;
name: string;
task_type: string;
cron: string;
payload: string;
is_active: boolean;
created_at: string;
updated_at: string;
}
/**
* 创建定时任务请求参数
*/
export interface CreateScheduleRequest {
name: string;
task_type: string;
cron: string;
payload: string;
is_active: boolean;
}
/**
* 修改定时任务请求参数
*/
export interface UpdateScheduleRequest {
name: string;
task_type: string;
cron: string;
payload: string;
is_active: boolean;
}
// ==================== 用户管理 ====================
/**
* 管理员用户信息
*/
export interface AdminUser {
/** 用户 ID */
id: string;
/** 用户名 */
username: string;
/** 昵称 */
nickname: string;
/** 邮箱 */
email?: string;
/** 头像 URL */
avatar_url: string;
/** 是否激活 */
is_active: boolean;
/** 是否管理员 */
is_admin: boolean;
/** 个人简介 */
bio?: string;
/** 手机号 */
phone?: string;
/** 性别 */
gender?: string;
/** 个人网站 */
website?: string;
/** 所在地 */
location?: string;
/** 最后登录时间 */
last_login_at: string;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/**
* 用户列表查询请求参数
*/
export interface ListUsersRequest {
/** 页码(从 1 开始) */
page: number;
/** 每页数量(1-100) */
page_size: number;
/** 用户 ID 精确过滤(可选) */
user_id?: string;
/** 用户名前缀过滤(可选) */
username?: string;
}
/**
* 用户列表响应
*/
export interface ListUsersResponse {
/** 用户列表 */
users: AdminUser[];
/** 总数 */
total: number;
}
/**
* 更新用户状态请求参数
*/
export interface UpdateUserStatusRequest {
/** 是否激活 */
is_active: boolean;
}
/**
* 创建用户请求参数
*/
export interface CreateUserRequest {
/** 用户名 */
username: string;
/** 密码 */
password: string;
/** 昵称 */
nickname?: string;
/** 邮箱 */
email: string;
/** 是否激活 */
is_active?: boolean;
/** 是否管理员 */
is_admin?: boolean;
}
/**
* 认证源信息
*/
export interface AuthSource {
id: string;
name: string;
type: 'oidc';
display_name: string;
is_active: boolean;
client_id: string;
client_secret?: string;
client_secret_configured?: boolean;
openid_discovery_url: string;
scopes: string;
icon_url: string;
created_at: string;
updated_at: string;
}
export interface AuthSourceRequest {
name: string;
type: 'oidc';
display_name: string;
is_active: boolean;
client_id: string;
client_secret: string;
openid_discovery_url: string;
scopes: string;
icon_url: string;
}
export interface ToggleAuthSourceRequest {
is_active: boolean;
}
/**
* 系统状态信息
*/
export interface SystemStatus {
uptime: string;
num_goroutine: number;
alloc: string;
total_alloc: string;
sys: string;
lookups: number;
mallocs: number;
frees: number;
heap_alloc: string;
heap_sys: string;
heap_idle: string;
heap_inuse: string;
heap_released: string;
heap_objects: number;
stack_inuse: string;
stack_sys: string;
mspan_inuse: string;
mspan_sys: string;
mcache_inuse: string;
mcache_sys: string;
buck_hash_sys: string;
gc_sys: string;
other_sys: string;
next_gc: string;
last_gc_time: string;
pause_total_ns: string;
last_pause: string;
num_gc: number;
}
/**
* 模板配置信息
*/
export interface Template {
id: string;
key: string;
name: string;
type: string;
subject: string;
content: string;
description: string;
is_system: boolean;
created_at: string;
updated_at: string;
}
/**
* 创建模板请求参数
*/
export interface CreateTemplateRequest {
key: string;
name: string;
type: string;
subject: string;
content: string;
description: string;
}
/**
* 更新模板请求参数
*/
export interface UpdateTemplateRequest {
name: string;
type: string;
subject: string;
content: string;
description: string;
}
/**
* 应用更新状态
*/
export interface AppUpdateStatus {
current_version: string;
build_time: string;
latest_version: string;
update_available: boolean;
can_upgrade: boolean;
prerelease: boolean;
release_name: string;
release_notes: string;
release_url: string;
published_at: string;
upstream_repository: string;
asset_name: string;
platform: string;
}
// ==================== 缓存管理 ====================
/**
* 缓存状态信息
*/
export interface CacheStatus {
/** 缓存文件总大小(字节) */
total_size: number;
/** 缓存 Key 数量 */
keys_count: number;
/** 最大容量限制 (MB) */
max_size_mb: number;
/** 过期时间 (分钟) */
ttl_minutes: number;
/** 是否启用 LRU 淘汰 */
lru_enabled: boolean;
/** 缓存目录基准路径 */
base_path: string;
}
/**
* 缓存配置请求参数
*/
export interface CacheConfig {
/** 最大容量限制 (MB) */
max_size_mb: number;
/** 过期时间 (分钟) */
ttl_minutes: number;
/** 是否启用 LRU 淘汰 */
lru_enabled: boolean;
}
@@ -0,0 +1,32 @@
import {BaseService} from '@/lib/services/core';
import type {
AdminUser,
CreateUserRequest,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
} from './types';
export class AdminUserService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listUsers(request: ListUsersRequest): Promise<ListUsersResponse> {
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
}
static async getUser(id: string): Promise<AdminUser> {
return this.get<AdminUser>(`/users/${id}`);
}
static async updateUserStatus(id: string, request: UpdateUserStatusRequest): Promise<void> {
return this.put<void>(`/users/${id}/status`, request);
}
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
return this.post<AdminUser>('/users', request);
}
static async deleteUser(id: string): Promise<void> {
return this.delete<void>(`/users/${id}`);
}
}
+135
View File
@@ -0,0 +1,135 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {BaseService} from '@/lib/services/core';
import type {
AuthSource,
ChangePasswordRequest,
ExternalAccountBinding,
LoginRequest,
OAuthAuthorizeResponse,
OAuthCallbackRequest,
OAuthCallbackResult,
OAuthLoginUrlResponse,
RegisterRequest,
UpdateProfileRequest,
User,
} from './types';
/**
* 认证服务
* 处理 OAuth 认证、用户信息获取、登出等
*/
export class AuthService extends BaseService {
protected static readonly basePath = '/api/v1';
/**
* 获取 OAuth 登录 URL
* @returns OAuth 授权 URL
* @throws {ApiErrorBase} 当获取失败时
*
* @example
* ```typescript
* const url = await AuthService.getLoginUrl();
* window.location.href = url; // 重定向到 OAuth 授权页面
* ```
*/
static async getLoginUrl(): Promise<OAuthLoginUrlResponse> {
return this.get<OAuthLoginUrlResponse>('/oauth/login');
}
static async getAuthSources(): Promise<AuthSource[]> {
return this.get<AuthSource[]>('/oauth/sources');
}
static async getAuthorizeUrl(source: string, purpose: 'login' | 'bind' = 'login'): Promise<OAuthAuthorizeResponse> {
return this.get<OAuthAuthorizeResponse>(`/oauth/${encodeURIComponent(source)}/authorize?purpose=${purpose}`);
}
/**
* 处理 OAuth 回调
* @param request - OAuth 回调参数(state 和 code)
* @throws {ApiErrorBase} 当回调处理失败时
* @throws {ValidationError} 当 state 无效时
*
* @example
* ```typescript
* // 在回调页面获取 URL 参数
* const params = new URLSearchParams(window.location.search);
* const state = params.get('state');
* const code = params.get('code');
*
* if (state && code) {
* await AuthService.handleCallback({ state, code });
* // 登录成功,跳转到首页
* router.push('/home');
* }
* ```
*/
static async handleCallback(request: OAuthCallbackRequest): Promise<OAuthCallbackResult> {
return this.post<OAuthCallbackResult>('/oauth/callback', request);
}
/**
* 获取当前登录用户信息
* @returns 用户信息
* @throws {UnauthorizedError} 当未登录时
*
* @example
* ```typescript
* try {
* const user = await AuthService.getUserInfo();
* console.log('当前用户:', user.username);
* } catch (error) {
* if (error instanceof UnauthorizedError) {
* // 跳转到登录页
* router.push('/login');
* }
* }
* ```
*/
static async getUserInfo(): Promise<User> {
return this.get<User>('/user-info');
}
/**
* 用户登出
* @throws {ApiErrorBase} 当登出失败时
*
* @example
* ```typescript
* await AuthService.logout();
* // 清除本地状态
* router.push('/login');
* ```
*/
static async logout(): Promise<void> {
await this.get<void>('/oauth/logout');
}
static async login(request: LoginRequest, headers?: Record<string, string>): Promise<User> {
return this.post<User>('/user/login', request, headers ? ({ headers } as unknown as InternalAxiosRequestConfig) : undefined);
}
static async register(request: RegisterRequest, headers?: Record<string, string>): Promise<User> {
return this.post<User>('/user/register', request, headers ? ({ headers } as unknown as InternalAxiosRequestConfig) : undefined);
}
static async sendEmailCode(email: string, scene: 'register' | 'login', headers?: Record<string, string>): Promise<void> {
return this.post<void>('/user/send-email-code', { email, scene }, headers ? ({ headers } as unknown as InternalAxiosRequestConfig) : undefined);
}
static async changePassword(request: ChangePasswordRequest): Promise<void> {
return this.post<void>('/user/change-password', request);
}
static async updateProfile(request: UpdateProfileRequest): Promise<User> {
return this.put<User>('/user/profile', request);
}
static async getExternalAccountBindings(): Promise<ExternalAccountBinding[]> {
return this.get<ExternalAccountBinding[]>('/oauth/external-accounts');
}
static async deleteExternalAccountBinding(id: string): Promise<void> {
return this.post<void>(`/oauth/external-accounts/${encodeURIComponent(id)}/delete`);
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
* 认证服务模块
*
* @description
* 提供 OAuth 认证相关的功能,包括:
* - 获取登录 URL
* - 处理 OAuth 回调
* - 获取用户信息
* - 用户登出
* - 发起登录流程
*
* @example
* ```typescript
* import { AuthService } from '@/lib/services';
*
* // 发起登录
* await AuthService.initiateLogin();
*
* // 获取用户信息
* const user = await AuthService.getUserInfo();
* console.log('当前用户:', user.username);
*
* // 登出
* await AuthService.logout();
* ```
*/
export { AuthService } from './auth.service';
export type {
User,
OAuthLoginUrlResponse,
OAuthCallbackRequest,
LoginRequest,
RegisterRequest,
OAuthAuthorizeResponse,
OAuthCallbackResult,
AuthSource,
ExternalAccountBinding,
ChangePasswordRequest,
UpdateProfileRequest,
} from './types';
+105
View File
@@ -0,0 +1,105 @@
/**
* 用户基本信息
*/
export interface User {
/** 用户 ID */
id: string;
/** 账户 */
username: string;
/** 昵称 */
nickname: string;
/** 头像 URL */
avatar_url: string;
/** 是否为管理员 */
is_admin: boolean;
/** 是否需要修改密码 */
need_change_password?: boolean;
/** 邮箱 */
email: string;
/** 个人简介 */
bio?: string;
/** 手机号码 */
phone?: string;
/** 性别 */
gender?: string;
/** 个人网站 */
website?: string;
/** 所在地 */
location?: string;
}
export interface UpdateProfileRequest {
nickname: string;
email: string;
avatar_url: string;
bio?: string;
phone?: string;
gender?: string;
website?: string;
location?: string;
}
/**
* OAuth 登录 URL 响应
* 后端直接返回字符串 URL
*/
export type OAuthLoginUrlResponse = string;
/**
* OAuth 回调请求参数
*/
export interface OAuthCallbackRequest {
/** 状态码 */
state: string;
/** 授权码 */
code: string;
}
export interface LoginRequest {
username: string;
password: string;
code?: string;
}
export interface RegisterRequest {
username: string;
password: string;
nickname?: string;
email?: string;
code?: string;
}
export interface OAuthAuthorizeResponse {
authorize_url: string;
}
export interface OAuthCallbackResult {
status: 'logged_in' | 'bound' | 'need_bind';
user?: User;
}
export interface AuthSource {
id: string;
name: string;
type: 'oidc';
display_name: string;
is_active: boolean;
icon_url: string;
client_secret_configured: boolean;
}
export interface ExternalAccountBinding {
id: string;
auth_source_id: string;
auth_source_name: string;
auth_source_type: string;
auth_source_label: string;
external_username: string;
email: string;
created_at: string;
}
export interface ChangePasswordRequest {
old_password: string;
new_password: string;
}
@@ -0,0 +1,23 @@
import {BaseService} from '@/lib/services/core';
import type {PublicConfigResponse} from './types';
/**
* 配置服务
* 处理系统公共配置相关的 API 请求
*/
export class ConfigService extends BaseService {
protected static readonly basePath = '/api/v1/config';
/**
* 获取公共配置
* @returns 公共配置信息
*
* @example
* ```typescript
* const config = await ConfigService.getPublicConfig();
* ```
*/
static async getPublicConfig(): Promise<PublicConfigResponse> {
return this.get<PublicConfigResponse>('/public');
}
}
+24
View File
@@ -0,0 +1,24 @@
/**
* 配置服务模块
*
* @description
* 提供系统公共配置相关的功能,包括:
* - 获取公共配置(争议时间窗口等)
* - 获取用户平台等级配置(公开接口)
*
* @example
* ```typescript
* import { ConfigService } from '@/lib/services';
*
* // 获取公共配置
* const config = await ConfigService.getPublicConfig();
* console.log('争议时间窗口:', config.dispute_time_window_hours, '小时');
*
* // 获取用户平台等级配置
* const payConfigs = await ConfigService.getUserPayConfigs();
* console.log('平台等级列表:', payConfigs);
* ```
*/
export { ConfigService } from './config.service';
export type * from './types';
+2
View File
@@ -0,0 +1,2 @@
/** 公共配置响应:key 为系统配置键,value 为配置表中保存的字符串值 */
export type PublicConfigResponse = Record<string, string | undefined>;
+324
View File
@@ -0,0 +1,324 @@
import axios, {AxiosError, AxiosResponse, CancelTokenSource, InternalAxiosRequestConfig} from 'axios';
import {toast} from 'sonner';
import {apiConfig, getApiBaseUrl} from './config';
import {
ApiErrorBase,
ForbiddenError,
NetworkError,
NotFoundError,
ServerError,
TimeoutError,
UnauthorizedError,
ValidationError,
} from './errors';
import {ApiError, ApiResponse} from './types';
/**
* API 客户端实例
* 统一处理请求配置、响应解析和错误处理
*/
const apiClient = axios.create({
baseURL: apiConfig.baseURL,
timeout: apiConfig.timeout,
withCredentials: apiConfig.withCredentials,
// Fail fast on slash redirect loops between Next dev proxy and Gin legacy routes.
maxRedirects: 5,
headers: {
'Content-Type': 'application/json',
},
});
/** Normalize legacy /api/* paths: strip trailing slash to avoid 308/301 loops in dev proxy. */
function normalizeApiPath(url?: string): string | undefined {
if (!url || !url.startsWith('/api/')) {
return url;
}
if (url.length > 5 && url.endsWith('/')) {
return url.replace(/\/+$/, '');
}
return url;
}
/**
* 请求取消令牌存储
*/
const cancelTokens = new Map<string, CancelTokenSource>();
/**
* 请求缓存存储
* 存储正在进行的请求 Promise,避免重复请求
*/
const pendingRequests = new Map<string, Promise<AxiosResponse<ApiResponse>>>();
/**
* 生成请求的唯一键
* 包含方法、URL 和请求数据的哈希,确保不同参数的请求不会被误取消
*/
function getRequestKey(config: { method?: string; url?: string; data?: unknown }): string {
const baseKey = `${ config.method?.toUpperCase() }_${ config.url }`;
/* 序列化加入键中 */
if (config.data) {
try {
const dataHash = JSON.stringify(config.data);
return `${ baseKey }_${ dataHash }`;
} catch {
// 失败使用基础键
return baseKey;
}
}
return baseKey;
}
/**
* 请求拦截器
* 添加取消令牌和其他配置
*/
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// Browser must use same-origin relative URLs so Session Cookie hits Next rewrite (3010→3000).
if (typeof window !== 'undefined') {
config.baseURL = getApiBaseUrl();
}
config.url = normalizeApiPath(config.url);
const requestKey = getRequestKey(config);
const source = axios.CancelToken.source();
config.cancelToken = source.token;
cancelTokens.set(requestKey, source);
return config;
},
(error: unknown) => Promise.reject(error),
);
/**
* 直接启动登录流程
* @param currentPath - 当前路径,用于登录成功后重定向回来
*/
function initiateLogin(currentPath: string): Promise<never> {
if (currentPath.startsWith('/login') || currentPath.startsWith('/callback')) {
return Promise.reject(new UnauthorizedError());
}
if (typeof window !== 'undefined') {
sessionStorage.setItem('redirect_after_login', currentPath);
const loginUrl = new URL('/login', window.location.origin);
loginUrl.searchParams.set('callbackUrl', currentPath);
window.location.href = loginUrl.toString();
}
return new Promise<never>(() => { });
}
/**
* 响应拦截器
* 处理 API 响应和统一错误处理
*/
apiClient.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const requestKey = getRequestKey(response.config);
cancelTokens.delete(requestKey);
pendingRequests.delete(requestKey);
const resData = response.data as ApiError & ApiResponse;
if (resData && resData.error_msg) {
return Promise.reject(new ApiErrorBase(resData.error_msg));
}
return response;
},
(error: AxiosError<ApiError>) => {
if (error.config) {
const requestKey = getRequestKey(error.config);
cancelTokens.delete(requestKey);
pendingRequests.delete(requestKey);
}
/* 请求被取消时静默处理 */
if (axios.isCancel(error)) {
const cancelError = new Error(error.message || '请求已被取消') as Error & { __CANCEL__?: boolean };
cancelError.__CANCEL__ = true;
return Promise.reject(cancelError);
}
/* 401 未授权错误 */
if (error.response?.status === 401) {
return initiateLogin(window.location.pathname + window.location.search);
}
/* 403 权限不足错误 */
if (error.response?.status === 403) {
return Promise.reject(
new ForbiddenError(error.response.data?.error_msg || '权限不足,请过盾后重试', error.response.data?.error_code, error.response.data?.details),
);
}
/* 404 资源未找到错误 */
if (error.response?.status === 404) {
return Promise.reject(
new NotFoundError(error.response.data?.error_msg || '请求的资源不存在'),
);
}
/* 400 验证错误 */
if (error.response?.status === 400) {
return Promise.reject(
new ValidationError(
error.response.data?.error_msg || '请求参数验证失败',
error.response.data?.details,
),
);
}
/* 429 速率限制错误 */
if (error.response?.status === 429) {
const retryAfter = error.response.headers?.['retry-after'];
const message = error.response.data?.error_msg ||
`请求过于频繁,请 ${ retryAfter || '稍后' } 秒后重试`;
toast.error('请求频率限制', {
description: message,
id: 'rate-limit-error',
});
return Promise.reject(
new ApiErrorBase(message, 'RATE_LIMITED', 429),
);
}
/* 5xx 服务器错误 */
if (error.response && error.response.status >= 500) {
return Promise.reject(
new ServerError(
error.response.data?.error_msg || '服务器内部错误,请稍后重试',
error.response.status,
),
);
}
/* 网络超时错误 */
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
return Promise.reject(new TimeoutError());
}
/* 网络连接错误(ECONNREFUSED, ERR_NETWORK 等) */
if (
!error.response ||
error.code === 'ECONNREFUSED' ||
error.code === 'ERR_NETWORK' ||
error.message?.includes('Network Error') ||
error.message?.includes('Failed to fetch')
) {
return Promise.reject(
new NetworkError('无法连接到服务器,请确认后端服务已启动'),
);
}
/* 其他后端返回的错误 */
if (error.response?.data?.error_msg) {
return Promise.reject(
new ApiErrorBase(
error.response.data.error_msg,
error.response.data.error_code,
error.response.status,
error.response.data.details,
),
);
}
/* 兜底错误 */
return Promise.reject(
new ApiErrorBase(error.message || '网络请求失败'),
);
},
);
/**
* 取消指定请求
* @param method - 请求方法
* @param url - 请求 URL
*/
export function cancelRequest(method: string, url: string): void {
const requestKey = `${ method.toUpperCase() }_${ url }`;
const source = cancelTokens.get(requestKey);
if (source) {
source.cancel('请求已被手动取消');
cancelTokens.delete(requestKey);
}
}
/**
* 取消所有请求
*/
export function cancelAllRequests(): void {
cancelTokens.forEach((source) => {
source.cancel('所有请求已被取消');
});
cancelTokens.clear();
}
/**
* 创建带有请求去重功能的请求方法
* @param method HTTP 方法名
* @param hasBody 是否包含请求体
*/
function createRequestMethod(
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
hasBody: boolean
) {
if (hasBody) {
return <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => {
const requestKey = getRequestKey({ method: method.toUpperCase(), url, data });
if (pendingRequests.has(requestKey)) {
return pendingRequests.get(requestKey) as Promise<AxiosResponse<T>>;
}
const promise = apiClient[method]<T>(url, data, config);
pendingRequests.set(requestKey, promise as Promise<AxiosResponse<ApiResponse>>);
promise.then(
() => pendingRequests.delete(requestKey),
() => pendingRequests.delete(requestKey),
);
return promise;
};
}
return <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => {
const requestKey = getRequestKey({ method: method.toUpperCase(), url, data: config?.params });
if (pendingRequests.has(requestKey)) {
return pendingRequests.get(requestKey) as Promise<AxiosResponse<T>>;
}
const promise = apiClient[method]<T>(url, config);
pendingRequests.set(requestKey, promise as Promise<AxiosResponse<ApiResponse>>);
promise.then(
() => pendingRequests.delete(requestKey),
() => pendingRequests.delete(requestKey),
);
return promise;
};
}
/**
* 包装的 API 客户端
* 在原有 axios 实例基础上添加请求缓存功能
*/
const wrappedApiClient = {
get: createRequestMethod('get', false) as <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
post: createRequestMethod('post', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
put: createRequestMethod('put', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
patch: createRequestMethod('patch', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
delete: createRequestMethod('delete', false) as <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
};
export default wrappedApiClient;
+188
View File
@@ -0,0 +1,188 @@
import apiClient from './api-client';
import {ApiResponse} from './types';
import {InternalAxiosRequestConfig} from 'axios';
/**
* 服务基类
* 提供通用的 HTTP 方法封装
*
* @example
* ```typescript
* class UserService extends BaseService {
* protected static readonly basePath = '/api/v1/users';
*
* static async getAll() {
* return this.get<User[]>('/');
* }
* }
* ```
*/
export class BaseService {
/**
* API 基础路径
* 子类必须重写此属性
*/
protected static readonly basePath: string = '';
/**
* 获取完整的 API 路径
* @param path - API 路径
* @returns 完整路径
*/
protected static getFullPath(path: string): string {
return `${ this.basePath }${ path }`;
}
/**
* GET 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param params - 查询参数
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async get<T>(
path: string,
params?: Record<string, unknown>,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const requestConfig: InternalAxiosRequestConfig = {
...config,
params,
} as InternalAxiosRequestConfig;
const response = await apiClient.get<ApiResponse<T>>(
this.getFullPath(path),
requestConfig,
);
return response.data.data;
}
/**
* POST 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async post<T>(
path: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.post<ApiResponse<T>>(
this.getFullPath(path),
data,
config,
);
return response.data.data;
}
/**
* PUT 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async put<T>(
path: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.put<ApiResponse<T>>(
this.getFullPath(path),
data,
config,
);
return response.data.data;
}
/**
* PATCH 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async patch<T>(
path: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.patch<ApiResponse<T>>(
this.getFullPath(path),
data,
config,
);
return response.data.data;
}
/**
* DELETE 请求
* @template T - 响应数据类型
* @param path - API 路径
* @param params - 查询参数
* @param config - 额外的请求配置
* @returns 响应数据
*/
protected static async delete<T>(
path: string,
params?: Record<string, unknown>,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const requestConfig: InternalAxiosRequestConfig = {
...config,
params,
} as InternalAxiosRequestConfig;
const response = await apiClient.delete<ApiResponse<T>>(
this.getFullPath(path),
requestConfig,
);
return response.data.data;
}
/**
* 原始 GET 请求(用于特殊 API 端点)
* @template T - 响应数据类型
* @param url - 完整 URL
* @param params - 查询参数
* @returns 响应数据(不经过 response.data.data 解包)
*
* @remarks
* 仅用于不遵循标准响应格式的特殊端点(如 /api.php)
*/
protected static async rawGet<T>(
url: string,
params?: unknown,
): Promise<T> {
const response = await apiClient.get<T>(
url,
{ params } as InternalAxiosRequestConfig,
);
return response.data;
}
/**
* 原始 POST 请求(用于特殊 API 端点)
* @template T - 响应数据类型
* @param url - 完整 URL
* @param data - 请求数据
* @param config - 额外的请求配置
* @returns 响应数据(不经过 response.data.data 解包)
*
* @remarks
* 仅用于不遵循标准响应格式的特殊端点(如 /api.php)
*/
protected static async rawPost<T>(
url: string,
data?: unknown,
config?: InternalAxiosRequestConfig,
): Promise<T> {
const response = await apiClient.post<T>(url, data, config);
return response.data;
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* API 配置
*/
/**
* 获取 API 基础 URL
* @returns API 基础 URL
*/
export function getApiBaseUrl(): string {
if (typeof window === 'undefined') {
return process.env.WAVELET_BACKEND_URL || process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || 'http://localhost:3000';
}
return process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || '';
}
/**
* API 配置选项
*/
export const apiConfig = {
/** Basic URL */
baseURL: getApiBaseUrl(),
/** 超时时间(毫秒) */
timeout: 15000,
/** 携带凭证 */
withCredentials: true,
} as const;
+105
View File
@@ -0,0 +1,105 @@
/**
* API 错误类型定义
*/
/**
* API 错误基类
*/
export class ApiErrorBase extends Error {
constructor(
message: string,
public readonly code?: string,
public readonly statusCode?: number,
public readonly details?: unknown,
) {
super(message);
this.name = 'ApiError';
Object.setPrototypeOf(this, ApiErrorBase.prototype);
}
}
/**
* 网络错误
*/
export class NetworkError extends ApiErrorBase {
constructor(message = '网络连接失败,请检查您的网络') {
super(message);
this.name = 'NetworkError';
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
/**
* 超时错误
*/
export class TimeoutError extends ApiErrorBase {
constructor(message = '请求超时,请稍后重试') {
super(message);
this.name = 'TimeoutError';
Object.setPrototypeOf(this, TimeoutError.prototype);
}
}
/**
* 未授权错误 (401)
*/
export class UnauthorizedError extends ApiErrorBase {
constructor(message = '未授权,请先登录') {
super(message, 'UNAUTHORIZED', 401);
this.name = 'UnauthorizedError';
Object.setPrototypeOf(this, UnauthorizedError.prototype);
}
}
/**
* 权限不足错误 (403)
*/
export class ForbiddenError extends ApiErrorBase {
constructor(message = '权限不足', code = 'FORBIDDEN', details?: unknown) {
super(message, code, 403, details);
this.name = 'ForbiddenError';
Object.setPrototypeOf(this, ForbiddenError.prototype);
}
}
/**
* 资源未找到错误 (404)
*/
export class NotFoundError extends ApiErrorBase {
constructor(message = '请求的资源不存在') {
super(message, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
/**
* 服务器错误 (5xx)
*/
export class ServerError extends ApiErrorBase {
constructor(message = '服务器内部错误,请稍后重试', statusCode = 500) {
super(message, 'SERVER_ERROR', statusCode);
this.name = 'ServerError';
Object.setPrototypeOf(this, ServerError.prototype);
}
}
/**
* 验证错误 (400)
*/
export class ValidationError extends ApiErrorBase {
constructor(message = '请求参数验证失败', details?: unknown) {
super(message, 'VALIDATION_ERROR', 400, details);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
/**
* 检查错误是否为请求取消错误
* @param error - 错误对象
* @returns 是否为取消错误
*/
export function isCancelError(error: unknown): boolean {
return error !== null && typeof error === 'object' && ('__CANCEL__' in error && (error as { __CANCEL__?: boolean }).__CANCEL__ === true || ('message' in error && (error as { message?: string }).message === '请求已被取消'));
}
+27
View File
@@ -0,0 +1,27 @@
/**
* 核心服务模块
* 提供 API 请求的基础设施
*/
export { default as apiClient, cancelRequest, cancelAllRequests } from './api-client';
export { BaseService } from './base.service';
export { apiConfig } from './config';
export {
ApiErrorBase,
NetworkError,
TimeoutError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ServerError,
ValidationError,
isCancelError,
} from './errors';
export type {
ApiResponse,
ApiError,
PaginationParams,
PaginationResponse,
RequestConfig,
} from './types';
+64
View File
@@ -0,0 +1,64 @@
/**
* API 响应通用结构
* @template T - 数据类型
*/
export interface ApiResponse<T = unknown> {
/** 响应数据 */
data: T;
/** 响应消息 */
message?: string;
/** 响应状态码 */
code?: number;
}
/**
* API 错误响应结构
*/
export interface ApiError {
/** 错误消息 */
error_msg: string;
/** 错误代码 */
error_code?: string;
/** 错误详情 */
details?: unknown;
}
/**
* 分页请求参数
*/
export interface PaginationParams {
/** 页码,从 1 开始 */
page: number;
/** 每页数量 */
page_size: number;
}
/**
* 分页响应数据
* @template T - 列表项类型
*/
export interface PaginationResponse<T> {
/** 数据列表 */
items: T[];
/** 总数 */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 总页数 */
total_pages: number;
}
/**
* HTTP 请求配置
*/
export interface RequestConfig {
/** 请求超时时间(毫秒) */
timeout?: number;
/** 请求头 */
headers?: Record<string, string>;
/** 是否携带凭证 */
withCredentials?: boolean;
}
@@ -0,0 +1,62 @@
import type {InternalAxiosRequestConfig} from 'axios';
import apiClient from '@/lib/services/core/api-client';
import {BaseService} from '@/lib/services/core';
import type {DBOverview, ExecuteSQLResponse, TableDataResponse} from './types';
/**
* 数据库管理服务类
*/
export class DbManageService extends BaseService {
protected static readonly basePath = '/api/v1/admin/db-manage';
/**
* 获取数据库运行概览
*/
static async getOverview(): Promise<DBOverview> {
return this.get<DBOverview>('/overview');
}
/**
* 获取数据库所有物理表名
*/
static async listTables(): Promise<string[]> {
return this.get<string[]>('/tables');
}
/**
* 获取某张数据表的数据(分页)
*/
static async getTableData(params: {
table: string;
page: number;
pageSize: number;
}): Promise<TableDataResponse> {
return this.get<TableDataResponse>('/table-data', params as unknown as Record<string, unknown>);
}
/**
* 在数据库中执行自定义 SQL 语句
*/
static async executeSQL(sql: string): Promise<ExecuteSQLResponse> {
return this.post<ExecuteSQLResponse>('/query', { sql });
}
/**
* 导出数据库备份(SQLite .db / PostgreSQL .sql)
*/
static async exportDatabase(): Promise<{ blob: Blob; filename: string }> {
const response = await apiClient.get<Blob>('/api/v1/admin/db-export', {
withCredentials: true,
responseType: 'blob',
} as InternalAxiosRequestConfig);
const disposition = response.headers['content-disposition'] as string | undefined;
let filename = 'openflare_export';
if (disposition) {
const match = disposition.match(/filename="?([^";]+)"?/);
if (match) filename = match[1];
}
return { blob: response.data, filename };
}
}
+2
View File
@@ -0,0 +1,2 @@
export { DbManageService } from './db-manage.service';
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './types';
+31
View File
@@ -0,0 +1,31 @@
/**
* 数据库概览信息
*/
export interface DBOverview {
type: string;
version: string;
name: string;
size: string;
table_count: number;
connections: number;
}
/**
* 动态数据表分页数据响应
*/
export interface TableDataResponse {
columns: string[];
total: number;
results: Record<string, unknown>[];
}
/**
* 执行自定义 SQL 的响应
*/
export interface ExecuteSQLResponse {
type: "select" | "exec";
columns?: string[];
results?: Record<string, unknown>[];
affected_rows: number;
execution_time_ms: number;
}
+225
View File
@@ -0,0 +1,225 @@
/**
* 服务层统一入口
*
* @example
* ```typescript
* import services from '@/lib/services';
*
* const user = await services.auth.getUserInfo();
* const configs = await services.adminSystemConfig.listSystemConfigs();
* ```
*/
import {
AdminAuthSourceService,
AdminCacheService,
AdminLogService,
AdminStatusService,
AdminSystemConfigService,
AdminTaskService,
AdminTemplateService,
AdminUserService,
} from './admin';
import {AuthService} from './auth';
import {ConfigService} from './config';
import {DbManageService} from './db-manage';
import {PushService} from './push';
import {AdminUploadService, UploadService} from './upload';
import {UserService} from './user';
import {
AccessLogService,
ApplyLogService,
ConfigVersionService,
DashboardService,
DnsAccountService,
NodeService,
OptionService,
OriginService,
PagesService,
ProxyRouteService,
StatusService,
TlsCertificateService,
UptimeKumaService,
WafService,
WebsiteService,
} from './openflare';
const services = {
auth: AuthService,
adminSystemConfig: AdminSystemConfigService,
adminAuthSource: AdminAuthSourceService,
adminTask: AdminTaskService,
adminUser: AdminUserService,
adminStatus: AdminStatusService,
adminLog: AdminLogService,
adminTemplate: AdminTemplateService,
adminCache: AdminCacheService,
user: UserService,
config: ConfigService,
upload: UploadService,
adminUpload: AdminUploadService,
dbManage: DbManageService,
push: PushService,
openflareNode: NodeService,
openflareProxyRoute: ProxyRouteService,
openflareConfigVersion: ConfigVersionService,
openflareApplyLog: ApplyLogService,
openflareDashboard: DashboardService,
openflareWaf: WafService,
openflareWebsite: WebsiteService,
openflareTls: TlsCertificateService,
openflareDns: DnsAccountService,
openflarePages: PagesService,
openflareOrigin: OriginService,
openflareAccessLog: AccessLogService,
openflareOption: OptionService,
openflareUptimeKuma: UptimeKumaService,
openflareStatus: StatusService,
} as const;
export default services;
export { services };
// ==================== 核心模块导出 ====================
export {
apiClient,
BaseService,
apiConfig,
cancelRequest,
cancelAllRequests,
} from './core';
export {
ApiErrorBase,
NetworkError,
TimeoutError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ServerError,
ValidationError,
isCancelError,
} from './core';
export type {
ApiResponse,
ApiError,
PaginationParams,
PaginationResponse,
RequestConfig,
} from './core';
// ==================== 业务服务导出 ====================
export { AuthService } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding, ChangePasswordRequest, UpdateProfileRequest } from './auth';
export { ConfigService } from './config';
export type { PublicConfigResponse } from './config';
export {
AdminSystemConfigService,
AdminAuthSourceService,
AdminTaskService,
AdminUserService,
AdminStatusService,
AdminLogService,
AdminTemplateService,
AdminCacheService,
} from './admin';
export type {
SystemConfig,
CreateSystemConfigRequest,
UpdateSystemConfigRequest,
TaskMeta,
TaskParam,
TaskParamType,
TaskExecution,
TaskExecutionStatus,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
DispatchTaskRequest,
AdminUser,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
CreateUserRequest,
SystemStatus,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
Template,
CreateTemplateRequest,
UpdateTemplateRequest,
AuthSource as AdminAuthSource,
AuthSourceRequest,
ToggleAuthSourceRequest,
StorageDriver,
StorageConfig,
ObjectStorageConfig,
} from './admin';
export { UserService } from './user';
export type { AccessToken, CreateTokenResponse } from './user';
export { UploadService, AdminUploadService, formatFileSize, getFileUrl } from './upload';
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse, ImageQuality } from './upload';
export { DbManageService } from './db-manage';
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './db-manage';
export { PushService } from './push';
export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
// ==================== OpenFlare 业务服务导出 ====================
export {
OpenFlareBaseService,
NodeService,
ProxyRouteService,
ConfigVersionService,
ApplyLogService,
DashboardService,
WafService,
WebsiteService,
TlsCertificateService,
DnsAccountService,
PagesService,
OriginService,
AccessLogService,
OptionService,
StatusService,
UptimeKumaService,
} from './openflare';
export type {
NodeItem,
ProxyRouteItem,
ProxyRouteConfigSection,
ConfigVersionSummary,
ConfigVersionDetail,
ConfigDiffResult,
ConfigPreviewResult,
ApplyLogList,
DashboardOverview,
WAFIPGroup,
WAFRuleGroup,
WAFSiteRuleGroups,
ManagedDomainItem,
TlsCertificateItem,
DnsAccountItem,
PagesProject,
PagesDeployment,
OriginItem,
OriginDetail,
AccessLogList,
OptionItem,
GeoIPLookupResult,
DatabaseCleanupResult,
OpenFlarePublicStatus,
} from './openflare';
@@ -0,0 +1,76 @@
import {OpenFlareBaseService} from './base.service';
import type {
AccessLogCleanupPayload,
AccessLogCleanupResult,
AccessLogFilters,
AccessLogIPSummaryFilters,
AccessLogIPSummaryList,
AccessLogIPTrend,
AccessLogIPTrendFilters,
AccessLogList,
FoldedAccessLogFilters,
FoldedAccessLogIPFilters,
FoldedAccessLogIPList,
FoldedAccessLogList,
} from './types';
function buildSearchParams(filters: object): Record<string, unknown> {
const params: Record<string, unknown> = {};
Object.entries(filters as Record<string, string | number | undefined>).forEach(
([key, value]) => {
if (value === undefined || value === null || value === '') return;
params[key] = value;
},
);
return params;
}
export class AccessLogService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/access-logs';
static list(filters: AccessLogFilters = {}): Promise<AccessLogList> {
return this.get<AccessLogList>('/', buildSearchParams(filters));
}
static listFolds(
filters: FoldedAccessLogFilters,
): Promise<FoldedAccessLogList> {
return this.get<FoldedAccessLogList>(
'/folds',
buildSearchParams(filters),
);
}
static listFoldIPs(
filters: FoldedAccessLogIPFilters,
): Promise<FoldedAccessLogIPList> {
return this.get<FoldedAccessLogIPList>(
'/folds/ip-summary',
buildSearchParams(filters),
);
}
static listIPSummaries(
filters: AccessLogIPSummaryFilters = {},
): Promise<AccessLogIPSummaryList> {
return this.get<AccessLogIPSummaryList>(
'/ip-summary',
buildSearchParams(filters),
);
}
static getIPTrend(
filters: AccessLogIPTrendFilters,
): Promise<AccessLogIPTrend> {
return this.get<AccessLogIPTrend>(
'/ip-summary/trend',
buildSearchParams(filters),
);
}
static cleanup(
payload: AccessLogCleanupPayload,
): Promise<AccessLogCleanupResult> {
return this.post<AccessLogCleanupResult>('/cleanup', payload);
}
}
@@ -0,0 +1,32 @@
import {OpenFlareBaseService} from './base.service';
import type {
ApplyLogCleanupPayload,
ApplyLogCleanupResult,
ApplyLogList,
ApplyLogListQuery,
} from './types';
export class ApplyLogService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/apply-logs';
static list(query: ApplyLogListQuery = {}): Promise<ApplyLogList> {
const params: Record<string, unknown> = {};
const normalizedNodeId = query.node_id?.trim();
if (normalizedNodeId) {
params.node_id = normalizedNodeId;
}
if (query.pageNo) {
params.pageNo = query.pageNo;
}
if (query.pageSize) {
params.pageSize = query.pageSize;
}
return this.get<ApplyLogList>('/', params);
}
static cleanup(payload: ApplyLogCleanupPayload): Promise<ApplyLogCleanupResult> {
return this.post<ApplyLogCleanupResult>('/cleanup', payload);
}
}
@@ -0,0 +1,5 @@
import {BaseService} from '@/lib/services/core';
export class OpenFlareBaseService extends BaseService {
protected static override readonly basePath: string = '/api/v1/d';
}
@@ -0,0 +1,55 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {OpenFlareBaseService} from './base.service';
import type {
ConfigDiffResult,
ConfigPreviewResult,
ConfigVersionCleanupPayload,
ConfigVersionCleanupResult,
ConfigVersionDetail,
ConfigVersionSummary,
} from './types';
export class ConfigVersionService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/config-versions';
static list(): Promise<ConfigVersionSummary[]> {
return this.get<ConfigVersionSummary[]>('/');
}
static getActive(): Promise<ConfigVersionDetail> {
return this.get<ConfigVersionDetail>('/active');
}
static preview(): Promise<ConfigPreviewResult> {
return this.get<ConfigPreviewResult>('/preview');
}
static diff(): Promise<ConfigDiffResult> {
return this.get<ConfigDiffResult>('/diff');
}
static getById(id: number): Promise<ConfigVersionDetail> {
return this.get<ConfigVersionDetail>(`/${id}`);
}
static publish(force?: boolean): Promise<ConfigVersionDetail> {
return this.post<ConfigVersionDetail>(
'/publish',
undefined,
force
? ({ params: { force: true } } as InternalAxiosRequestConfig)
: undefined,
);
}
static activate(id: number): Promise<ConfigVersionDetail> {
return this.post<ConfigVersionDetail>(`/${id}/activate`);
}
static cleanup(
payload: ConfigVersionCleanupPayload,
): Promise<ConfigVersionCleanupResult> {
return this.post<ConfigVersionCleanupResult>('/cleanup', payload);
}
}
@@ -0,0 +1,242 @@
import {OpenFlareBaseService} from './base.service';
import type {
CompactCapacityTrendPoint,
CompactDashboardNodeHealth,
CompactDiskIOTrendPoint,
CompactDistributionItem,
CompactNetworkTrendPoint,
CompactTrafficTrendPoint,
DashboardCapacity,
DashboardNodeHealth,
DashboardOverview,
DashboardOverviewCompact,
DashboardSummary,
DashboardTraffic,
DistributionItem,
} from './types';
function arrayOrEmpty<T>(value: T[] | null | undefined) {
return Array.isArray(value) ? value : [];
}
function isCompactDistributionItem(
value: DistributionItem | CompactDistributionItem,
): value is CompactDistributionItem {
return Array.isArray(value);
}
function isCompactTrafficTrendPoint(
value:
| DashboardOverview['trends']['traffic_24h'][number]
| CompactTrafficTrendPoint,
): value is CompactTrafficTrendPoint {
return Array.isArray(value);
}
function isCompactCapacityTrendPoint(
value:
| DashboardOverview['trends']['capacity_24h'][number]
| CompactCapacityTrendPoint,
): value is CompactCapacityTrendPoint {
return Array.isArray(value);
}
function isCompactNetworkTrendPoint(
value:
| DashboardOverview['trends']['network_24h'][number]
| CompactNetworkTrendPoint,
): value is CompactNetworkTrendPoint {
return Array.isArray(value);
}
function isCompactDiskIOTrendPoint(
value:
| DashboardOverview['trends']['disk_io_24h'][number]
| CompactDiskIOTrendPoint,
): value is CompactDiskIOTrendPoint {
return Array.isArray(value);
}
function isCompactDashboardNode(
value: DashboardNodeHealth | CompactDashboardNodeHealth,
): value is CompactDashboardNodeHealth {
return Array.isArray(value);
}
function normalizeDistributionItems(
items: Array<DistributionItem | CompactDistributionItem> | null | undefined,
): DistributionItem[] {
return arrayOrEmpty(items).map((item) =>
isCompactDistributionItem(item)
? { key: String(item[0] ?? ''), value: Number(item[1] ?? 0) }
: item,
);
}
function normalizeTrafficTrendPoints(
items:
| Array<
| DashboardOverview['trends']['traffic_24h'][number]
| CompactTrafficTrendPoint
>
| null
| undefined,
) {
return arrayOrEmpty(items).map((item) =>
isCompactTrafficTrendPoint(item)
? {
bucket_started_at: String(item[0] ?? ''),
request_count: Number(item[1] ?? 0),
error_count: Number(item[2] ?? 0),
unique_visitor_count: Number(item[3] ?? 0),
}
: item,
);
}
function normalizeCapacityTrendPoints(
items:
| Array<
| DashboardOverview['trends']['capacity_24h'][number]
| CompactCapacityTrendPoint
>
| null
| undefined,
) {
return arrayOrEmpty(items).map((item) =>
isCompactCapacityTrendPoint(item)
? {
bucket_started_at: String(item[0] ?? ''),
average_cpu_usage_percent: Number(item[1] ?? 0),
average_memory_usage_percent: Number(item[2] ?? 0),
reported_nodes: Number(item[3] ?? 0),
}
: item,
);
}
function normalizeNetworkTrendPoints(
items:
| Array<
| DashboardOverview['trends']['network_24h'][number]
| CompactNetworkTrendPoint
>
| null
| undefined,
) {
return arrayOrEmpty(items).map((item) =>
isCompactNetworkTrendPoint(item)
? {
bucket_started_at: String(item[0] ?? ''),
network_rx_bytes: Number(item[1] ?? 0),
network_tx_bytes: Number(item[2] ?? 0),
openresty_rx_bytes: Number(item[3] ?? 0),
openresty_tx_bytes: Number(item[4] ?? 0),
reported_nodes: Number(item[5] ?? 0),
}
: item,
);
}
function normalizeDiskIOTrendPoints(
items:
| Array<
| DashboardOverview['trends']['disk_io_24h'][number]
| CompactDiskIOTrendPoint
>
| null
| undefined,
) {
return arrayOrEmpty(items).map((item) =>
isCompactDiskIOTrendPoint(item)
? {
bucket_started_at: String(item[0] ?? ''),
disk_read_bytes: Number(item[1] ?? 0),
disk_write_bytes: Number(item[2] ?? 0),
reported_nodes: Number(item[3] ?? 0),
}
: item,
);
}
function normalizeDashboardNodes(
items:
| Array<DashboardNodeHealth | CompactDashboardNodeHealth>
| null
| undefined,
): DashboardNodeHealth[] {
return arrayOrEmpty(items).map((item) =>
isCompactDashboardNode(item)
? {
id: Number(item[0] ?? 0),
node_id: String(item[1] ?? ''),
name: String(item[2] ?? ''),
geo_name: String(item[3] ?? ''),
geo_latitude:
item[4] === null || item[4] === undefined ? null : Number(item[4]),
geo_longitude:
item[5] === null || item[5] === undefined ? null : Number(item[5]),
status: (item[6] ?? 'pending') as DashboardNodeHealth['status'],
openresty_status: (item[7] ??
'unknown') as DashboardNodeHealth['openresty_status'],
current_version: String(item[8] ?? ''),
last_seen_at: String(item[9] ?? ''),
active_event_count: Number(item[10] ?? 0),
cpu_usage_percent: Number(item[11] ?? 0),
memory_usage_percent: Number(item[12] ?? 0),
storage_usage_percent: Number(item[13] ?? 0),
request_count: Number(item[14] ?? 0),
error_count: Number(item[15] ?? 0),
unique_visitor_count: Number(item[16] ?? 0),
}
: item,
);
}
function normalizeDashboardOverview(
overview: DashboardOverview | DashboardOverviewCompact | null | undefined,
): DashboardOverview | null {
if (!overview) {
return null;
}
const summary = (overview.summary ?? {}) as DashboardSummary;
const traffic = (overview.traffic ?? {}) as DashboardTraffic;
const capacity = (overview.capacity ?? {}) as DashboardCapacity;
return {
generated_at: String(overview.generated_at ?? ''),
summary,
traffic,
capacity,
nodes: normalizeDashboardNodes(overview.nodes),
distributions: {
source_countries: normalizeDistributionItems(
overview.distributions?.source_countries,
),
status_codes: normalizeDistributionItems(
overview.distributions?.status_codes,
),
top_domains: normalizeDistributionItems(
overview.distributions?.top_domains,
),
},
trends: {
traffic_24h: normalizeTrafficTrendPoints(overview.trends?.traffic_24h),
capacity_24h: normalizeCapacityTrendPoints(overview.trends?.capacity_24h),
network_24h: normalizeNetworkTrendPoints(overview.trends?.network_24h),
disk_io_24h: normalizeDiskIOTrendPoints(overview.trends?.disk_io_24h),
},
};
}
export class DashboardService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/dashboard';
static async getOverview(): Promise<DashboardOverview | null> {
const overview = await this.get<
DashboardOverview | DashboardOverviewCompact
>('/overview');
return normalizeDashboardOverview(overview);
}
}
@@ -0,0 +1,27 @@
import {OpenFlareBaseService} from './base.service';
import type {DnsAccountItem, DnsAccountMutationPayload} from './types';
export class DnsAccountService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/dns-accounts';
static async list(): Promise<DnsAccountItem[]> {
return this.get<DnsAccountItem[]>('/');
}
static async create(
payload: DnsAccountMutationPayload,
): Promise<DnsAccountItem> {
return this.post<DnsAccountItem>('/', payload);
}
static async update(
id: number,
payload: DnsAccountMutationPayload,
): Promise<DnsAccountItem> {
return this.post<DnsAccountItem>(`/${id}/update`, payload);
}
static async deleteById(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
}
+148
View File
@@ -0,0 +1,148 @@
export { OpenFlareBaseService } from './base.service';
export { NodeService } from './node.service';
export { ProxyRouteService } from './proxy-route.service';
export { ConfigVersionService } from './config-version.service';
export { ApplyLogService } from './apply-log.service';
export { DashboardService } from './dashboard.service';
export { WafService } from './waf.service';
export { WebsiteService } from './website.service';
export { TlsCertificateService } from './tls-certificate.service';
export { DnsAccountService } from './dns-account.service';
export { PagesService } from './pages.service';
export { OriginService } from './origin.service';
export { AccessLogService } from './access-log.service';
export { OptionService } from './option.service';
export { UptimeKumaService } from './uptimekuma.service';
export { StatusService } from './status.service';
export type {
ApplyLogCleanupPayload,
ApplyLogCleanupResult,
ApplyLogItem,
ApplyLogList,
ApplyLogListQuery,
ApplyResult,
ConfigDiffResult,
ConfigOptionDiffItem,
ConfigPreviewResult,
ConfigVersionCleanupPayload,
ConfigVersionCleanupResult,
ConfigVersionDetail,
ConfigVersionSummary,
NodeAgentReleaseInfo,
NodeAgentUpdatePayload,
NodeBootstrapToken,
NodeItem,
NodeMutationPayload,
NodeHealthEvent,
NodeObservability,
NodeObservabilityAnalytics,
NodeObservabilityTrends,
NodeSystemProfile,
NodeTrafficReport,
NodeStatus,
NodeType,
OpenrestyStatus,
ProxyRouteConfigSection,
ProxyRouteCustomHeader,
ProxyRouteItem,
ProxyRouteMutationPayload,
ProxyRoutePoWConfig,
ReleaseChannel,
SupportFile,
DashboardOverview,
DashboardNodeHealth,
DashboardSummary,
DashboardTraffic,
DashboardCapacity,
DistributionItem,
TrafficDistributions,
TrafficTrendPoint,
CapacityTrendPoint,
NetworkTrendPoint,
DiskIOTrendPoint,
WAFIPGroup,
WAFIPGroupAutoTestPayload,
WAFIPGroupAutoTestResult,
WAFIPGroupPayload,
WAFIPGroupSyncResult,
WAFIPGroupSubscriptionFormat,
WAFIPGroupType,
WAFRuleGroup,
WAFRuleGroupPayload,
WAFSiteRuleGroups,
AccessLogCleanupPayload,
AccessLogCleanupResult,
AccessLogFilters,
AccessLogIPSummaryFilters,
AccessLogIPSummaryItem,
AccessLogIPSummaryList,
AccessLogIPTrend,
AccessLogIPTrendFilters,
AccessLogItem,
AccessLogList,
FoldedAccessLogFilters,
FoldedAccessLogIPFilters,
FoldedAccessLogIPList,
FoldedAccessLogList,
OptionItem,
GeoIPLookupResult,
DatabaseCleanupPayload,
DatabaseCleanupResult,
DatabaseCleanupTarget,
OpenFlarePublicStatus,
OriginDetail,
OriginItem,
OriginMutationPayload,
PagesDeployment,
PagesDeploymentFile,
PagesProject,
PagesProjectPayload,
AcmeAccountItem,
DnsAccountItem,
DnsAccountMutationPayload,
ManagedDomainItem,
ManagedDomainMatchResult,
ManagedDomainMutationPayload,
TlsCertificateApplyPayload,
TlsCertificateContentItem,
TlsCertificateDetailItem,
TlsCertificateFileImportPayload,
TlsCertificateItem,
TlsCertificateMutationPayload,
} from './types';
import {AccessLogService} from './access-log.service';
import {ApplyLogService} from './apply-log.service';
import {ConfigVersionService} from './config-version.service';
import {DashboardService} from './dashboard.service';
import {DnsAccountService} from './dns-account.service';
import {NodeService} from './node.service';
import {OptionService} from './option.service';
import {StatusService} from './status.service';
import {UptimeKumaService} from './uptimekuma.service';
import {OriginService} from './origin.service';
import {PagesService} from './pages.service';
import {ProxyRouteService} from './proxy-route.service';
import {TlsCertificateService} from './tls-certificate.service';
import {WafService} from './waf.service';
import {WebsiteService} from './website.service';
export const openflareServices = {
node: NodeService,
proxyRoute: ProxyRouteService,
configVersion: ConfigVersionService,
applyLog: ApplyLogService,
dashboard: DashboardService,
waf: WafService,
website: WebsiteService,
tlsCertificate: TlsCertificateService,
dnsAccount: DnsAccountService,
pages: PagesService,
origin: OriginService,
accessLog: AccessLogService,
option: OptionService,
uptimeKuma: UptimeKumaService,
status: StatusService,
} as const;
@@ -0,0 +1,78 @@
import {OpenFlareBaseService} from './base.service';
import type {
NodeAgentReleaseInfo,
NodeAgentUpdatePayload,
NodeBootstrapToken,
NodeItem,
NodeMutationPayload,
NodeObservability,
ReleaseChannel,
} from './types';
export class NodeService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/nodes';
static async listNodes(): Promise<NodeItem[]> {
return this.get<NodeItem[]>('/');
}
static async createNode(payload: NodeMutationPayload): Promise<NodeItem> {
return this.post<NodeItem>('/', payload);
}
static async updateNode(id: number, payload: NodeMutationPayload): Promise<NodeItem> {
return this.post<NodeItem>(`/${id}/update`, payload);
}
static async deleteNode(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
static async getBootstrapToken(): Promise<NodeBootstrapToken> {
return this.get<NodeBootstrapToken>('/bootstrap-token');
}
static async rotateBootstrapToken(): Promise<NodeBootstrapToken> {
return this.post<NodeBootstrapToken>('/bootstrap-token/rotate');
}
static async requestAgentUpdate(
id: number,
payload?: NodeAgentUpdatePayload,
): Promise<NodeItem> {
return this.post<NodeItem>(`/${id}/agent-update`, payload ?? {});
}
static async requestForceSync(id: number): Promise<NodeItem> {
return this.post<NodeItem>(`/${id}/force-sync`);
}
static async requestOpenrestyRestart(id: number): Promise<NodeItem> {
return this.post<NodeItem>(`/${id}/openresty-restart`);
}
static async getAgentRelease(
id: number,
channel: ReleaseChannel = 'stable',
): Promise<NodeAgentReleaseInfo> {
return this.get<NodeAgentReleaseInfo>(`/${id}/agent-release`, { channel });
}
static async getObservability(
id: number,
options?: { hours?: number; limit?: number },
): Promise<NodeObservability> {
return this.get<NodeObservability>(`/${id}/observability`, {
hours: options?.hours,
limit: options?.limit,
});
}
static async cleanupHealthEvents(
id: number,
): Promise<{ node_id: string; deleted_count: number }> {
return this.post<{ node_id: string; deleted_count: number }>(
`/${id}/observability/cleanup`,
);
}
}
@@ -0,0 +1,35 @@
import {OpenFlareBaseService} from './base.service';
import type {
DatabaseCleanupPayload,
DatabaseCleanupResult,
GeoIPLookupResult,
OptionBatchPayload,
OptionItem,
} from './types';
export class OptionService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/option';
static list(): Promise<OptionItem[]> {
return this.get<OptionItem[]>('/');
}
static update(key: string, value: string): Promise<void> {
return this.post<void>('/update', { key, value });
}
static updateBatch(options: OptionItem[]): Promise<void> {
const payload: OptionBatchPayload = { options };
return this.post<void>('/update-batch', payload);
}
static lookupGeoIP(provider: string, ip: string): Promise<GeoIPLookupResult> {
return this.post<GeoIPLookupResult>('/geoip/lookup', { provider, ip });
}
static cleanupDatabase(
payload: DatabaseCleanupPayload,
): Promise<DatabaseCleanupResult> {
return this.post<DatabaseCleanupResult>('/database/cleanup', payload);
}
}
@@ -0,0 +1,29 @@
import {OpenFlareBaseService} from './base.service';
import type {OriginDetail, OriginItem, OriginMutationPayload} from './types';
export class OriginService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/origins';
static list(): Promise<OriginItem[]> {
return this.get<OriginItem[]>('/');
}
static getById(id: number): Promise<OriginDetail> {
return this.get<OriginDetail>(`/${id}`);
}
static create(payload: OriginMutationPayload): Promise<OriginItem> {
return this.post<OriginItem>('/', payload);
}
static update(
id: number,
payload: OriginMutationPayload,
): Promise<OriginItem> {
return this.post<OriginItem>(`/${id}/update`, payload);
}
static deleteById(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
}
@@ -0,0 +1,108 @@
import type {AxiosProgressEvent, InternalAxiosRequestConfig} from 'axios';
import apiClient from '@/lib/services/core/api-client';
import type {ApiResponse} from '@/lib/services/core';
import {OpenFlareBaseService} from './base.service';
import type {
PagesDeployment,
PagesDeploymentFile,
PagesDeploymentUploadPayload,
PagesProject,
PagesProjectPayload,
} from './types';
export class PagesService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/pages';
static listProjects(): Promise<PagesProject[]> {
return this.get<PagesProject[]>('/');
}
static getProject(id: number): Promise<PagesProject> {
return this.get<PagesProject>(`/${id}`);
}
static createProject(payload: PagesProjectPayload): Promise<PagesProject> {
return this.post<PagesProject>('/', payload);
}
static updateProject(
id: number,
payload: PagesProjectPayload,
): Promise<PagesProject> {
return this.post<PagesProject>(`/${id}/update`, payload);
}
static deleteProject(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
static listDeployments(projectId: number): Promise<PagesDeployment[]> {
return this.get<PagesDeployment[]>(`/${projectId}/deployments`);
}
static listDeploymentFiles(
projectId: number,
deploymentId: number,
): Promise<PagesDeploymentFile[]> {
return this.get<PagesDeploymentFile[]>(
`/${projectId}/deployments/${deploymentId}/files`,
);
}
static uploadDeployment(
projectId: number,
payload: PagesDeploymentUploadPayload,
): Promise<PagesDeployment> {
const formData = new FormData();
formData.append('package', payload.file);
formData.append('root_dir', payload.rootDir ?? '');
formData.append('entry_file', payload.entryFile ?? 'index.html');
return this.postFormData<PagesDeployment>(
`/${projectId}/deployments/upload`,
formData,
payload.onProgress,
);
}
static activateDeployment(
projectId: number,
deploymentId: number,
): Promise<PagesProject> {
return this.post<PagesProject>(
`/${projectId}/deployments/${deploymentId}/activate`,
);
}
static deleteDeployment(
projectId: number,
deploymentId: number,
): Promise<void> {
return this.post<void>(
`/${projectId}/deployments/${deploymentId}/delete`,
);
}
private static async postFormData<T>(
path: string,
formData: FormData,
onProgress?: (percent: number) => void,
): Promise<T> {
const response = await apiClient.post<ApiResponse<T>>(
this.getFullPath(path),
formData,
{
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (event: AxiosProgressEvent) => {
if (!onProgress || !event.total) return;
const percent = Math.round((event.loaded / event.total) * 100);
onProgress(percent);
},
} as InternalAxiosRequestConfig,
);
return response.data.data;
}
}
@@ -0,0 +1,29 @@
import {OpenFlareBaseService} from './base.service';
import type {ProxyRouteItem, ProxyRouteMutationPayload} from './types';
export class ProxyRouteService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/proxy-routes';
static async list(): Promise<ProxyRouteItem[]> {
return this.get<ProxyRouteItem[]>('/');
}
static async getById(id: number): Promise<ProxyRouteItem> {
return this.get<ProxyRouteItem>(`/${id}`);
}
static async create(payload: ProxyRouteMutationPayload): Promise<ProxyRouteItem> {
return this.post<ProxyRouteItem>('/', payload);
}
static async update(
id: number,
payload: ProxyRouteMutationPayload,
): Promise<ProxyRouteItem> {
return this.post<ProxyRouteItem>(`/${id}/update`, payload);
}
static async deleteById(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
}
@@ -0,0 +1,8 @@
import {OpenFlareBaseService} from './base.service';
import type {OpenFlarePublicStatus} from './types';
export class StatusService extends OpenFlareBaseService {
static getPublicStatus(): Promise<OpenFlarePublicStatus> {
return this.get<OpenFlarePublicStatus>('/status');
}
}
@@ -0,0 +1,91 @@
import {OpenFlareBaseService} from './base.service';
import type {
AcmeAccountItem,
TlsCertificateApplyPayload,
TlsCertificateContentItem,
TlsCertificateDetailItem,
TlsCertificateFileImportPayload,
TlsCertificateItem,
TlsCertificateMutationPayload,
} from './types';
class AcmeAccountApi extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/acme-accounts';
static getDefault(): Promise<AcmeAccountItem> {
return this.get<AcmeAccountItem>('/default');
}
}
export class TlsCertificateService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/tls-certificates';
static async list(): Promise<TlsCertificateItem[]> {
return this.get<TlsCertificateItem[]>('/');
}
static async getById(id: number): Promise<TlsCertificateDetailItem> {
return this.get<TlsCertificateDetailItem>(`/${id}`);
}
static async getContent(id: number): Promise<TlsCertificateContentItem> {
return this.get<TlsCertificateContentItem>(`/${id}/content`);
}
static async create(
payload: TlsCertificateMutationPayload,
): Promise<TlsCertificateItem> {
return this.post<TlsCertificateItem>('/', payload);
}
static async update(
id: number,
payload: TlsCertificateMutationPayload,
): Promise<TlsCertificateItem> {
return this.post<TlsCertificateItem>(`/${id}/update`, payload);
}
static async deleteById(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
static async apply(
payload: TlsCertificateApplyPayload,
): Promise<TlsCertificateItem> {
return this.post<TlsCertificateItem>('/apply', payload);
}
static async renew(id: number): Promise<TlsCertificateItem> {
return this.post<TlsCertificateItem>(`/${id}/renew`);
}
static async updateAcme(
id: number,
payload: TlsCertificateApplyPayload,
): Promise<TlsCertificateItem> {
return this.post<TlsCertificateItem>(`/${id}/update-acme`, payload);
}
static async convertToAcme(
id: number,
payload: TlsCertificateApplyPayload,
): Promise<TlsCertificateItem> {
return this.post<TlsCertificateItem>(`/${id}/convert-acme`, payload);
}
static async importFile(
payload: TlsCertificateFileImportPayload,
): Promise<TlsCertificateItem> {
const formData = new FormData();
formData.append('name', payload.name);
formData.append('remark', payload.remark);
formData.append('cert_file', payload.certFile);
formData.append('key_file', payload.keyFile);
return this.post<TlsCertificateItem>('/import-file', formData);
}
static getDefaultAcmeAccount(): Promise<AcmeAccountItem> {
return AcmeAccountApi.getDefault();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
import {OpenFlareBaseService} from './base.service';
export class UptimeKumaService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/uptimekuma';
static sync(): Promise<void> {
return this.post<void>('/sync');
}
}
@@ -0,0 +1,85 @@
import {OpenFlareBaseService} from './base.service';
import type {
WAFIPGroup,
WAFIPGroupAutoTestPayload,
WAFIPGroupAutoTestResult,
WAFIPGroupPayload,
WAFIPGroupSyncResult,
WAFRuleGroup,
WAFRuleGroupPayload,
WAFSiteRuleGroups,
} from './types';
export class WafService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/waf';
static async listRuleGroups(): Promise<WAFRuleGroup[]> {
return this.get<WAFRuleGroup[]>('/rule-groups');
}
static async getRuleGroup(id: number): Promise<WAFRuleGroup> {
return this.get<WAFRuleGroup>(`/rule-groups/${id}`);
}
static async createRuleGroup(payload: WAFRuleGroupPayload): Promise<WAFRuleGroup> {
return this.post<WAFRuleGroup>('/rule-groups', payload);
}
static async updateRuleGroup(
id: number,
payload: WAFRuleGroupPayload,
): Promise<WAFRuleGroup> {
return this.post<WAFRuleGroup>(`/rule-groups/${id}/update`, payload);
}
static async deleteRuleGroup(id: number): Promise<void> {
return this.post<void>(`/rule-groups/${id}/delete`);
}
static async updateRuleGroupSites(id: number, ids: number[]): Promise<WAFRuleGroup> {
return this.post<WAFRuleGroup>(`/rule-groups/${id}/sites`, { ids });
}
static async listSiteRuleGroups(routeId: number): Promise<WAFSiteRuleGroups> {
return this.get<WAFSiteRuleGroups>(`/sites/${routeId}/rule-groups`);
}
static async updateSiteRuleGroups(
routeId: number,
ids: number[],
): Promise<WAFSiteRuleGroups> {
return this.post<WAFSiteRuleGroups>(`/sites/${routeId}/rule-groups`, {
ids,
});
}
static async listIPGroups(): Promise<WAFIPGroup[]> {
return this.get<WAFIPGroup[]>('/ip-groups');
}
static async getIPGroup(id: number): Promise<WAFIPGroup> {
return this.get<WAFIPGroup>(`/ip-groups/${id}`);
}
static async createIPGroup(payload: WAFIPGroupPayload): Promise<WAFIPGroup> {
return this.post<WAFIPGroup>('/ip-groups', payload);
}
static async updateIPGroup(id: number, payload: WAFIPGroupPayload): Promise<WAFIPGroup> {
return this.post<WAFIPGroup>(`/ip-groups/${id}/update`, payload);
}
static async deleteIPGroup(id: number): Promise<void> {
return this.post<void>(`/ip-groups/${id}/delete`);
}
static async testIPGroup(
payload: WAFIPGroupAutoTestPayload,
): Promise<WAFIPGroupAutoTestResult> {
return this.post<WAFIPGroupAutoTestResult>('/ip-groups/test', payload);
}
static async syncIPGroup(id: number): Promise<WAFIPGroupSyncResult> {
return this.post<WAFIPGroupSyncResult>(`/ip-groups/${id}/sync`);
}
}
@@ -0,0 +1,35 @@
import {OpenFlareBaseService} from './base.service';
import type {
ManagedDomainItem,
ManagedDomainMatchResult,
ManagedDomainMutationPayload,
} from './types';
export class WebsiteService extends OpenFlareBaseService {
protected static override readonly basePath: string = '/api/v1/d/managed-domains';
static async list(): Promise<ManagedDomainItem[]> {
return this.get<ManagedDomainItem[]>('/');
}
static async create(
payload: ManagedDomainMutationPayload,
): Promise<ManagedDomainItem> {
return this.post<ManagedDomainItem>('/', payload);
}
static async update(
id: number,
payload: ManagedDomainMutationPayload,
): Promise<ManagedDomainItem> {
return this.post<ManagedDomainItem>(`/${id}/update`, payload);
}
static async deleteById(id: number): Promise<void> {
return this.post<void>(`/${id}/delete`);
}
static async match(domain: string): Promise<ManagedDomainMatchResult> {
return this.get<ManagedDomainMatchResult>('/match', {domain});
}
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
export * from './types';
export * from './push.service';
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
import {BaseService} from '@/lib/services/core';
import type {
ChannelDefinition,
CreateChannelRequest,
CreatePushEventRequest,
EventMetadata,
ListPushHistoriesRequest,
ListPushHistoriesResponse,
PushChannel,
PushEvent,
TestChannelRequest,
TestPushRequest,
UpdateChannelRequest,
UpdatePushEventRequest
} from './types';
/**
* 通知推送服务类
*/
export class PushService extends BaseService {
protected static readonly basePath = '/api/v1/admin/push';
/**
* 获取通知事件列表
*/
static async listEvents(): Promise<PushEvent[]> {
return this.get<PushEvent[]>('/events');
}
/**
* 获取系统内置通知事件元数据列表
*/
static async listBuiltInEvents(): Promise<EventMetadata[]> {
return this.get<EventMetadata[]>('/events/builtin');
}
/**
* 创建通知事件配置
*/
static async createEvent(data: CreatePushEventRequest): Promise<PushEvent> {
return this.post<PushEvent>('/events', data as unknown as Record<string, unknown>);
}
/**
* 更新指定通知事件配置
*/
static async updateEvent(id: number, data: UpdatePushEventRequest): Promise<void> {
return this.put<void>(`/events/${id}`, data as unknown as Record<string, unknown>);
}
/**
* 删除指定通知事件配置
*/
static async deleteEvent(id: number): Promise<void> {
return this.delete<void>(`/events/${id}`);
}
/**
* 快捷切换事件启用状态
*/
static async toggleEvent(id: number): Promise<boolean> {
return this.post<boolean>(`/events/${id}/toggle`);
}
/**
* 分页查询通知推送历史
*/
static async listHistories(params: ListPushHistoriesRequest): Promise<ListPushHistoriesResponse> {
return this.get<ListPushHistoriesResponse>('/histories', params as unknown as Record<string, unknown>);
}
/**
* 发送测试推送进行联通性校验
*/
static async testPush(data: TestPushRequest): Promise<void> {
return this.post<void>('/test', data as unknown as Record<string, unknown>);
}
/**
* 获取所有消息通道
*/
static async listChannels(): Promise<PushChannel[]> {
return this.get<PushChannel[]>('/channels');
}
/**
* 创建新的消息通道
*/
static async createChannel(data: CreateChannelRequest): Promise<PushChannel> {
return this.post<PushChannel>('/channels', data as unknown as Record<string, unknown>);
}
/**
* 更新指定消息通道
*/
static async updateChannel(id: number, data: UpdateChannelRequest): Promise<PushChannel> {
return this.put<PushChannel>(`/channels/${id}`, data as unknown as Record<string, unknown>);
}
/**
* 删除指定消息通道
*/
static async deleteChannel(id: number): Promise<void> {
return this.delete<void>(`/channels/${id}`);
}
/**
* 测试消息通道连通性
*/
static async testChannel(data: TestChannelRequest): Promise<void> {
return this.post<void>('/channels/test', data as unknown as Record<string, unknown>);
}
/**
* 获取各消息通道的动态表单定义
*/
static async listChannelDefinitions(): Promise<ChannelDefinition[]> {
return this.get<ChannelDefinition[]>('/channels/definitions');
}
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
export interface PushEvent {
id: number;
event_key: string;
name: string;
task_type?: string;
channels: string[];
targets: string[];
template: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
export interface PushHistory {
id: number;
event_key: string;
channel: string;
target: string;
title: string;
content: string;
level: 'INFO' | 'IMPORTANT' | 'CRITICAL';
status: 'success' | 'failed';
error_msg?: string;
created_at: string;
}
export interface PushChannelConfig {
channel: string;
url?: string;
secret?: string;
key?: string;
ext?: Record<string, unknown>;
}
export interface ListPushHistoriesRequest {
page?: number;
page_size?: number;
event_key?: string;
status?: string;
}
export interface ListPushHistoriesResponse {
total: number;
results: PushHistory[];
}
export interface UpdatePushEventRequest {
channels: string[];
targets: string[];
template: string;
enabled: boolean;
}
export interface TestPushRequest {
config: PushChannelConfig;
target?: string;
}
export interface EventMetadata {
key: string;
name: string;
default_template: {
title: string;
content: string;
level: string;
ext?: Record<string, unknown>;
};
description: string;
}
export interface CreatePushEventRequest {
event_key?: string;
task_type?: string;
channels: string[];
targets?: string[];
template?: string;
enabled: boolean;
}
export interface PushChannel {
id: number;
name: string;
description?: string;
type: string;
token?: string;
url: string;
other: string;
enabled: boolean;
created_at?: string;
updated_at?: string;
}
export interface CreateChannelRequest {
name: string;
description?: string;
type: string;
token?: string;
url: string;
other: string;
enabled?: boolean;
}
export interface UpdateChannelRequest {
description?: string;
type: string;
token?: string;
url: string;
other: string;
enabled?: boolean;
}
export interface TestChannelRequest {
name?: string;
type?: string;
url?: string;
other?: string;
target?: string;
}
export interface ChannelFieldDef {
key: string;
label: string;
type: 'text' | 'password' | 'textarea';
required: boolean;
placeholder?: string;
description?: string;
}
export interface ChannelDefinition {
type: string;
name: string;
description: string;
fields: ChannelFieldDef[];
}
@@ -0,0 +1,40 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {BaseService} from '../core/base.service';
import type {FileStatsResponse, ListUploadsResponse} from './types';
export class AdminUploadService extends BaseService {
protected static readonly basePath = '/api/v1/admin/uploads';
static async listUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string,
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (type) params.type = type;
if (extension) params.extension = extension;
return this.get<ListUploadsResponse>('', params);
}
static async getFileStats(): Promise<FileStatsResponse> {
return this.get<FileStatsResponse>('/stats');
}
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`);
}
static getDownloadUrl(id: string): string {
return `${this.basePath}/download/${id}`;
}
static async batchDownload(ids: string[]): Promise<Blob> {
return this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig);
}
}
+5
View File
@@ -0,0 +1,5 @@
export { UploadService } from './upload.service';
export { AdminUploadService } from './admin-upload.service';
export { formatFileSize, getFileUrl } from './utils';
export type { ImageQuality } from './utils';
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse } from './types';
+91
View File
@@ -0,0 +1,91 @@
/**
* 文件上传元数据
*/
export interface UploadMetadata {
width?: number
height?: number
duration?: number
original_mime?: string
user_agent?: string
client_ip?: string
bucket?: string
extra?: Record<string, unknown>
}
/**
* 上传记录
*/
export interface Upload {
id: string
user_id: string
file_name: string
file_path: string
file_size: number
mime_type: string
extension: string
hash: string
type: string
status: string
access_mode: number
metadata: UploadMetadata
created_at: string
updated_at: string
}
/**
* 上传接口响应(原 UploadImageResponse 兼容)
*/
export interface UploadImageResponse {
/** 上传记录 ID */
id: string
}
/**
* 文件列表查询参数
*/
export interface ListUploadsQuery {
page?: number
page_size?: number
type?: string
extension?: string
keyword?: string
}
/**
* 文件列表分页响应
*/
export interface ListUploadsResponse {
total: number
page: number
page_size: number
items: Upload[]
}
/**
* 近 7 天新增文件趋势项
*/
export interface TrendItem {
date: string
count: number
size: number
}
/**
* 文件分类/类型统计项
*/
export interface DistributionItem {
name: string
count: number
size: number
}
/**
* 文件统计信息响应
*/
export interface FileStatsResponse {
total_count: number
total_size: number
trend: TrendItem[]
categories: DistributionItem[]
types: DistributionItem[]
}
@@ -0,0 +1,78 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {BaseService} from '../core/base.service';
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types';
export class UploadService extends BaseService {
protected static readonly basePath = '/api/v1/upload';
static async uploadFile(
file: File,
type: string = 'generic',
metadata?: Record<string, unknown>,
accessMode?: number,
): Promise<Upload> {
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
if (metadata) {
formData.append('metadata', JSON.stringify(metadata));
}
if (accessMode !== undefined) {
formData.append('access_mode', String(accessMode));
}
return this.post<Upload>('', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
} as InternalAxiosRequestConfig);
}
static async uploadBase64Image(
base64: string,
type: string = 'generic',
filename: string = 'image.png',
accessMode?: number,
): Promise<UploadImageResponse> {
const response = await fetch(base64);
const blob = await response.blob();
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png';
const file = new File([blob], filename, { type: mimeType });
const result = await this.uploadFile(file, type, undefined, accessMode);
return { id: result.id };
}
static async listMyUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string,
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (type) params.type = type;
if (extension) params.extension = extension;
return this.get<ListUploadsResponse>('/my', params);
}
static async deleteMyFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`);
}
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
return this.put<Upload>(`/${id}`, {
file_name: fileName,
access_mode: accessMode,
});
}
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
return this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig);
}
static getDownloadUrl(id: string): string {
return `${this.basePath}/download/${id}`;
}
}
+18
View File
@@ -0,0 +1,18 @@
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin';
export function getFileUrl(
id: string | number | null | undefined,
quality: ImageQuality = 'origin',
): string | null {
if (!id) return null;
if (quality === 'origin') return `/f/${id}`;
return `/f/${id}?quality=${quality}`;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}
+9
View File
@@ -0,0 +1,9 @@
/**
* 用户服务模块
*
* @description
* 提供用户个人设置相关的功能
*/
export { UserService } from './user.service';
export type { AccessToken, CreateTokenResponse } from './user.service';
+2
View File
@@ -0,0 +1,2 @@
@@ -0,0 +1,56 @@
import {BaseService} from '../core/base.service';
export interface AccessToken {
id: number;
user_id: number;
name: string;
masked_token: string;
is_admin: boolean;
created_at: string;
updated_at: string;
}
export interface CreateTokenResponse {
token: string;
record: AccessToken;
}
/**
* 用户服务
* 处理用户个人设置相关的 API 请求
*/
export class UserService extends BaseService {
protected static readonly basePath = '/api/v1/user';
/**
* 获取当前用户的 AccessToken 列表
*/
static async getAccessTokens(): Promise<AccessToken[]> {
return this.get<AccessToken[]>('/access-tokens');
}
/**
* 创建一个新的 AccessToken
* @param name - 令牌名称
* @param isAdmin - 是否赋予管理员权限(默认 false)
*/
static async createAccessToken(name: string, isAdmin = false): Promise<CreateTokenResponse> {
return this.post<CreateTokenResponse>('/access-tokens', { name, is_admin: isAdmin });
}
/**
* 删除一个 AccessToken
* @param id - 令牌 ID
*/
static async deleteAccessToken(id: number): Promise<string> {
return this.delete<string>(`/access-tokens/${id}`);
}
/**
* 轮换一个 AccessToken 密钥
* @param id - 令牌 ID
*/
static async rotateAccessToken(id: number): Promise<CreateTokenResponse> {
return this.post<CreateTokenResponse>(`/access-tokens/${id}/rotate`);
}
}