mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(admin)!: move the admin api to rest and fix its defects (#2515)
This commit is contained in:
1 parent
a70924d4b0
commit
7f8f09ee51
143 files changed
+15238
-12790
No files matched your search
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {
|
||||
GIFT_CODE_DURATION_TYPE_DEFINITIONS,
|
||||
MAX_GIFT_CODES_PER_REQUEST,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
SystemChannelFlagsDescriptions,
|
||||
} from '@fluxer/constants/src/GuildConstants';
|
||||
import {LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {ADMIN_ACL_COUNT, AdminAclType} from '@fluxer/schema/src/domains/admin/AdminAclType';
|
||||
import {GuildAdminResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
||||
import {UserAdminResponseSchema} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
NSFWLevelSchema,
|
||||
} from '@fluxer/schema/src/primitives/GuildValidators';
|
||||
import {PermissionStringType} from '@fluxer/schema/src/primitives/PermissionValidators';
|
||||
import {createQueryIntegerType, QueryBooleanType} from '@fluxer/schema/src/primitives/QueryValidators';
|
||||
import {
|
||||
createBitflagInt32Type,
|
||||
createInt32EnumType,
|
||||
@@ -45,8 +46,6 @@ import {
|
||||
import {EmailType} from '@fluxer/schema/src/primitives/UserValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
const ADMIN_ACL_COUNT = Object.keys(AdminACLs).length;
|
||||
|
||||
const ReportStatusSchema = withOpenApiType(
|
||||
createInt32EnumType(
|
||||
[
|
||||
@@ -113,28 +112,26 @@ const SearchIndexTypeEnum = createNamedStringLiteralUnion(
|
||||
],
|
||||
'Type of search index to refresh',
|
||||
);
|
||||
export const ListAuditLogsRequest = z.object({
|
||||
admin_user_id: SnowflakeType.optional().describe('Filter by admin user who performed the action'),
|
||||
target_type: createStringType(1, 64).optional().describe('Filter by target entity type'),
|
||||
target_id: z.string().optional().describe('Filter by target entity ID (user, channel, role, invite code, etc.)'),
|
||||
limit: z.number().int().min(1).max(200).default(50).describe('Maximum number of entries to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of entries to skip'),
|
||||
export const AuditLogIdParam = z.object({
|
||||
log_id: SnowflakeType.describe('The ID of the audit log entry'),
|
||||
});
|
||||
|
||||
export type ListAuditLogsRequest = z.infer<typeof ListAuditLogsRequest>;
|
||||
export type AuditLogIdParam = z.infer<typeof AuditLogIdParam>;
|
||||
|
||||
export const SearchAuditLogsRequest = z.object({
|
||||
query: createStringType(1, 1024).optional().describe('Search query string'),
|
||||
export const ListAdminAuditLogsQuery = z.object({
|
||||
q: createStringType(1, 1024).optional().describe('Free-text query run against the audit log search index'),
|
||||
admin_user_id: SnowflakeType.optional().describe('Filter by admin user who performed the action'),
|
||||
target_type: createStringType(1, 64).optional().describe('Filter by target entity type'),
|
||||
target_id: z.string().optional().describe('Filter by target entity ID (user, channel, role, invite code, etc.)'),
|
||||
sort_by: AuditLogSortByEnum.default('createdAt'),
|
||||
sort_order: SortOrderEnum.default('desc'),
|
||||
limit: z.number().int().min(1).max(200).default(50).describe('Maximum number of entries to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of entries to skip'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of entries to return',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0}).describe('Number of entries to skip'),
|
||||
});
|
||||
|
||||
export type SearchAuditLogsRequest = z.infer<typeof SearchAuditLogsRequest>;
|
||||
export type ListAdminAuditLogsQuery = z.infer<typeof ListAdminAuditLogsQuery>;
|
||||
|
||||
export const SearchReportsRequest = z.object({
|
||||
query: createStringType(1, 1024).optional().describe('Search query string'),
|
||||
@@ -155,34 +152,78 @@ export const SearchReportsRequest = z.object({
|
||||
|
||||
export type SearchReportsRequest = z.infer<typeof SearchReportsRequest>;
|
||||
|
||||
export const ListReportsRequest = z.object({
|
||||
status: ReportStatusSchema.optional(),
|
||||
limit: z.number().int().min(1).max(200).optional().describe('Maximum number of reports to return'),
|
||||
offset: z.number().int().min(0).optional().describe('Number of reports to skip'),
|
||||
const ReportStatusFilterEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['pending', 'pending', 'Only reports that are waiting to be reviewed'],
|
||||
['resolved', 'resolved', 'Only reports that have been resolved'],
|
||||
],
|
||||
'Only return reports with this status',
|
||||
);
|
||||
|
||||
const ReportTypeFilterEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['message', 'message', 'Only reports about a message'],
|
||||
['user', 'user', 'Only reports about a user'],
|
||||
['guild', 'guild', 'Only reports about a community'],
|
||||
],
|
||||
'Only return reports about this kind of entity',
|
||||
);
|
||||
|
||||
const ReportSortByQueryEnum = createNamedStringLiteralUnion(
|
||||
[
|
||||
['created_at', 'created_at', 'Sort by the time the report was created'],
|
||||
['reported_at', 'reported_at', 'Sort by the time the report was submitted'],
|
||||
['resolved_at', 'resolved_at', 'Sort by the time the report was resolved'],
|
||||
],
|
||||
'The field to sort the reports by',
|
||||
);
|
||||
|
||||
export const ListReportsQuery = z.object({
|
||||
q: createStringType(1, 1024).optional().describe('Free-text query matched against the searchable report fields'),
|
||||
status: ReportStatusFilterEnum.optional().describe('Only return reports with this status'),
|
||||
report_type: ReportTypeFilterEnum.optional().describe('Only return reports about this kind of entity'),
|
||||
category: createStringType(1, 128).optional().describe('Only return reports filed under this category'),
|
||||
reporter_id: SnowflakeType.optional().describe('Only return reports submitted by this user'),
|
||||
reported_user_id: SnowflakeType.optional().describe('Only return reports about this user'),
|
||||
reported_guild_id: SnowflakeType.optional().describe('Only return reports about this community'),
|
||||
reported_channel_id: SnowflakeType.optional().describe('Only return reports about content in this channel'),
|
||||
guild_context_id: SnowflakeType.optional().describe('Only return reports filed from within this community'),
|
||||
resolved_by_admin_id: SnowflakeType.optional().describe('Only return reports resolved by this admin'),
|
||||
sort_by: ReportSortByQueryEnum.default('reported_at').describe('The field to sort the reports by'),
|
||||
sort_order: SortOrderEnum.default('desc').describe('The direction to sort the reports in'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of reports to return (1-200, default 50)',
|
||||
),
|
||||
offset: createQueryIntegerType({defaultValue: 0, minValue: 0, maxValue: 10000}).describe('Number of reports to skip'),
|
||||
});
|
||||
|
||||
export type ListReportsRequest = z.infer<typeof ListReportsRequest>;
|
||||
export type ListReportsQuery = z.infer<typeof ListReportsQuery>;
|
||||
|
||||
export const ResolveReportRequest = z.object({
|
||||
report_id: SnowflakeType.describe('The ID of the report to resolve'),
|
||||
export const UpdateReportRequest = z.object({
|
||||
status: z.literal('resolved').describe('The status to move the report to'),
|
||||
public_comment: createStringType(0, 512).optional().describe('Public comment to include with the resolution'),
|
||||
});
|
||||
|
||||
export type ResolveReportRequest = z.infer<typeof ResolveReportRequest>;
|
||||
export type UpdateReportRequest = z.infer<typeof UpdateReportRequest>;
|
||||
|
||||
export const SearchIndexNameParam = z.object({
|
||||
index_name: SearchIndexTypeEnum.describe('The name of the search index'),
|
||||
});
|
||||
|
||||
export type SearchIndexNameParam = z.infer<typeof SearchIndexNameParam>;
|
||||
|
||||
export const RefreshSearchIndexRequest = z.object({
|
||||
index_type: SearchIndexTypeEnum,
|
||||
guild_id: SnowflakeType.optional().describe('Specific guild ID to reindex'),
|
||||
user_id: SnowflakeType.optional().describe('Specific user ID to reindex'),
|
||||
});
|
||||
|
||||
export type RefreshSearchIndexRequest = z.infer<typeof RefreshSearchIndexRequest>;
|
||||
|
||||
export const GetIndexRefreshStatusRequest = z.object({
|
||||
job_id: createStringType(1, 128).describe('ID of the index refresh job to check'),
|
||||
export const SearchIndexRefreshIdParam = z.object({
|
||||
job_id: createStringType(1, 128).describe('ID of the index refresh to read'),
|
||||
});
|
||||
|
||||
export type GetIndexRefreshStatusRequest = z.infer<typeof GetIndexRefreshStatusRequest>;
|
||||
export type SearchIndexRefreshIdParam = z.infer<typeof SearchIndexRefreshIdParam>;
|
||||
|
||||
export const PurgeGuildAssetsRequest = z.object({
|
||||
ids: z.array(createStringType(1, 64)).max(100).describe('List of asset IDs to purge'),
|
||||
@@ -190,29 +231,33 @@ export const PurgeGuildAssetsRequest = z.object({
|
||||
|
||||
export type PurgeGuildAssetsRequest = z.infer<typeof PurgeGuildAssetsRequest>;
|
||||
|
||||
export const TriggerUserArchiveRequest = z.object({
|
||||
user_id: SnowflakeType.describe('ID of the user to archive'),
|
||||
export const AdminArchiveCreateRequest = z.object({
|
||||
include_attachments: z.boolean().default(false).describe('Whether to include attachment binaries in the archive'),
|
||||
});
|
||||
|
||||
export type TriggerUserArchiveRequest = z.infer<typeof TriggerUserArchiveRequest>;
|
||||
export type AdminArchiveCreateRequest = z.infer<typeof AdminArchiveCreateRequest>;
|
||||
|
||||
export const TriggerGuildArchiveRequest = z.object({
|
||||
guild_id: SnowflakeType.describe('ID of the guild to archive'),
|
||||
include_attachments: z.boolean().default(false).describe('Whether to include attachment binaries in the archive'),
|
||||
});
|
||||
export const ListArchivesQuery = z
|
||||
.object({
|
||||
subject_type: ArchiveListSubjectTypeEnum.default('all'),
|
||||
subject_id: SnowflakeType.optional().describe('Filter by specific subject ID'),
|
||||
requested_by: SnowflakeType.optional().describe('Filter by user who requested the archive'),
|
||||
limit: createQueryIntegerType({defaultValue: 50, minValue: 1, maxValue: 200}).describe(
|
||||
'Maximum number of archives to return (1-200, default 50)',
|
||||
),
|
||||
include_expired: QueryBooleanType.describe('Whether to include archives past their expires_at'),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.subject_id !== undefined && value.subject_type === 'all') {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'subject_type must name user or guild when subject_id is supplied',
|
||||
path: ['subject_type'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type TriggerGuildArchiveRequest = z.infer<typeof TriggerGuildArchiveRequest>;
|
||||
|
||||
export const ListArchivesRequest = z.object({
|
||||
subject_type: ArchiveListSubjectTypeEnum.default('all'),
|
||||
subject_id: SnowflakeType.optional().describe('Filter by specific subject ID'),
|
||||
requested_by: SnowflakeType.optional().describe('Filter by user who requested the archive'),
|
||||
limit: z.number().min(1).max(200).default(50).describe('Maximum number of archives to return'),
|
||||
include_expired: z.boolean().default(false).describe('Whether to include expired archives'),
|
||||
});
|
||||
|
||||
export type ListArchivesRequest = z.infer<typeof ListArchivesRequest>;
|
||||
export type ListArchivesQuery = z.infer<typeof ListArchivesQuery>;
|
||||
|
||||
const IP_OR_CIDR_REGEX = /^(?:(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?|(?:[a-fA-F0-9:]+)(?:\/\d{1,3})?)$/;
|
||||
export const BanIpRequest = z.object({
|
||||
@@ -268,12 +313,6 @@ export const BanUrlRequest = z.object({
|
||||
|
||||
export type BanUrlRequest = z.infer<typeof BanUrlRequest>;
|
||||
|
||||
export const UnbanUrlRequest = z.object({
|
||||
url: createStringType(1, 2048).describe('URL to unban (must match the canonicalized form in storage)'),
|
||||
});
|
||||
|
||||
export type UnbanUrlRequest = z.infer<typeof UnbanUrlRequest>;
|
||||
|
||||
export const BanUrlDomainRequest = z.object({
|
||||
domain: createStringType(1, 253)
|
||||
.refine(
|
||||
@@ -296,12 +335,6 @@ export const BanUrlDomainRequest = z.object({
|
||||
|
||||
export type BanUrlDomainRequest = z.infer<typeof BanUrlDomainRequest>;
|
||||
|
||||
export const UnbanUrlDomainRequest = z.object({
|
||||
domain: createStringType(1, 253).describe('Domain to unban'),
|
||||
});
|
||||
|
||||
export type UnbanUrlDomainRequest = z.infer<typeof UnbanUrlDomainRequest>;
|
||||
|
||||
export const BanFileShaRequest = z.object({
|
||||
sha256_hex: createStringType(64, 64)
|
||||
.refine((v) => /^[0-9a-fA-F]{64}$/.test(v), 'Must be a 64-character hex SHA-256')
|
||||
@@ -315,24 +348,6 @@ export const BanFileShaRequest = z.object({
|
||||
|
||||
export type BanFileShaRequest = z.infer<typeof BanFileShaRequest>;
|
||||
|
||||
export const UnbanFileShaRequest = z.object({
|
||||
sha256_hex: createStringType(64, 64).refine((v) => /^[0-9a-fA-F]{64}$/.test(v), 'Must be a 64-character hex SHA-256'),
|
||||
});
|
||||
|
||||
export type UnbanFileShaRequest = z.infer<typeof UnbanFileShaRequest>;
|
||||
|
||||
export const CheckUrlBlocklistRequest = z.object({
|
||||
url: createStringType(1, 2048).describe('URL to check against the blocklist'),
|
||||
});
|
||||
|
||||
export type CheckUrlBlocklistRequest = z.infer<typeof CheckUrlBlocklistRequest>;
|
||||
|
||||
export const CheckFileShaRequest = z.object({
|
||||
sha256_hex: createStringType(64, 64).refine((v) => /^[0-9a-fA-F]{64}$/.test(v), 'Must be a 64-character hex SHA-256'),
|
||||
});
|
||||
|
||||
export type CheckFileShaRequest = z.infer<typeof CheckFileShaRequest>;
|
||||
|
||||
const AvatarHashShortType = createStringType(8, 10).refine(
|
||||
(v) => /^(a_)?[0-9a-fA-F]{8}$/.test(v),
|
||||
'Must be an 8-character MD5 prefix (with optional "a_" prefix)',
|
||||
@@ -732,6 +747,7 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
single_community_enabled: z.boolean().optional(),
|
||||
single_community_name: z.string().trim().min(1).max(100).optional(),
|
||||
direct_messages_disabled: z.boolean().optional(),
|
||||
direct_messages_locked: z.literal(false).optional(),
|
||||
premium_mode: z.enum(['mirror', 'everyone']).optional(),
|
||||
services: z
|
||||
.object({
|
||||
@@ -794,14 +810,22 @@ export const CreateRegistrationUrlResponse = z.object({
|
||||
|
||||
export type CreateRegistrationUrlResponse = z.infer<typeof CreateRegistrationUrlResponse>;
|
||||
|
||||
export const RegistrationUrlActionRequest = z.object({
|
||||
id: createStringType(1, 128),
|
||||
export const RegistrationUrlIdParam = z.object({
|
||||
registration_url_id: createStringType(1, 128).describe('The ID of the registration URL'),
|
||||
});
|
||||
|
||||
export type RegistrationUrlActionRequest = z.infer<typeof RegistrationUrlActionRequest>;
|
||||
export type RegistrationUrlIdParam = z.infer<typeof RegistrationUrlIdParam>;
|
||||
|
||||
const PendingRegistrationStatusSchema = createNamedStringLiteralUnion(
|
||||
[
|
||||
['approved', 'approved', 'The account may log in'],
|
||||
['rejected', 'rejected', 'The account is blocked from logging in'],
|
||||
],
|
||||
'Pending registration decision',
|
||||
);
|
||||
|
||||
export const PendingRegistrationActionRequest = z.object({
|
||||
user_id: SnowflakeStringType,
|
||||
status: PendingRegistrationStatusSchema,
|
||||
});
|
||||
|
||||
export type PendingRegistrationActionRequest = z.infer<typeof PendingRegistrationActionRequest>;
|
||||
@@ -860,7 +884,7 @@ export const CreateAdminApiKeyRequest = z.object({
|
||||
.refine((value) => value.trim().length > 0, 'Name cannot be empty')
|
||||
.describe('Display name for the API key'),
|
||||
expires_in_days: z.number().int().min(1).max(365).optional().describe('Number of days until the key expires'),
|
||||
acls: z.array(z.string()).max(ADMIN_ACL_COUNT).describe('List of access control permissions for the key'),
|
||||
acls: z.array(AdminAclType).max(ADMIN_ACL_COUNT).describe('List of access control permissions for the key'),
|
||||
});
|
||||
|
||||
export type CreateAdminApiKeyRequest = z.infer<typeof CreateAdminApiKeyRequest>;
|
||||
@@ -888,6 +912,23 @@ export const ListAdminApiKeyResponse = z.object({
|
||||
|
||||
export type ListAdminApiKeyResponse = z.infer<typeof ListAdminApiKeyResponse>;
|
||||
|
||||
export const UpdateAdminApiKeyRequest = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.refine((value) => value.trim().length > 0, 'Name cannot be empty')
|
||||
.optional()
|
||||
.describe('New display name for the API key'),
|
||||
acls: z
|
||||
.array(AdminAclType)
|
||||
.max(ADMIN_ACL_COUNT)
|
||||
.optional()
|
||||
.describe('Replacement list of access control permissions for the key'),
|
||||
});
|
||||
|
||||
export type UpdateAdminApiKeyRequest = z.infer<typeof UpdateAdminApiKeyRequest>;
|
||||
|
||||
export const SearchGuildsResponse = z.object({
|
||||
guilds: z.array(GuildAdminResponse),
|
||||
total: z.number(),
|
||||
@@ -1016,7 +1057,7 @@ const AdminAuditLogChannelSummarySchema = z.object({
|
||||
type: ChannelTypeSchema,
|
||||
guild_id: SnowflakeStringType.nullable(),
|
||||
});
|
||||
const AdminAuditLogResponseSchema = z.object({
|
||||
export const AdminAuditLogResponseSchema = z.object({
|
||||
log_id: SnowflakeStringType,
|
||||
admin_user_id: SnowflakeStringType,
|
||||
admin_user: AdminAuditLogUserSummarySchema.nullable(),
|
||||
@@ -1228,8 +1269,8 @@ const AdminLookupGuildSchema = z.object({
|
||||
rules_channel_id: SnowflakeStringType.nullable(),
|
||||
disabled_operations: Int32Type,
|
||||
member_count: Int32Type,
|
||||
channels: z.array(AdminGuildChannelSummarySchema).max(500),
|
||||
roles: z.array(AdminGuildRoleSummarySchema).max(250),
|
||||
channels: z.array(AdminGuildChannelSummarySchema),
|
||||
roles: z.array(AdminGuildRoleSummarySchema),
|
||||
});
|
||||
export const LookupGuildResponse = z.object({
|
||||
guild: AdminLookupGuildSchema.nullable(),
|
||||
@@ -1250,14 +1291,14 @@ const GuildAssetItemSchema = z.object({
|
||||
|
||||
export const ListGuildEmojisResponse = z.object({
|
||||
guild_id: SnowflakeStringType,
|
||||
emojis: z.array(GuildAssetItemSchema).max(500),
|
||||
emojis: z.array(GuildAssetItemSchema),
|
||||
});
|
||||
|
||||
export type ListGuildEmojisResponse = z.infer<typeof ListGuildEmojisResponse>;
|
||||
|
||||
export const ListGuildStickersResponse = z.object({
|
||||
guild_id: SnowflakeStringType,
|
||||
stickers: z.array(GuildAssetItemSchema).max(500),
|
||||
stickers: z.array(GuildAssetItemSchema),
|
||||
});
|
||||
|
||||
export type ListGuildStickersResponse = z.infer<typeof ListGuildStickersResponse>;
|
||||
@@ -1403,7 +1444,7 @@ export const ReportAdminResponseSchema = z.object({
|
||||
message_context: z.array(ReportMessageContextSchema).optional(),
|
||||
message_responses: z.array(MessageResponseSchema).optional(),
|
||||
});
|
||||
export const ListReportsResponse = z.object({
|
||||
const ListReportsResponse = z.object({
|
||||
reports: z.array(ReportAdminResponseSchema),
|
||||
});
|
||||
export const ResolveReportResponse = z.object({
|
||||
@@ -1412,12 +1453,13 @@ export const ResolveReportResponse = z.object({
|
||||
resolved_at: z.string().nullable(),
|
||||
public_comment: z.string().nullable(),
|
||||
});
|
||||
export const SearchReportsResponse = z.object({
|
||||
const SearchReportsResponse = z.object({
|
||||
reports: z.array(ReportAdminResponseSchema),
|
||||
total: z.number(),
|
||||
offset: z.number(),
|
||||
limit: z.number(),
|
||||
});
|
||||
export const AdminReportListResponse = z.union([SearchReportsResponse, ListReportsResponse]);
|
||||
const LimitKeyMetadataSchema = z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
|
||||
Reference in new issue
Block a user