mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-10 04:32:34 +09:00
223 lines
9.0 KiB
TypeScript
223 lines
9.0 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
|
|
import {recordAdminRead, recordAdminWrite} from '@app/api/admin/AdminAuditRecorder';
|
|
import {createGuildID, createUserID} from '@app/api/BrandedTypes';
|
|
import {requireAdminACL, requireAnyAdminACL} from '@app/api/middleware/AdminMiddleware';
|
|
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
|
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
|
import {RateLimitConfigs} from '@app/api/RateLimitConfig';
|
|
import type {HonoApp} from '@app/api/types/HonoEnv';
|
|
import {Validator} from '@app/api/Validator';
|
|
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
|
import {MissingACLError} from '@fluxer/errors/src/domains/core/MissingACLError';
|
|
import {
|
|
AdminArchiveResponseSchema,
|
|
type ArchiveSubjectType,
|
|
} from '@fluxer/schema/src/domains/admin/AdminArchiveSchemas';
|
|
import {
|
|
AdminArchiveCreateRequest,
|
|
DownloadUrlResponseSchema,
|
|
GetArchiveResponseSchema,
|
|
ListArchivesQuery,
|
|
ListArchivesResponseSchema,
|
|
} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
|
import {ArchivePathParam, GuildIdParam, UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
|
|
|
function canViewArchive(adminAcls: Set<string>, subjectType: ArchiveSubjectType): boolean {
|
|
if (adminAcls.has(AdminACLs.WILDCARD) || adminAcls.has(AdminACLs.ARCHIVE_VIEW_ALL)) return true;
|
|
if (subjectType === 'user') return adminAcls.has(AdminACLs.ARCHIVE_TRIGGER_USER);
|
|
return adminAcls.has(AdminACLs.ARCHIVE_TRIGGER_GUILD);
|
|
}
|
|
|
|
function requireArchiveSubjectAccess(adminAcls: Set<string>, subjectType: 'user' | 'guild'): void {
|
|
if (canViewArchive(adminAcls, subjectType) || adminAcls.has(AdminACLs.WILDCARD)) return;
|
|
throw new MissingACLError(subjectType === 'user' ? AdminACLs.ARCHIVE_TRIGGER_USER : AdminACLs.ARCHIVE_TRIGGER_GUILD);
|
|
}
|
|
|
|
function resolveListSubjectType(adminAcls: Set<string>, requested: 'all' | 'user' | 'guild'): 'all' | 'user' | 'guild' {
|
|
if (requested !== 'all') {
|
|
requireArchiveSubjectAccess(adminAcls, requested);
|
|
return requested;
|
|
}
|
|
const viewUser = canViewArchive(adminAcls, 'user');
|
|
const viewGuild = canViewArchive(adminAcls, 'guild');
|
|
if (viewUser && viewGuild) return 'all';
|
|
if (viewUser) return 'user';
|
|
if (viewGuild) return 'guild';
|
|
throw new MissingACLError(AdminACLs.ARCHIVE_VIEW_ALL);
|
|
}
|
|
|
|
export function ArchiveAdminController(app: HonoApp) {
|
|
app.post(
|
|
'/admin/users/:user_id/archives',
|
|
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.ARCHIVE_TRIGGER_USER),
|
|
Validator('param', UserIdParam),
|
|
Validator('json', AdminArchiveCreateRequest),
|
|
OpenAPI({
|
|
operationId: 'create_admin_user_archive',
|
|
summary: 'Create user archive',
|
|
responseSchema: AdminArchiveResponseSchema,
|
|
statusCode: 200,
|
|
security: ['adminApiKey'],
|
|
tags: ['Admin'],
|
|
description:
|
|
"Initiates a data export for a user. Creates an archive containing all the user's data (messages, server memberships, preferences, etc.) for export or compliance purposes.",
|
|
}),
|
|
async (ctx) => {
|
|
const adminArchiveService = ctx.get('adminArchiveService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const userId = createUserID(ctx.req.valid('param').user_id);
|
|
const includeAttachments = ctx.req.valid('json').include_attachments;
|
|
const result = await adminArchiveService.triggerUserArchive(userId, adminUserId, includeAttachments);
|
|
await recordAdminWrite(ctx, {
|
|
targetType: 'user',
|
|
targetId: userId,
|
|
action: 'trigger_user_archive',
|
|
metadata: {archive_id: result.archive_id, include_attachments: includeAttachments},
|
|
});
|
|
return ctx.json(result, 200);
|
|
},
|
|
);
|
|
app.post(
|
|
'/admin/guilds/:guild_id/archives',
|
|
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAdminACL(AdminACLs.ARCHIVE_TRIGGER_GUILD),
|
|
Validator('param', GuildIdParam),
|
|
Validator('json', AdminArchiveCreateRequest),
|
|
OpenAPI({
|
|
operationId: 'create_admin_guild_archive',
|
|
summary: 'Create guild archive',
|
|
responseSchema: AdminArchiveResponseSchema,
|
|
statusCode: 200,
|
|
security: ['adminApiKey'],
|
|
tags: ['Admin'],
|
|
description:
|
|
'Initiates a data export for a guild (server). Creates an archive containing all guild data including channels, messages, members, roles, and settings.',
|
|
}),
|
|
async (ctx) => {
|
|
const adminArchiveService = ctx.get('adminArchiveService');
|
|
const adminUserId = ctx.get('adminUserId');
|
|
const guildId = createGuildID(ctx.req.valid('param').guild_id);
|
|
const includeAttachments = ctx.req.valid('json').include_attachments;
|
|
const result = await adminArchiveService.triggerGuildArchive(guildId, adminUserId, includeAttachments);
|
|
await recordAdminWrite(ctx, {
|
|
targetType: 'guild',
|
|
targetId: guildId,
|
|
action: 'trigger_guild_archive',
|
|
metadata: {archive_id: result.archive_id, include_attachments: includeAttachments},
|
|
});
|
|
return ctx.json(result, 200);
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/archives',
|
|
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAnyAdminACL([AdminACLs.ARCHIVE_VIEW_ALL, AdminACLs.ARCHIVE_TRIGGER_USER, AdminACLs.ARCHIVE_TRIGGER_GUILD]),
|
|
Validator('query', ListArchivesQuery),
|
|
OpenAPI({
|
|
operationId: 'list_admin_archives',
|
|
summary: 'List archives',
|
|
responseSchema: ListArchivesResponseSchema,
|
|
statusCode: 200,
|
|
security: ['adminApiKey'],
|
|
tags: ['Admin'],
|
|
description:
|
|
'Query and filter created archives by type (user or guild), subject ID, requestor, and expiration status. Admins with limited ACLs see only archives matching their permissions.',
|
|
}),
|
|
async (ctx) => {
|
|
const adminArchiveService = ctx.get('adminArchiveService');
|
|
const adminAcls = ctx.get('adminUserAcls');
|
|
const query = ctx.req.valid('query');
|
|
const subjectType = resolveListSubjectType(adminAcls, query.subject_type);
|
|
const result = await adminArchiveService.listArchives({
|
|
subjectType,
|
|
subjectId: query.subject_id ?? undefined,
|
|
requestedBy: query.requested_by ?? undefined,
|
|
limit: query.limit,
|
|
includeExpired: query.include_expired,
|
|
});
|
|
await recordAdminRead(ctx, {
|
|
targetType: 'archive',
|
|
targetId: 0n,
|
|
action: AdminAuditReadActions.LIST_ARCHIVES,
|
|
metadata: {
|
|
subject_type: subjectType,
|
|
subject_user_id: subjectType === 'user' ? query.subject_id : undefined,
|
|
subject_guild_id: subjectType === 'guild' ? query.subject_id : undefined,
|
|
requested_by_user_id: query.requested_by,
|
|
limit: query.limit,
|
|
include_expired: query.include_expired,
|
|
result_count: result.length,
|
|
},
|
|
});
|
|
return ctx.json({archives: result}, 200);
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/archives/:subject_type/:subject_id/:archive_id',
|
|
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAnyAdminACL([AdminACLs.ARCHIVE_VIEW_ALL, AdminACLs.ARCHIVE_TRIGGER_USER, AdminACLs.ARCHIVE_TRIGGER_GUILD]),
|
|
Validator('param', ArchivePathParam),
|
|
OpenAPI({
|
|
operationId: 'get_admin_archive',
|
|
summary: 'Get archive details',
|
|
responseSchema: GetArchiveResponseSchema,
|
|
statusCode: 200,
|
|
security: ['adminApiKey'],
|
|
tags: ['Admin'],
|
|
description:
|
|
'Retrieve metadata for a specific archive including its status, creation time, expiration, and file location. Does not return the archive contents themselves.',
|
|
}),
|
|
async (ctx) => {
|
|
const adminArchiveService = ctx.get('adminArchiveService');
|
|
const adminAcls = ctx.get('adminUserAcls');
|
|
const params = ctx.req.valid('param');
|
|
requireArchiveSubjectAccess(adminAcls, params.subject_type);
|
|
const archive = await adminArchiveService.getArchive(params.subject_type, params.subject_id, params.archive_id);
|
|
await recordAdminRead(ctx, {
|
|
targetType: params.subject_type,
|
|
targetId: params.subject_id,
|
|
action: AdminAuditReadActions.GET_ARCHIVE,
|
|
metadata: {archive_id: params.archive_id, found: archive !== null},
|
|
});
|
|
return ctx.json({archive}, 200);
|
|
},
|
|
);
|
|
app.get(
|
|
'/admin/archives/:subject_type/:subject_id/:archive_id/download',
|
|
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
|
|
requireAnyAdminACL([AdminACLs.ARCHIVE_VIEW_ALL, AdminACLs.ARCHIVE_TRIGGER_USER, AdminACLs.ARCHIVE_TRIGGER_GUILD]),
|
|
Validator('param', ArchivePathParam),
|
|
OpenAPI({
|
|
operationId: 'get_admin_archive_download',
|
|
summary: 'Get archive download URL',
|
|
responseSchema: DownloadUrlResponseSchema,
|
|
statusCode: 200,
|
|
security: ['adminApiKey'],
|
|
tags: ['Admin'],
|
|
description:
|
|
'Generate a time-limited download link to the archive file. The URL provides direct access to download the compressed archive contents.',
|
|
}),
|
|
async (ctx) => {
|
|
const adminArchiveService = ctx.get('adminArchiveService');
|
|
const adminAcls = ctx.get('adminUserAcls');
|
|
const params = ctx.req.valid('param');
|
|
requireArchiveSubjectAccess(adminAcls, params.subject_type);
|
|
const result = await adminArchiveService.getDownloadUrl(
|
|
params.subject_type,
|
|
params.subject_id,
|
|
params.archive_id,
|
|
);
|
|
await recordAdminRead(ctx, {
|
|
targetType: params.subject_type,
|
|
targetId: params.subject_id,
|
|
action: AdminAuditReadActions.GET_ARCHIVE_DOWNLOAD_URL,
|
|
metadata: {archive_id: params.archive_id},
|
|
});
|
|
return ctx.json(result, 200);
|
|
},
|
|
);
|
|
}
|