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

169 lines
6.8 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {AdminAuditReadActions} from '@app/api/admin/AdminAuditActions';
import {recordAdminRead} from '@app/api/admin/AdminAuditRecorder';
import {createApplicationID, 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 {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import {MissingACLError} from '@fluxer/errors/src/domains/core/MissingACLError';
import {
AdminApplicationIdParam,
ApplicationUpdateResponse,
ListApplicationsQuery,
ListApplicationsResponse,
LookupApplicationResponse,
TransferApplicationOwnershipRequest,
} from '@fluxer/schema/src/domains/admin/AdminApplicationSchemas';
import {UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
function requireRequestAdminACL(granted: ReadonlySet<string>, required: string): void {
if (!granted.has(required) && !granted.has(AdminACLs.WILDCARD)) {
throw new MissingACLError(required);
}
}
export function ApplicationAdminController(app: HonoApp) {
app.get(
'/admin/applications',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAnyAdminACL([AdminACLs.APPLICATION_LOOKUP, AdminACLs.APPLICATION_LIST_BY_OWNER]),
Validator('query', ListApplicationsQuery),
OpenAPI({
operationId: 'list_admin_applications',
summary: 'List applications',
description:
'Lists OAuth2 applications and bots. Pass owner_id to list the applications a user owns, or guild_id to list the applications whose bot users are members of a guild. Exactly one of the two is required. owner_id requires APPLICATION_LIST_BY_OWNER permission, guild_id requires APPLICATION_LOOKUP permission.',
responseSchema: ListApplicationsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {owner_id: ownerId, guild_id: guildId} = ctx.req.valid('query');
if (guildId != null && ownerId != null) {
throw InputValidationError.create('guild_id', 'Only one of owner_id and guild_id may be supplied');
}
if (guildId != null) {
requireRequestAdminACL(ctx.get('adminUserAcls'), AdminACLs.APPLICATION_LOOKUP);
const response = await adminService.applicationService.listGuildApplications(createGuildID(guildId));
await recordAdminRead(ctx, {
targetType: 'guild',
targetId: guildId,
action: AdminAuditReadActions.LIST_GUILD_APPLICATIONS,
metadata: {application_count: response.applications.length},
});
return ctx.json(response);
}
if (ownerId != null) {
requireRequestAdminACL(ctx.get('adminUserAcls'), AdminACLs.APPLICATION_LIST_BY_OWNER);
const response = await adminService.applicationService.listUserApplications(createUserID(ownerId));
await recordAdminRead(ctx, {
targetType: 'user',
targetId: ownerId,
action: AdminAuditReadActions.LIST_USER_APPLICATIONS,
metadata: {application_count: response.applications.length},
});
return ctx.json(response);
}
throw InputValidationError.create('owner_id', 'One of owner_id and guild_id is required');
},
);
app.get(
'/admin/users/:user_id/applications',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.APPLICATION_LIST_BY_OWNER),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'list_admin_user_applications',
summary: 'List user applications',
description: 'Lists the OAuth2 applications and bots a user owns. Requires APPLICATION_LIST_BY_OWNER permission.',
responseSchema: ListApplicationsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const userId = createUserID(ctx.req.valid('param').user_id);
const response = await adminService.applicationService.listUserApplications(userId);
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.LIST_USER_APPLICATIONS,
metadata: {application_count: response.applications.length},
});
return ctx.json(response);
},
);
app.get(
'/admin/applications/:application_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.APPLICATION_LOOKUP),
Validator('param', AdminApplicationIdParam),
OpenAPI({
operationId: 'get_admin_application',
summary: 'Get application',
description:
'Retrieves complete application details including ownership, bot user, OAuth2 redirect URIs, and credential status. Requires APPLICATION_LOOKUP permission.',
responseSchema: LookupApplicationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const applicationId = createApplicationID(ctx.req.valid('param').application_id);
const response = await adminService.applicationService.lookupApplication(applicationId);
await recordAdminRead(ctx, {
targetType: 'application',
targetId: applicationId,
action: AdminAuditReadActions.GET_APPLICATION,
metadata: {
found: response.application !== null,
owner_user_id: response.application?.owner_user_id,
bot_user_id: response.application?.bot_user_id,
},
});
return ctx.json(response);
},
);
app.patch(
'/admin/applications/:application_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_GUILD_MODIFY),
requireAdminACL(AdminACLs.APPLICATION_TRANSFER_OWNERSHIP),
Validator('param', AdminApplicationIdParam),
Validator('json', TransferApplicationOwnershipRequest),
OpenAPI({
operationId: 'update_admin_application',
summary: 'Update application',
description:
'Updates an application. Transfers ownership to the user given by new_owner_id, which is used when the owner is inactive or for administrative recovery. Logged to audit log. Requires APPLICATION_TRANSFER_OWNERSHIP permission.',
responseSchema: ApplicationUpdateResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const applicationId = createApplicationID(ctx.req.valid('param').application_id);
return ctx.json(
await adminService.applicationService.transferApplicationOwnership(
applicationId,
ctx.req.valid('json'),
adminUserId,
auditLogReason,
),
);
},
);
}