refactor progress
This commit is contained in:
49
packages/admin/src/api/AdminApiKeys.tsx
Normal file
49
packages/admin/src/api/AdminApiKeys.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {CreateAdminApiKeyResponse, ListAdminApiKeyResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function createApiKey(
|
||||
config: Config,
|
||||
session: Session,
|
||||
name: string,
|
||||
acls: Array<string>,
|
||||
): Promise<ApiResult<CreateAdminApiKeyResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<CreateAdminApiKeyResponse>('/admin/api-keys', {name, acls});
|
||||
}
|
||||
|
||||
export async function listApiKeys(
|
||||
config: Config,
|
||||
session: Session,
|
||||
): Promise<ApiResult<Array<ListAdminApiKeyResponse>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<Array<ListAdminApiKeyResponse>>('/admin/api-keys');
|
||||
}
|
||||
|
||||
export async function revokeApiKey(config: Config, session: Session, key_id: string): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.deleteVoid(`/admin/api-keys/${key_id}`);
|
||||
}
|
||||
86
packages/admin/src/api/Archives.tsx
Normal file
86
packages/admin/src/api/Archives.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
AdminArchiveResponseSchema,
|
||||
DownloadUrlResponseSchema,
|
||||
ListArchivesResponseSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export type Archive = z.infer<typeof AdminArchiveResponseSchema>;
|
||||
export type ListArchivesResponse = z.infer<typeof ListArchivesResponseSchema>;
|
||||
export type ArchiveDownloadUrlResponse = z.infer<typeof DownloadUrlResponseSchema>;
|
||||
|
||||
export async function triggerUserArchive(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
reason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/archives/user', {user_id: userId}, reason);
|
||||
}
|
||||
|
||||
export async function triggerGuildArchive(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
reason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/archives/guild', {guild_id: guildId}, reason);
|
||||
}
|
||||
|
||||
export async function listArchives(
|
||||
config: Config,
|
||||
session: Session,
|
||||
subjectType: string,
|
||||
subjectId?: string,
|
||||
includeExpired: boolean = false,
|
||||
requestedBy?: string,
|
||||
): Promise<ApiResult<ListArchivesResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
subject_type: subjectType,
|
||||
include_expired: includeExpired,
|
||||
...(subjectId ? {subject_id: subjectId} : {}),
|
||||
...(requestedBy ? {requested_by: requestedBy} : {}),
|
||||
};
|
||||
|
||||
return client.post<ListArchivesResponse>('/admin/archives/list', body);
|
||||
}
|
||||
|
||||
export async function getArchiveDownloadUrl(
|
||||
config: Config,
|
||||
session: Session,
|
||||
subjectType: string,
|
||||
subjectId: string,
|
||||
archiveId: string,
|
||||
): Promise<ApiResult<ArchiveDownloadUrlResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<ArchiveDownloadUrlResponse>(`/admin/archives/${subjectType}/${subjectId}/${archiveId}/download`);
|
||||
}
|
||||
36
packages/admin/src/api/Assets.tsx
Normal file
36
packages/admin/src/api/Assets.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {PurgeGuildAssetsResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function purgeAssets(
|
||||
config: Config,
|
||||
session: Session,
|
||||
ids: Array<string>,
|
||||
reason?: string,
|
||||
): Promise<ApiResult<PurgeGuildAssetsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<PurgeGuildAssetsResponse>('/admin/assets/purge', {ids}, reason);
|
||||
}
|
||||
55
packages/admin/src/api/Audit.tsx
Normal file
55
packages/admin/src/api/Audit.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {AuditLogsListResponseSchema} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type AuditLogsListResponse = z.infer<typeof AuditLogsListResponseSchema>;
|
||||
|
||||
export interface SearchAuditLogsParams {
|
||||
query: string | undefined;
|
||||
admin_user_id: string | undefined;
|
||||
target_id: string | undefined;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export async function searchAuditLogs(
|
||||
config: Config,
|
||||
session: Session,
|
||||
params: SearchAuditLogsParams,
|
||||
): Promise<ApiResult<AuditLogsListResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
...(params.query && params.query !== '' ? {query: params.query} : {}),
|
||||
...(params.admin_user_id && params.admin_user_id !== '' ? {admin_user_id: params.admin_user_id} : {}),
|
||||
...(params.target_id && params.target_id !== '' ? {target_id: params.target_id} : {}),
|
||||
};
|
||||
|
||||
return client.post<AuditLogsListResponse>('/admin/audit-logs/search', body);
|
||||
}
|
||||
147
packages/admin/src/api/Bans.tsx
Normal file
147
packages/admin/src/api/Bans.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
BanCheckResponseSchema,
|
||||
ListEmailBansResponseSchema,
|
||||
ListIpBansResponseSchema,
|
||||
ListPhoneBansResponseSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export type BanCheckResponse = z.infer<typeof BanCheckResponseSchema>;
|
||||
export type ListIpBansResponse = z.infer<typeof ListIpBansResponseSchema>;
|
||||
export type ListEmailBansResponse = z.infer<typeof ListEmailBansResponseSchema>;
|
||||
export type ListPhoneBansResponse = z.infer<typeof ListPhoneBansResponseSchema>;
|
||||
|
||||
export async function banEmail(
|
||||
config: Config,
|
||||
session: Session,
|
||||
email: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/bans/email/add', {email}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function unbanEmail(
|
||||
config: Config,
|
||||
session: Session,
|
||||
email: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/bans/email/remove', {email}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function checkEmailBan(
|
||||
config: Config,
|
||||
session: Session,
|
||||
email: string,
|
||||
): Promise<ApiResult<BanCheckResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<BanCheckResponse>('/admin/bans/email/check', {email});
|
||||
}
|
||||
|
||||
export async function banIp(
|
||||
config: Config,
|
||||
session: Session,
|
||||
ip: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/bans/ip/add', {ip}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function unbanIp(
|
||||
config: Config,
|
||||
session: Session,
|
||||
ip: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/bans/ip/remove', {ip}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function checkIpBan(config: Config, session: Session, ip: string): Promise<ApiResult<BanCheckResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<BanCheckResponse>('/admin/bans/ip/check', {ip});
|
||||
}
|
||||
|
||||
export async function banPhone(
|
||||
config: Config,
|
||||
session: Session,
|
||||
phone: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/bans/phone/add', {phone}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function unbanPhone(
|
||||
config: Config,
|
||||
session: Session,
|
||||
phone: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/bans/phone/remove', {phone}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function checkPhoneBan(
|
||||
config: Config,
|
||||
session: Session,
|
||||
phone: string,
|
||||
): Promise<ApiResult<BanCheckResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<BanCheckResponse>('/admin/bans/phone/check', {phone});
|
||||
}
|
||||
|
||||
export async function listIpBans(
|
||||
config: Config,
|
||||
session: Session,
|
||||
limit: number = 200,
|
||||
): Promise<ApiResult<ListIpBansResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<ListIpBansResponse>('/admin/bans/ip/list', {limit});
|
||||
}
|
||||
|
||||
export async function listEmailBans(
|
||||
config: Config,
|
||||
session: Session,
|
||||
limit: number = 200,
|
||||
): Promise<ApiResult<ListEmailBansResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<ListEmailBansResponse>('/admin/bans/email/list', {limit});
|
||||
}
|
||||
|
||||
export async function listPhoneBans(
|
||||
config: Config,
|
||||
session: Session,
|
||||
limit: number = 200,
|
||||
): Promise<ApiResult<ListPhoneBansResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<ListPhoneBansResponse>('/admin/bans/phone/list', {limit});
|
||||
}
|
||||
107
packages/admin/src/api/Bulk.tsx
Normal file
107
packages/admin/src/api/Bulk.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {BulkOperationResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type BulkOperationResponseType = z.infer<typeof BulkOperationResponse>;
|
||||
|
||||
export async function bulkUpdateUserFlags(
|
||||
config: Config,
|
||||
session: Session,
|
||||
user_ids: Array<string>,
|
||||
add_flags: Array<string>,
|
||||
remove_flags: Array<string>,
|
||||
audit_log_reason?: string,
|
||||
): Promise<ApiResult<BulkOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<BulkOperationResponseType>(
|
||||
'/admin/bulk/update-user-flags',
|
||||
{
|
||||
user_ids,
|
||||
add_flags,
|
||||
remove_flags,
|
||||
},
|
||||
audit_log_reason,
|
||||
);
|
||||
}
|
||||
|
||||
export async function bulkUpdateGuildFeatures(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guild_ids: Array<string>,
|
||||
add_features: Array<string>,
|
||||
remove_features: Array<string>,
|
||||
audit_log_reason?: string,
|
||||
): Promise<ApiResult<BulkOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<BulkOperationResponseType>(
|
||||
'/admin/bulk/update-guild-features',
|
||||
{
|
||||
guild_ids,
|
||||
add_features,
|
||||
remove_features,
|
||||
},
|
||||
audit_log_reason,
|
||||
);
|
||||
}
|
||||
|
||||
export async function bulkAddGuildMembers(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guild_id: string,
|
||||
user_ids: Array<string>,
|
||||
audit_log_reason?: string,
|
||||
): Promise<ApiResult<BulkOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<BulkOperationResponseType>(
|
||||
'/admin/bulk/add-guild-members',
|
||||
{
|
||||
guild_id,
|
||||
user_ids,
|
||||
},
|
||||
audit_log_reason,
|
||||
);
|
||||
}
|
||||
|
||||
export async function bulkScheduleUserDeletion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
user_ids: Array<string>,
|
||||
reason_code: number,
|
||||
days_until_deletion: number,
|
||||
public_reason?: string,
|
||||
audit_log_reason?: string,
|
||||
): Promise<ApiResult<BulkOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_ids,
|
||||
reason_code,
|
||||
days_until_deletion,
|
||||
...(public_reason ? {public_reason} : {}),
|
||||
};
|
||||
return client.post<BulkOperationResponseType>('/admin/bulk/schedule-user-deletion', body, audit_log_reason);
|
||||
}
|
||||
216
packages/admin/src/api/Client.tsx
Normal file
216
packages/admin/src/api/Client.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {type ApiError, parseApiResponse} from '@fluxer/admin/src/api/Errors';
|
||||
import type {JsonValue} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig} from '@fluxer/admin/src/types/Config';
|
||||
import {buildEndpointUrl, validateOutboundEndpointUrl} from '@fluxer/hono/src/security/OutboundEndpoint';
|
||||
|
||||
export type ApiResult<T> = {ok: true; data: T} | {ok: false; error: ApiError};
|
||||
export interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
path: string;
|
||||
body?: JsonValue | string;
|
||||
queryParams?: Record<string, string | number | boolean | undefined | null>;
|
||||
auditLogReason?: string;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private session: Session;
|
||||
private apiEndpointUrl: URL;
|
||||
|
||||
constructor(config: AdminConfig, session: Session) {
|
||||
this.session = session;
|
||||
this.apiEndpointUrl = validateOutboundEndpointUrl(config.apiEndpoint, {
|
||||
name: 'admin.apiEndpoint',
|
||||
allowHttp: config.env !== 'production',
|
||||
allowLocalhost: config.env !== 'production',
|
||||
allowPrivateIpLiterals: config.env !== 'production',
|
||||
});
|
||||
}
|
||||
|
||||
private buildHeaders(auditLogReason?: string): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${this.session.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (auditLogReason) {
|
||||
headers['X-Audit-Log-Reason'] = auditLogReason;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private buildUrl(path: string, queryParams?: Record<string, string | number | boolean | undefined | null>): string {
|
||||
const baseUrl = buildEndpointUrl(this.apiEndpointUrl, path);
|
||||
|
||||
if (!queryParams) {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
}
|
||||
|
||||
async request<T>(options: RequestOptions): Promise<ApiResult<T>> {
|
||||
try {
|
||||
const url = this.buildUrl(options.path, options.queryParams);
|
||||
const headers = this.buildHeaders(options.auditLogReason);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: options.method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (options.body !== undefined && options.method !== 'GET') {
|
||||
if (typeof options.body === 'string') {
|
||||
fetchOptions.body = options.body;
|
||||
} else {
|
||||
fetchOptions.body = JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (response.status === 204) {
|
||||
return {ok: true, data: undefined as T};
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const contentLength = response.headers.get('content-length');
|
||||
if (contentLength === '0') {
|
||||
return {ok: true, data: undefined as T};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = (await response.json()) as T;
|
||||
return {ok: true, data};
|
||||
} catch {
|
||||
return {ok: true, data: undefined as T};
|
||||
}
|
||||
}
|
||||
|
||||
return parseApiResponse<T>(response);
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {type: 'networkError', message: (e as Error).message},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(
|
||||
path: string,
|
||||
queryParams?: Record<string, string | number | boolean | undefined | null>,
|
||||
): Promise<ApiResult<T>> {
|
||||
return this.request<T>({
|
||||
method: 'GET',
|
||||
path,
|
||||
...(queryParams !== undefined ? {queryParams} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async post<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
|
||||
return this.request<T>({
|
||||
method: 'POST',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async postVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
|
||||
return this.request<void>({
|
||||
method: 'POST',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async patch<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
|
||||
return this.request<T>({
|
||||
method: 'PATCH',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async patchVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
|
||||
return this.request<void>({
|
||||
method: 'PATCH',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
|
||||
return this.request<T>({
|
||||
method: 'DELETE',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
|
||||
return this.request<void>({
|
||||
method: 'DELETE',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async put<T>(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<T>> {
|
||||
return this.request<T>({
|
||||
method: 'PUT',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async putVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise<ApiResult<void>> {
|
||||
return this.request<void>({
|
||||
method: 'PUT',
|
||||
path,
|
||||
...(body !== undefined ? {body} : {}),
|
||||
...(auditLogReason !== undefined ? {auditLogReason} : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiClient(config: AdminConfig, session: Session): ApiClient {
|
||||
return new ApiClient(config, session);
|
||||
}
|
||||
43
packages/admin/src/api/Codes.tsx
Normal file
43
packages/admin/src/api/Codes.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {CodesResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type CodesResponseType = z.infer<typeof CodesResponse>;
|
||||
|
||||
export async function generateGiftCodes(
|
||||
config: Config,
|
||||
session: Session,
|
||||
count: number,
|
||||
product_type: string,
|
||||
): Promise<ApiResult<Array<string>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<CodesResponseType>('/admin/codes/gift', {count, product_type});
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data.codes};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
77
packages/admin/src/api/Discovery.tsx
Normal file
77
packages/admin/src/api/Discovery.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {DiscoveryApplicationResponse} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type DiscoveryApplicationResponseType = z.infer<typeof DiscoveryApplicationResponse>;
|
||||
|
||||
export async function listDiscoveryApplications(
|
||||
config: Config,
|
||||
session: Session,
|
||||
status: string,
|
||||
limit?: number,
|
||||
): Promise<ApiResult<Array<DiscoveryApplicationResponseType>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.get<Array<DiscoveryApplicationResponseType>>('/admin/discovery/applications', {
|
||||
status,
|
||||
limit: limit ?? 25,
|
||||
});
|
||||
}
|
||||
|
||||
export async function approveDiscoveryApplication(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
reason?: string,
|
||||
): Promise<ApiResult<DiscoveryApplicationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<DiscoveryApplicationResponseType>(
|
||||
`/admin/discovery/applications/${guildId}/approve`,
|
||||
reason ? {reason} : {},
|
||||
);
|
||||
}
|
||||
|
||||
export async function rejectDiscoveryApplication(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
reason: string,
|
||||
): Promise<ApiResult<DiscoveryApplicationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<DiscoveryApplicationResponseType>(`/admin/discovery/applications/${guildId}/reject`, {
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeFromDiscovery(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
reason: string,
|
||||
): Promise<ApiResult<DiscoveryApplicationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<DiscoveryApplicationResponseType>(`/admin/discovery/guilds/${guildId}/remove`, {reason});
|
||||
}
|
||||
325
packages/admin/src/api/Errors.tsx
Normal file
325
packages/admin/src/api/Errors.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {JsonValue} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import {isJsonObject, parseJson} from '@fluxer/admin/src/api/JsonTypes';
|
||||
|
||||
export interface ValidationError {
|
||||
path: string;
|
||||
message: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export type ApiError =
|
||||
| {type: 'unauthorized'}
|
||||
| {type: 'forbidden'; code: string; message: string; details: Record<string, string>}
|
||||
| {type: 'notFound'; code: string; message: string}
|
||||
| {type: 'badRequest'; code: string; message: string; errors: Array<ValidationError>}
|
||||
| {type: 'rateLimited'; code: string; message: string; retryAfter: number; isGlobal: boolean}
|
||||
| {type: 'clientError'; status: number; code: string; message: string}
|
||||
| {type: 'serverError'; status: number; code: string | null; message: string}
|
||||
| {type: 'networkError'; message: string}
|
||||
| {type: 'parseError'; body: string; parseError: string};
|
||||
|
||||
export function isUnauthorized(error: ApiError): error is {type: 'unauthorized'} {
|
||||
return error.type === 'unauthorized';
|
||||
}
|
||||
|
||||
export function isForbidden(
|
||||
error: ApiError,
|
||||
): error is {type: 'forbidden'; code: string; message: string; details: Record<string, string>} {
|
||||
return error.type === 'forbidden';
|
||||
}
|
||||
|
||||
export function isNotFound(error: ApiError): error is {type: 'notFound'; code: string; message: string} {
|
||||
return error.type === 'notFound';
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: ApiError): string {
|
||||
switch (error.type) {
|
||||
case 'unauthorized':
|
||||
return 'Authentication required';
|
||||
case 'forbidden':
|
||||
return error.message;
|
||||
case 'notFound':
|
||||
return error.message;
|
||||
case 'badRequest':
|
||||
return error.message;
|
||||
case 'rateLimited':
|
||||
return error.message;
|
||||
case 'clientError':
|
||||
return error.message;
|
||||
case 'serverError':
|
||||
return error.message;
|
||||
case 'networkError':
|
||||
return error.message;
|
||||
case 'parseError':
|
||||
return 'Failed to parse API response';
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorCode(error: ApiError): string | null {
|
||||
switch (error.type) {
|
||||
case 'forbidden':
|
||||
return error.code;
|
||||
case 'notFound':
|
||||
return error.code;
|
||||
case 'badRequest':
|
||||
return error.code;
|
||||
case 'rateLimited':
|
||||
return error.code;
|
||||
case 'clientError':
|
||||
return error.code;
|
||||
case 'serverError':
|
||||
return error.code;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorDisplayString(error: ApiError): string {
|
||||
switch (error.type) {
|
||||
case 'badRequest': {
|
||||
const base = `${error.message} (${error.code})`;
|
||||
if (error.errors.length === 0) return base;
|
||||
const errorDetails = error.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
||||
return `${base}\n${errorDetails}`;
|
||||
}
|
||||
case 'rateLimited':
|
||||
return `${error.message} (Retry after ${Math.round(error.retryAfter)}s)`;
|
||||
case 'forbidden':
|
||||
return error.message;
|
||||
case 'notFound':
|
||||
return error.message;
|
||||
case 'clientError':
|
||||
return error.message;
|
||||
case 'serverError':
|
||||
return error.code ? `${error.message} (Error code: ${error.code})` : error.message;
|
||||
default:
|
||||
return getErrorMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function isRetryable(error: ApiError): boolean {
|
||||
return error.type === 'rateLimited' || error.type === 'serverError' || error.type === 'networkError';
|
||||
}
|
||||
|
||||
export function getRetryAfterSeconds(error: ApiError): number | null {
|
||||
if (error.type === 'rateLimited') {
|
||||
return error.retryAfter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getValidationErrors(error: ApiError): Array<ValidationError> {
|
||||
if (error.type === 'badRequest') {
|
||||
return error.errors;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getErrorTitle(error: ApiError): string {
|
||||
switch (error.type) {
|
||||
case 'unauthorized':
|
||||
return 'Authentication Required';
|
||||
case 'forbidden':
|
||||
return 'Permission Denied';
|
||||
case 'notFound':
|
||||
return 'Not Found';
|
||||
case 'badRequest':
|
||||
return 'Validation Error';
|
||||
case 'rateLimited':
|
||||
return 'Rate Limited';
|
||||
case 'clientError':
|
||||
return 'Client Error';
|
||||
case 'serverError':
|
||||
return 'Server Error';
|
||||
case 'networkError':
|
||||
return 'Network Error';
|
||||
case 'parseError':
|
||||
return 'Response Error';
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorSubtitle(error: ApiError): string {
|
||||
switch (error.type) {
|
||||
case 'unauthorized':
|
||||
return 'Your session has expired. Please log in again.';
|
||||
case 'forbidden':
|
||||
return "You don't have permission to perform this action.";
|
||||
case 'notFound':
|
||||
return 'The requested resource could not be found.';
|
||||
case 'badRequest':
|
||||
return 'Please check your input and try again.';
|
||||
case 'rateLimited':
|
||||
return "You've made too many requests. Please wait before trying again.";
|
||||
case 'clientError':
|
||||
return 'The request was invalid or malformed.';
|
||||
case 'serverError':
|
||||
return 'An internal server error occurred. Please try again later.';
|
||||
case 'networkError':
|
||||
return 'Could not connect to the server. Please check your connection.';
|
||||
case 'parseError':
|
||||
return 'The server returned an invalid response.';
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorDetails(error: ApiError): Array<string> {
|
||||
switch (error.type) {
|
||||
case 'forbidden':
|
||||
return [`Permission denied (Error code: ${error.code})`];
|
||||
case 'notFound':
|
||||
return [`Resource not found (Error code: ${error.code})`];
|
||||
case 'badRequest': {
|
||||
const codeDetail = `Validation failed (Error code: ${error.code})`;
|
||||
const validationDetails = error.errors.map((e) => `${e.path}: ${e.message}`);
|
||||
return [codeDetail, ...validationDetails];
|
||||
}
|
||||
case 'rateLimited':
|
||||
return [
|
||||
`Rate limit exceeded (Error code: ${error.code})`,
|
||||
`Retry after: ${Math.round(error.retryAfter)} seconds`,
|
||||
error.isGlobal ? 'This is a global rate limit' : 'This is an endpoint-specific rate limit',
|
||||
];
|
||||
case 'clientError':
|
||||
return [`HTTP Status: ${error.status}`, `Client error (Error code: ${error.code})`];
|
||||
case 'serverError': {
|
||||
const statusDetail = `HTTP Status: ${error.status}`;
|
||||
return error.code ? [statusDetail, `Error code: ${error.code}`] : [statusDetail];
|
||||
}
|
||||
case 'parseError': {
|
||||
const preview = error.body.slice(0, 200).replace(/\n/g, ' ');
|
||||
return ['Could not parse server response', `Response preview: ${preview}`];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseApiResponse<T>(
|
||||
response: Response,
|
||||
): Promise<{ok: true; data: T} | {ok: false; error: ApiError}> {
|
||||
if (response.ok) {
|
||||
try {
|
||||
const data = (await response.json()) as T;
|
||||
return {ok: true, data};
|
||||
} catch (e) {
|
||||
const body = await response.text().catch(() => '');
|
||||
return {ok: false, error: {type: 'parseError', body, parseError: String(e)}};
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
return {ok: false, error: {type: 'unauthorized'}};
|
||||
}
|
||||
|
||||
const bodyText = await response.text().catch(() => '');
|
||||
const parsed = parseJson(bodyText);
|
||||
const body = isJsonObject(parsed) ? parsed : {};
|
||||
|
||||
if (response.status === 403) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'forbidden',
|
||||
code: typeof body['code'] === 'string' ? body['code'] : 'FORBIDDEN',
|
||||
message: typeof body['message'] === 'string' ? body['message'] : bodyText || 'Forbidden',
|
||||
details: Object.fromEntries(
|
||||
Object.entries(body).flatMap(([k, v]) => (typeof v === 'string' ? [[k, v] as const] : [])),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'notFound',
|
||||
code: typeof body['code'] === 'string' ? body['code'] : 'NOT_FOUND',
|
||||
message: typeof body['message'] === 'string' ? body['message'] : 'The requested resource was not found.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
const errors: Array<ValidationError> = [];
|
||||
const errorList = Array.isArray(body['errors']) ? body['errors'] : [];
|
||||
for (const e of errorList) {
|
||||
if (typeof e !== 'object' || e === null || Array.isArray(e)) {
|
||||
continue;
|
||||
}
|
||||
const errObj = e as {[key: string]: JsonValue};
|
||||
errors.push({
|
||||
path: typeof errObj['path'] === 'string' ? errObj['path'] : '',
|
||||
message: typeof errObj['message'] === 'string' ? errObj['message'] : '',
|
||||
code: typeof errObj['code'] === 'string' ? errObj['code'] : '',
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'badRequest',
|
||||
code: typeof body['code'] === 'string' ? body['code'] : 'BAD_REQUEST',
|
||||
message: typeof body['message'] === 'string' ? body['message'] : 'Bad request',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 429) {
|
||||
const retryAfterHeader = response.headers.get('retry-after');
|
||||
const retryAfter = retryAfterHeader ? parseFloat(retryAfterHeader) : 60;
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'rateLimited',
|
||||
code: (body['code'] as string) ?? 'RATE_LIMITED',
|
||||
message: (body['message'] as string) ?? 'Rate limited',
|
||||
retryAfter,
|
||||
isGlobal: (body['global'] as boolean) ?? false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status >= 500) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'serverError',
|
||||
status: response.status,
|
||||
code: (body['code'] as string) ?? null,
|
||||
message: (body['message'] as string) ?? (bodyText || 'Server error'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'clientError',
|
||||
status: response.status,
|
||||
code: (body['code'] as string) ?? 'CLIENT_ERROR',
|
||||
message: (body['message'] as string) ?? (bodyText || `HTTP ${response.status}`),
|
||||
},
|
||||
};
|
||||
}
|
||||
44
packages/admin/src/api/GuildAssets.tsx
Normal file
44
packages/admin/src/api/GuildAssets.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {ListGuildEmojisResponse, ListGuildStickersResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function listGuildEmojis(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guild_id: string,
|
||||
): Promise<ApiResult<ListGuildEmojisResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<ListGuildEmojisResponse>(`/admin/guilds/${guild_id}/emojis`);
|
||||
}
|
||||
|
||||
export async function listGuildStickers(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guild_id: string,
|
||||
): Promise<ApiResult<ListGuildStickersResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<ListGuildStickersResponse>(`/admin/guilds/${guild_id}/stickers`);
|
||||
}
|
||||
212
packages/admin/src/api/Guilds.tsx
Normal file
212
packages/admin/src/api/Guilds.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
ListGuildMembersResponse,
|
||||
LookupGuildResponse,
|
||||
SearchGuildsResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export type GuildLookupResult = NonNullable<z.infer<typeof LookupGuildResponse>['guild']>;
|
||||
export type GuildChannel = GuildLookupResult['channels'][number];
|
||||
export type GuildRole = GuildLookupResult['roles'][number];
|
||||
|
||||
export interface UpdateGuildSettingsOptions {
|
||||
verification_level?: number | undefined;
|
||||
mfa_level?: number | undefined;
|
||||
nsfw_level?: number | undefined;
|
||||
explicit_content_filter?: number | undefined;
|
||||
default_message_notifications?: number | undefined;
|
||||
disabled_operations?: number | undefined;
|
||||
}
|
||||
|
||||
export async function lookupGuild(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
): Promise<ApiResult<GuildLookupResult | null>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<z.infer<typeof LookupGuildResponse>>('/admin/guilds/lookup', {guild_id: guildId});
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data.guild};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function clearGuildFields(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
fields: Array<string>,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/clear-fields', {guild_id: guildId, fields});
|
||||
}
|
||||
|
||||
export async function updateGuildFeatures(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
addFeatures: Array<string>,
|
||||
removeFeatures: Array<string>,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/update-features', {
|
||||
guild_id: guildId,
|
||||
add_features: addFeatures,
|
||||
remove_features: removeFeatures,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGuildSettings(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
options: UpdateGuildSettingsOptions,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
guild_id: guildId,
|
||||
...(options.verification_level !== undefined ? {verification_level: options.verification_level} : {}),
|
||||
...(options.mfa_level !== undefined ? {mfa_level: options.mfa_level} : {}),
|
||||
...(options.nsfw_level !== undefined ? {nsfw_level: options.nsfw_level} : {}),
|
||||
...(options.explicit_content_filter !== undefined
|
||||
? {explicit_content_filter: options.explicit_content_filter}
|
||||
: {}),
|
||||
...(options.default_message_notifications !== undefined
|
||||
? {default_message_notifications: options.default_message_notifications}
|
||||
: {}),
|
||||
...(options.disabled_operations !== undefined ? {disabled_operations: options.disabled_operations} : {}),
|
||||
};
|
||||
|
||||
return client.postVoid('/admin/guilds/update-settings', body);
|
||||
}
|
||||
|
||||
export async function updateGuildName(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
name: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/update-name', {guild_id: guildId, name});
|
||||
}
|
||||
|
||||
export async function updateGuildVanity(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
vanityUrlCode?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
guild_id: guildId,
|
||||
...(vanityUrlCode !== undefined ? {vanity_url_code: vanityUrlCode} : {}),
|
||||
};
|
||||
return client.postVoid('/admin/guilds/update-vanity', body);
|
||||
}
|
||||
|
||||
export async function transferGuildOwnership(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
newOwnerId: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/transfer-ownership', {guild_id: guildId, new_owner_id: newOwnerId});
|
||||
}
|
||||
|
||||
export async function reloadGuild(config: Config, session: Session, guildId: string): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/reload', {guild_id: guildId});
|
||||
}
|
||||
|
||||
export async function shutdownGuild(config: Config, session: Session, guildId: string): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/shutdown', {guild_id: guildId});
|
||||
}
|
||||
|
||||
export async function deleteGuild(config: Config, session: Session, guildId: string): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/delete', {guild_id: guildId});
|
||||
}
|
||||
|
||||
export async function forceAddUserToGuild(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
guildId: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/force-add-user', {user_id: userId, guild_id: guildId});
|
||||
}
|
||||
|
||||
export async function searchGuilds(
|
||||
config: Config,
|
||||
session: Session,
|
||||
query: string,
|
||||
limit: number = 25,
|
||||
offset: number = 0,
|
||||
): Promise<ApiResult<z.infer<typeof SearchGuildsResponse>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<z.infer<typeof SearchGuildsResponse>>('/admin/guilds/search', {query, limit, offset});
|
||||
}
|
||||
|
||||
export async function listGuildMembers(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
): Promise<ApiResult<z.infer<typeof ListGuildMembersResponse>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<z.infer<typeof ListGuildMembersResponse>>('/admin/guilds/list-members', {
|
||||
guild_id: guildId,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
}
|
||||
|
||||
export async function banGuildMember(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/ban-member', {guild_id: guildId, user_id: userId});
|
||||
}
|
||||
|
||||
export async function kickGuildMember(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/guilds/kick-member', {guild_id: guildId, user_id: userId});
|
||||
}
|
||||
79
packages/admin/src/api/InstanceConfig.tsx
Normal file
79
packages/admin/src/api/InstanceConfig.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
InstanceConfigResponse,
|
||||
InstanceConfigUpdateRequest,
|
||||
SnowflakeReservationEntry,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function getInstanceConfig(config: Config, session: Session): Promise<ApiResult<InstanceConfigResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<InstanceConfigResponse>('/admin/instance-config/get', {});
|
||||
}
|
||||
|
||||
export async function updateInstanceConfig(
|
||||
config: Config,
|
||||
session: Session,
|
||||
update: InstanceConfigUpdateRequest,
|
||||
): Promise<ApiResult<InstanceConfigResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<InstanceConfigResponse>('/admin/instance-config/update', update as JsonObject);
|
||||
}
|
||||
|
||||
export async function listSnowflakeReservations(
|
||||
config: Config,
|
||||
session: Session,
|
||||
): Promise<ApiResult<Array<SnowflakeReservationEntry>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<{reservations: Array<SnowflakeReservationEntry>}>(
|
||||
'/admin/snowflake-reservations/list',
|
||||
{},
|
||||
);
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data.reservations};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function addSnowflakeReservation(
|
||||
config: Config,
|
||||
session: Session,
|
||||
email: string,
|
||||
snowflake: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/snowflake-reservations/add', {email, snowflake});
|
||||
}
|
||||
|
||||
export async function deleteSnowflakeReservation(
|
||||
config: Config,
|
||||
session: Session,
|
||||
email: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/snowflake-reservations/delete', {email});
|
||||
}
|
||||
37
packages/admin/src/api/JsonTypes.tsx
Normal file
37
packages/admin/src/api/JsonTypes.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
||||
export type JsonObject = {[key: string]: JsonValue};
|
||||
export type JsonArray = Array<JsonValue>;
|
||||
export function parseJson(text: string): JsonValue | null {
|
||||
try {
|
||||
return JSON.parse(text) as JsonValue;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isJsonObject(value: JsonValue | null): value is JsonObject {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
74
packages/admin/src/api/LimitConfig.tsx
Normal file
74
packages/admin/src/api/LimitConfig.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {LimitConfigGetResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type LimitConfigResponse = z.infer<typeof LimitConfigGetResponse>;
|
||||
type LimitRule = LimitConfigResponse['limit_config']['rules'][number];
|
||||
|
||||
export async function getLimitConfig(config: Config, session: Session): Promise<ApiResult<LimitConfigResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<LimitConfigResponse>('/admin/limit-config/get', {});
|
||||
}
|
||||
|
||||
export async function updateLimitConfig(
|
||||
config: Config,
|
||||
session: Session,
|
||||
limitConfigJsonValue: string,
|
||||
): Promise<ApiResult<LimitConfigResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<LimitConfigResponse>('/admin/limit-config/update', `{"limit_config":${limitConfigJsonValue}}`);
|
||||
}
|
||||
|
||||
export function getDefaultValue(response: LimitConfigResponse, ruleId: string, limitKey: string): number | null {
|
||||
const ruleDefaults = response.defaults[ruleId];
|
||||
if (!ruleDefaults) {
|
||||
return null;
|
||||
}
|
||||
const value = ruleDefaults[limitKey];
|
||||
return value !== undefined ? value : null;
|
||||
}
|
||||
|
||||
export function isModified(rule: LimitRule, limitKey: string): boolean {
|
||||
return rule.modifiedFields?.includes(limitKey) ?? false;
|
||||
}
|
||||
|
||||
export function getKeysByCategory(response: LimitConfigResponse): Record<string, Array<string>> {
|
||||
const result: Record<string, Array<string>> = {};
|
||||
|
||||
for (const key of response.limit_keys) {
|
||||
const meta = response.metadata[key];
|
||||
if (meta) {
|
||||
const category = meta.category;
|
||||
if (!result[category]) {
|
||||
result[category] = [];
|
||||
}
|
||||
result[category].push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
131
packages/admin/src/api/Messages.tsx
Normal file
131
packages/admin/src/api/Messages.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
DeleteAllUserMessagesResponse,
|
||||
MessageShredResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminMessageSchemas';
|
||||
import type {
|
||||
LookupMessageResponse as LookupMessageResponseSchema,
|
||||
MessageShredStatusResponse as MessageShredStatusResponseSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export type LookupMessageResponse = z.infer<typeof LookupMessageResponseSchema>;
|
||||
export type MessageShredStatusResponse = z.infer<typeof MessageShredStatusResponseSchema>;
|
||||
|
||||
export type ShredEntry = {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
};
|
||||
|
||||
export async function deleteMessage(
|
||||
config: Config,
|
||||
session: Session,
|
||||
channel_id: string,
|
||||
message_id: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid(
|
||||
'/admin/messages/delete',
|
||||
{
|
||||
channel_id,
|
||||
message_id,
|
||||
},
|
||||
auditLogReason,
|
||||
);
|
||||
}
|
||||
|
||||
export async function lookupMessage(
|
||||
config: Config,
|
||||
session: Session,
|
||||
channel_id: string,
|
||||
message_id: string,
|
||||
context_limit: number,
|
||||
): Promise<ApiResult<LookupMessageResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<LookupMessageResponse>('/admin/messages/lookup', {
|
||||
channel_id,
|
||||
message_id,
|
||||
context_limit,
|
||||
});
|
||||
}
|
||||
|
||||
export async function queueMessageShred(
|
||||
config: Config,
|
||||
session: Session,
|
||||
user_id: string,
|
||||
entries: Array<ShredEntry>,
|
||||
): Promise<ApiResult<MessageShredResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_id,
|
||||
entries: entries.map((e) => ({channel_id: e.channel_id, message_id: e.message_id})),
|
||||
};
|
||||
return client.post<MessageShredResponse>('/admin/messages/shred', body);
|
||||
}
|
||||
|
||||
export async function deleteAllUserMessages(
|
||||
config: Config,
|
||||
session: Session,
|
||||
user_id: string,
|
||||
dry_run: boolean,
|
||||
): Promise<ApiResult<DeleteAllUserMessagesResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<DeleteAllUserMessagesResponse>('/admin/messages/delete-all', {
|
||||
user_id,
|
||||
dry_run,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getMessageShredStatus(
|
||||
config: Config,
|
||||
session: Session,
|
||||
job_id: string,
|
||||
): Promise<ApiResult<MessageShredStatusResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<MessageShredStatusResponse>('/admin/messages/shred-status', {
|
||||
job_id,
|
||||
});
|
||||
}
|
||||
|
||||
export async function lookupMessageByAttachment(
|
||||
config: Config,
|
||||
session: Session,
|
||||
channel_id: string,
|
||||
attachment_id: string,
|
||||
filename: string,
|
||||
context_limit: number,
|
||||
): Promise<ApiResult<LookupMessageResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<LookupMessageResponse>('/admin/messages/lookup-by-attachment', {
|
||||
channel_id,
|
||||
attachment_id,
|
||||
filename,
|
||||
context_limit,
|
||||
});
|
||||
}
|
||||
94
packages/admin/src/api/Reports.tsx
Normal file
94
packages/admin/src/api/Reports.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
ListReportsResponse,
|
||||
ReportAdminResponseSchema,
|
||||
SearchReportsResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export type Report = z.infer<typeof ReportAdminResponseSchema>;
|
||||
|
||||
export async function listReports(
|
||||
config: Config,
|
||||
session: Session,
|
||||
status: number,
|
||||
limit: number,
|
||||
offset?: number,
|
||||
): Promise<ApiResult<z.infer<typeof ListReportsResponse>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
status,
|
||||
limit,
|
||||
...(offset !== undefined ? {offset} : {}),
|
||||
};
|
||||
|
||||
return client.post<z.infer<typeof ListReportsResponse>>('/admin/reports/list', body);
|
||||
}
|
||||
|
||||
export async function resolveReport(
|
||||
config: Config,
|
||||
session: Session,
|
||||
reportId: string,
|
||||
publicComment?: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
report_id: reportId,
|
||||
...(publicComment !== undefined ? {public_comment: publicComment} : {}),
|
||||
};
|
||||
return client.postVoid('/admin/reports/resolve', body, auditLogReason);
|
||||
}
|
||||
|
||||
export async function searchReports(
|
||||
config: Config,
|
||||
session: Session,
|
||||
query?: string,
|
||||
statusFilter?: number,
|
||||
typeFilter?: number,
|
||||
categoryFilter?: string,
|
||||
limit: number = 25,
|
||||
offset: number = 0,
|
||||
): Promise<ApiResult<z.infer<typeof SearchReportsResponse>>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
limit,
|
||||
offset,
|
||||
...(query && query !== '' ? {query} : {}),
|
||||
...(statusFilter !== undefined ? {status: statusFilter} : {}),
|
||||
...(typeFilter !== undefined ? {report_type: typeFilter} : {}),
|
||||
...(categoryFilter && categoryFilter !== '' ? {category: categoryFilter} : {}),
|
||||
};
|
||||
|
||||
return client.post<z.infer<typeof SearchReportsResponse>>('/admin/reports/search', body);
|
||||
}
|
||||
|
||||
export async function getReportDetail(config: Config, session: Session, reportId: string): Promise<ApiResult<Report>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<Report>(`/admin/reports/${reportId}`);
|
||||
}
|
||||
62
packages/admin/src/api/Search.tsx
Normal file
62
packages/admin/src/api/Search.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
IndexRefreshStatusResponse,
|
||||
RefreshSearchIndexResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function refreshSearchIndex(
|
||||
config: Config,
|
||||
session: Session,
|
||||
indexType: string,
|
||||
reason?: string,
|
||||
): Promise<ApiResult<RefreshSearchIndexResponse>> {
|
||||
return refreshSearchIndexWithGuild(config, session, indexType, undefined, reason);
|
||||
}
|
||||
|
||||
export async function refreshSearchIndexWithGuild(
|
||||
config: Config,
|
||||
session: Session,
|
||||
indexType: string,
|
||||
guildId?: string,
|
||||
reason?: string,
|
||||
): Promise<ApiResult<RefreshSearchIndexResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: Record<string, string> = {
|
||||
index_type: indexType,
|
||||
...(guildId ? {guild_id: guildId} : {}),
|
||||
};
|
||||
return client.post<RefreshSearchIndexResponse>('/admin/search/refresh-index', body, reason);
|
||||
}
|
||||
|
||||
export async function getIndexRefreshStatus(
|
||||
config: Config,
|
||||
session: Session,
|
||||
jobId: string,
|
||||
): Promise<ApiResult<IndexRefreshStatusResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<IndexRefreshStatusResponse>('/admin/search/refresh-status', {job_id: jobId});
|
||||
}
|
||||
53
packages/admin/src/api/System.tsx
Normal file
53
packages/admin/src/api/System.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
GuildMemoryStatsResponse,
|
||||
NodeStatsResponse,
|
||||
ReloadAllGuildsResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function getGuildMemoryStats(
|
||||
config: Config,
|
||||
session: Session,
|
||||
limit: number = 25,
|
||||
): Promise<ApiResult<GuildMemoryStatsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<GuildMemoryStatsResponse>('/admin/gateway/memory-stats', {limit});
|
||||
}
|
||||
|
||||
export async function reloadAllGuilds(
|
||||
config: Config,
|
||||
session: Session,
|
||||
guildIds: Array<string>,
|
||||
): Promise<ApiResult<ReloadAllGuildsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<ReloadAllGuildsResponse>('/admin/gateway/reload-all', {guild_ids: guildIds});
|
||||
}
|
||||
|
||||
export async function getNodeStats(config: Config, session: Session): Promise<ApiResult<NodeStatsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<NodeStatsResponse>('/admin/gateway/stats');
|
||||
}
|
||||
68
packages/admin/src/api/SystemDm.tsx
Normal file
68
packages/admin/src/api/SystemDm.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {ListSystemDmJobsResponse, SystemDmJobResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
|
||||
export async function createSystemDmJob(
|
||||
config: Config,
|
||||
session: Session,
|
||||
content: string,
|
||||
registrationStart?: string,
|
||||
registrationEnd?: string,
|
||||
excludedGuildIds: Array<string> = [],
|
||||
): Promise<ApiResult<SystemDmJobResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
content,
|
||||
...(registrationStart ? {registration_start: registrationStart} : {}),
|
||||
...(registrationEnd ? {registration_end: registrationEnd} : {}),
|
||||
...(excludedGuildIds.length > 0 ? {excluded_guild_ids: excludedGuildIds} : {}),
|
||||
};
|
||||
|
||||
return client.post<SystemDmJobResponse>('/admin/system-dm-jobs', body);
|
||||
}
|
||||
|
||||
export async function listSystemDmJobs(
|
||||
config: Config,
|
||||
session: Session,
|
||||
limit: number = 25,
|
||||
beforeJobId?: string,
|
||||
): Promise<ApiResult<ListSystemDmJobsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.get<ListSystemDmJobsResponse>('/admin/system-dm-jobs', {
|
||||
limit,
|
||||
before_job_id: beforeJobId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function approveSystemDmJob(
|
||||
config: Config,
|
||||
session: Session,
|
||||
jobId: string,
|
||||
): Promise<ApiResult<SystemDmJobResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<SystemDmJobResponse>(`/admin/system-dm-jobs/${jobId}/approve`, {});
|
||||
}
|
||||
419
packages/admin/src/api/Users.tsx
Normal file
419
packages/admin/src/api/Users.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {ListUserGuildsResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
||||
import type {
|
||||
AdminUsersMeResponse,
|
||||
ListUserChangeLogResponse,
|
||||
ListUserDmChannelsResponse,
|
||||
ListUserSessionsResponse,
|
||||
LookupUserResponse,
|
||||
UserAdminResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
|
||||
export async function getCurrentAdmin(config: Config, session: Session): Promise<ApiResult<UserAdminResponse | null>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.get<AdminUsersMeResponse>('/admin/users/me');
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data.user};
|
||||
}
|
||||
if (result.error.type === 'notFound') {
|
||||
return {ok: true, data: null};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface UserSearchResult {
|
||||
users: Array<UserAdminResponse>;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export async function searchUsers(
|
||||
config: Config,
|
||||
session: Session,
|
||||
query: string,
|
||||
page: number = 0,
|
||||
limit: number = 25,
|
||||
): Promise<ApiResult<UserSearchResult>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const offset = Math.max(0, page) * limit;
|
||||
const result = await client.post<{users: Array<UserAdminResponse>; total: number}>('/admin/users/search', {
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const users = result.data.users;
|
||||
const hasMore = offset + users.length < result.data.total;
|
||||
return {ok: true, data: {users, has_more: hasMore}};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function lookupUser(
|
||||
config: Config,
|
||||
session: Session,
|
||||
query: string,
|
||||
): Promise<ApiResult<UserAdminResponse | null>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<LookupUserResponse>('/admin/users/lookup', {query});
|
||||
if (result.ok) {
|
||||
const user = result.data.users[0];
|
||||
return {ok: true, data: user ?? null};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function lookupUsersByIds(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userIds: Array<string>,
|
||||
): Promise<ApiResult<Array<UserAdminResponse>>> {
|
||||
if (userIds.length === 0) {
|
||||
return {ok: true, data: []};
|
||||
}
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<LookupUserResponse>('/admin/users/lookup', {user_ids: userIds});
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data.users};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listUserGuilds(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
before?: string,
|
||||
after?: string,
|
||||
limit: number = 25,
|
||||
withCounts: boolean = true,
|
||||
): Promise<ApiResult<ListUserGuildsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_id: userId,
|
||||
limit,
|
||||
with_counts: withCounts,
|
||||
...(before ? {before} : {}),
|
||||
...(after ? {after} : {}),
|
||||
};
|
||||
|
||||
const result = await client.post<ListUserGuildsResponse>('/admin/users/list-guilds', body);
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listUserDmChannels(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
before?: string,
|
||||
after?: string,
|
||||
limit: number = 50,
|
||||
): Promise<ApiResult<ListUserDmChannelsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_id: userId,
|
||||
limit,
|
||||
...(before ? {before} : {}),
|
||||
...(after ? {after} : {}),
|
||||
};
|
||||
|
||||
const result = await client.post<ListUserDmChannelsResponse>('/admin/users/list-dm-channels', body);
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listUserSessions(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
): Promise<ApiResult<ListUserSessionsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<ListUserSessionsResponse>('/admin/users/list-sessions', {
|
||||
user_id: userId,
|
||||
});
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listUserChangeLog(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
): Promise<ApiResult<ListUserChangeLogResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const result = await client.post<ListUserChangeLogResponse>('/admin/users/change-log', {user_id: userId, limit: 50});
|
||||
if (result.ok) {
|
||||
return {ok: true, data: result.data};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateUserFlags(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
addFlags: Array<string>,
|
||||
removeFlags: Array<string>,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid(
|
||||
'/admin/users/update-flags',
|
||||
{user_id: userId, add_flags: addFlags, remove_flags: removeFlags},
|
||||
auditLogReason,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateSuspiciousActivityFlags(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
flags: number,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/update-suspicious-activity-flags', {user_id: userId, flags}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function setUserAcls(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
acls: Array<string>,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/set-acls', {user_id: userId, acls}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function setUserTraits(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
traits: Array<string>,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/set-traits', {user_id: userId, traits}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function disableMfa(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/disable-mfa', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function verifyEmail(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/verify-email', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function unlinkPhone(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/unlink-phone', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function terminateSessions(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/terminate-sessions', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function clearUserFields(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
fields: Array<string>,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/clear-fields', {user_id: userId, fields}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function setBotStatus(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
bot: boolean,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/set-bot-status', {user_id: userId, bot}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function setSystemStatus(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
system: boolean,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/set-system-status', {user_id: userId, system}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function changeUsername(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
username: string,
|
||||
discriminator?: number,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_id: userId,
|
||||
username,
|
||||
...(discriminator !== undefined ? {discriminator} : {}),
|
||||
};
|
||||
return client.postVoid('/admin/users/change-username', body, auditLogReason);
|
||||
}
|
||||
|
||||
export async function changeEmail(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
email: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/change-email', {user_id: userId, email}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function scheduleDeletion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
reasonCode: number,
|
||||
publicReason: string | undefined,
|
||||
daysUntilDeletion: number,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_id: userId,
|
||||
reason_code: reasonCode,
|
||||
days_until_deletion: daysUntilDeletion,
|
||||
...(publicReason ? {public_reason: publicReason} : {}),
|
||||
};
|
||||
return client.postVoid('/admin/users/schedule-deletion', body, auditLogReason);
|
||||
}
|
||||
|
||||
export async function cancelDeletion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/cancel-deletion', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function cancelBulkMessageDeletion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/cancel-bulk-message-deletion', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function tempBanUser(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
durationHours: number,
|
||||
reason?: string,
|
||||
privateReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
user_id: userId,
|
||||
duration_hours: durationHours,
|
||||
...(reason ? {reason} : {}),
|
||||
};
|
||||
return client.postVoid('/admin/users/temp-ban', body, privateReason);
|
||||
}
|
||||
|
||||
export async function unbanUser(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/unban', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function changeDob(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
dateOfBirth: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/change-dob', {user_id: userId, date_of_birth: dateOfBirth}, auditLogReason);
|
||||
}
|
||||
|
||||
export async function sendPasswordReset(
|
||||
config: Config,
|
||||
session: Session,
|
||||
userId: string,
|
||||
auditLogReason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/users/send-password-reset', {user_id: userId}, auditLogReason);
|
||||
}
|
||||
87
packages/admin/src/api/VisionarySlots.tsx
Normal file
87
packages/admin/src/api/VisionarySlots.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
ListVisionarySlotsResponse,
|
||||
VisionarySlotOperationResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type ListVisionarySlotsResponseType = z.infer<typeof ListVisionarySlotsResponse>;
|
||||
type VisionarySlotOperationResponseType = z.infer<typeof VisionarySlotOperationResponse>;
|
||||
|
||||
export async function listVisionarySlots(
|
||||
config: Config,
|
||||
session: Session,
|
||||
): Promise<ApiResult<ListVisionarySlotsResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.get<ListVisionarySlotsResponseType>('/admin/visionary-slots');
|
||||
}
|
||||
|
||||
export async function expandVisionarySlots(
|
||||
config: Config,
|
||||
session: Session,
|
||||
count: number,
|
||||
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/expand', {count});
|
||||
}
|
||||
|
||||
export async function shrinkVisionarySlots(
|
||||
config: Config,
|
||||
session: Session,
|
||||
targetCount: number,
|
||||
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/shrink', {
|
||||
target_count: targetCount,
|
||||
});
|
||||
}
|
||||
|
||||
export async function reserveVisionarySlot(
|
||||
config: Config,
|
||||
session: Session,
|
||||
slotIndex: number,
|
||||
userId: string | null,
|
||||
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/reserve', {
|
||||
slot_index: slotIndex,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function swapVisionarySlots(
|
||||
config: Config,
|
||||
session: Session,
|
||||
slotIndexA: number,
|
||||
slotIndexB: number,
|
||||
): Promise<ApiResult<VisionarySlotOperationResponseType>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return await client.post<VisionarySlotOperationResponseType>('/admin/visionary-slots/swap', {
|
||||
slot_index_a: slotIndexA,
|
||||
slot_index_b: slotIndexB,
|
||||
});
|
||||
}
|
||||
217
packages/admin/src/api/Voice.tsx
Normal file
217
packages/admin/src/api/Voice.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ApiClient, type ApiResult} from '@fluxer/admin/src/api/Client';
|
||||
import type {JsonObject} from '@fluxer/admin/src/api/JsonTypes';
|
||||
import type {Session} from '@fluxer/admin/src/types/App';
|
||||
import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config';
|
||||
import type {
|
||||
CreateVoiceRegionRequest,
|
||||
CreateVoiceServerRequest,
|
||||
ListVoiceRegionsResponse,
|
||||
ListVoiceServersResponse,
|
||||
UpdateVoiceRegionRequest,
|
||||
UpdateVoiceServerRequest,
|
||||
VoiceRegionWithServersResponse,
|
||||
VoiceServerAdminResponse,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminVoiceSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export interface GetVoiceRegionResponse {
|
||||
region: VoiceRegionWithServersResponse | null;
|
||||
}
|
||||
|
||||
export interface GetVoiceServerResponse {
|
||||
server: VoiceServerAdminResponse | null;
|
||||
}
|
||||
|
||||
export async function listVoiceRegions(
|
||||
config: Config,
|
||||
session: Session,
|
||||
include_servers: boolean,
|
||||
): Promise<ApiResult<ListVoiceRegionsResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<ListVoiceRegionsResponse>('/admin/voice/regions/list', {include_servers});
|
||||
}
|
||||
|
||||
export async function getVoiceRegion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
id: string,
|
||||
include_servers: boolean,
|
||||
): Promise<ApiResult<GetVoiceRegionResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<GetVoiceRegionResponse>('/admin/voice/regions/get', {
|
||||
id,
|
||||
include_servers,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateVoiceRegionParams
|
||||
extends Omit<z.infer<typeof CreateVoiceRegionRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
|
||||
allowed_guild_ids: Array<string>;
|
||||
allowed_user_ids?: Array<string>;
|
||||
audit_log_reason?: string;
|
||||
}
|
||||
|
||||
export async function createVoiceRegion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
params: CreateVoiceRegionParams,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid(
|
||||
'/admin/voice/regions/create',
|
||||
{
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
emoji: params.emoji,
|
||||
latitude: params.latitude,
|
||||
longitude: params.longitude,
|
||||
is_default: params.is_default,
|
||||
vip_only: params.vip_only,
|
||||
required_guild_features: params.required_guild_features,
|
||||
allowed_guild_ids: params.allowed_guild_ids,
|
||||
allowed_user_ids: params.allowed_user_ids ?? [],
|
||||
},
|
||||
params.audit_log_reason,
|
||||
);
|
||||
}
|
||||
|
||||
export interface UpdateVoiceRegionParams
|
||||
extends Omit<z.infer<typeof UpdateVoiceRegionRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
|
||||
allowed_guild_ids?: Array<string>;
|
||||
allowed_user_ids?: Array<string>;
|
||||
audit_log_reason?: string;
|
||||
}
|
||||
|
||||
export async function updateVoiceRegion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
params: UpdateVoiceRegionParams,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
id: params.id,
|
||||
...(params.name !== undefined ? {name: params.name} : {}),
|
||||
...(params.emoji !== undefined ? {emoji: params.emoji} : {}),
|
||||
...(params.latitude !== undefined ? {latitude: params.latitude} : {}),
|
||||
...(params.longitude !== undefined ? {longitude: params.longitude} : {}),
|
||||
...(params.is_default !== undefined ? {is_default: params.is_default} : {}),
|
||||
...(params.vip_only !== undefined ? {vip_only: params.vip_only} : {}),
|
||||
...(params.required_guild_features !== undefined ? {required_guild_features: params.required_guild_features} : {}),
|
||||
...(params.allowed_guild_ids !== undefined ? {allowed_guild_ids: params.allowed_guild_ids} : {}),
|
||||
...(params.allowed_user_ids !== undefined ? {allowed_user_ids: params.allowed_user_ids} : {}),
|
||||
};
|
||||
|
||||
return client.postVoid('/admin/voice/regions/update', body, params.audit_log_reason);
|
||||
}
|
||||
|
||||
export async function deleteVoiceRegion(
|
||||
config: Config,
|
||||
session: Session,
|
||||
id: string,
|
||||
audit_log_reason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/voice/regions/delete', {id}, audit_log_reason);
|
||||
}
|
||||
|
||||
export async function listVoiceServers(
|
||||
config: Config,
|
||||
session: Session,
|
||||
region_id: string,
|
||||
): Promise<ApiResult<ListVoiceServersResponse>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.post<ListVoiceServersResponse>('/admin/voice/servers/list', {region_id});
|
||||
}
|
||||
|
||||
export interface CreateVoiceServerParams
|
||||
extends Omit<z.infer<typeof CreateVoiceServerRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
|
||||
allowed_guild_ids: Array<string>;
|
||||
allowed_user_ids?: Array<string>;
|
||||
audit_log_reason?: string;
|
||||
}
|
||||
|
||||
export async function createVoiceServer(
|
||||
config: Config,
|
||||
session: Session,
|
||||
params: CreateVoiceServerParams,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid(
|
||||
'/admin/voice/servers/create',
|
||||
{
|
||||
region_id: params.region_id,
|
||||
server_id: params.server_id,
|
||||
endpoint: params.endpoint,
|
||||
api_key: params.api_key,
|
||||
api_secret: params.api_secret,
|
||||
is_active: params.is_active,
|
||||
vip_only: params.vip_only,
|
||||
required_guild_features: params.required_guild_features,
|
||||
allowed_guild_ids: params.allowed_guild_ids,
|
||||
allowed_user_ids: params.allowed_user_ids ?? [],
|
||||
},
|
||||
params.audit_log_reason,
|
||||
);
|
||||
}
|
||||
|
||||
export interface UpdateVoiceServerParams
|
||||
extends Omit<z.infer<typeof UpdateVoiceServerRequest>, 'allowed_guild_ids' | 'allowed_user_ids'> {
|
||||
allowed_guild_ids?: Array<string>;
|
||||
allowed_user_ids?: Array<string>;
|
||||
audit_log_reason?: string;
|
||||
}
|
||||
|
||||
export async function updateVoiceServer(
|
||||
config: Config,
|
||||
session: Session,
|
||||
params: UpdateVoiceServerParams,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
const body: JsonObject = {
|
||||
region_id: params.region_id,
|
||||
server_id: params.server_id,
|
||||
...(params.endpoint !== undefined ? {endpoint: params.endpoint} : {}),
|
||||
...(params.api_key !== undefined ? {api_key: params.api_key} : {}),
|
||||
...(params.api_secret !== undefined ? {api_secret: params.api_secret} : {}),
|
||||
...(params.is_active !== undefined ? {is_active: params.is_active} : {}),
|
||||
...(params.vip_only !== undefined ? {vip_only: params.vip_only} : {}),
|
||||
...(params.required_guild_features !== undefined ? {required_guild_features: params.required_guild_features} : {}),
|
||||
...(params.allowed_guild_ids !== undefined ? {allowed_guild_ids: params.allowed_guild_ids} : {}),
|
||||
...(params.allowed_user_ids !== undefined ? {allowed_user_ids: params.allowed_user_ids} : {}),
|
||||
};
|
||||
|
||||
return client.postVoid('/admin/voice/servers/update', body, params.audit_log_reason);
|
||||
}
|
||||
|
||||
export async function deleteVoiceServer(
|
||||
config: Config,
|
||||
session: Session,
|
||||
region_id: string,
|
||||
server_id: string,
|
||||
audit_log_reason?: string,
|
||||
): Promise<ApiResult<void>> {
|
||||
const client = new ApiClient(config, session);
|
||||
return client.postVoid('/admin/voice/servers/delete', {region_id, server_id}, audit_log_reason);
|
||||
}
|
||||
Reference in New Issue
Block a user