Files
fluxer/fluxer_api/src/api/admin/controllers/ArchiveAdminController.ts
T

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);
},
);
}