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

1096 lines
40 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 {mapUserToAdminResponse} from '@app/api/admin/models/UserTypes';
import {createUserID} from '@app/api/BrandedTypes';
import {requireAdminACL} 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 {inputValidationErrorFromZodIssues, Validator} from '@app/api/Validator';
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
import {MissingACLError} from '@fluxer/errors/src/domains/core/MissingACLError';
import {ListUserGuildsResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
import {SearchUsersResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import {
AdminAclListResponse,
AdminUserAclsRequest,
AdminUserBanNoteRequest,
AdminUserBanRequest,
AdminUserChangeLogQuery,
AdminUserClearFieldsRequest,
AdminUserDeletionCancelRequest,
AdminUserDeletionScheduleRequest,
AdminUserDmChannelListQuery,
AdminUserDmChannelListResponse,
AdminUserDobUpdateRequest,
AdminUserEmailUpdateRequest,
AdminUserFlagsUpdateRequest,
AdminUserGuildListQuery,
AdminUserListQuery,
AdminUserPremiumFlagsUpdateRequest,
AdminUserRelationshipCategoryQuery,
AdminUserRelationshipParam,
AdminUsersMeResponse,
AdminUserTraitsRequest,
AdminUserUnbanRequest,
AdminUserUsernameUpdateRequest,
AdminUserWebAuthnCredentialParam,
ListUserChangeLogResponseSchema,
ListUserRelationshipsResponse,
ListUserSessionsResponse,
LookupUserRequest,
LookupUserResponse,
RemoveUserRelationshipsResponse,
TerminateSessionsResponse,
UserMutationResponse,
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
import {WebAuthnCredentialListResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
import {UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
function requireSelectorACL(granted: ReadonlySet<string>, acl: string): void {
if (!granted.has(acl) && !granted.has(AdminACLs.WILDCARD)) {
throw new MissingACLError(acl);
}
}
export function UserAdminController(app: HonoApp) {
app.get(
'/admin/acls',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.AUTHENTICATE),
OpenAPI({
operationId: 'list_admin_acls',
summary: 'List admin permissions',
responseSchema: AdminAclListResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Returns every access control permission the admin API recognises. This is the registry admin accounts and admin API keys draw their permissions from. Requires AUTHENTICATE permission.',
}),
async (ctx) => {
const acls = Object.values(AdminACLs);
await recordAdminRead(ctx, {
targetType: 'admin_acl',
targetId: 0n,
action: AdminAuditReadActions.LIST_ADMIN_ACLS,
metadata: {acl_count: acls.length},
});
return ctx.json({acls});
},
);
app.get(
'/admin/users/@me',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.AUTHENTICATE),
OpenAPI({
operationId: 'get_current_admin_user',
summary: 'Get current admin',
responseSchema: AdminUsersMeResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Return the admin the request was authenticated as, with the admin permissions, roles, and metadata of the account. Requires AUTHENTICATE permission.',
}),
async (ctx) => {
const adminUser = ctx.get('user');
const cacheService = ctx.get('cacheService');
return ctx.json({
user: await mapUserToAdminResponse(adminUser, cacheService),
});
},
);
app.get(
'/admin/users',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.USER_LOOKUP),
Validator('query', AdminUserListQuery),
OpenAPI({
operationId: 'list_admin_users',
summary: 'List users',
responseSchema: SearchUsersResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Lists and searches users. Exactly one selector is honoured, in this precedence order: user_id, resolve, email, last_active_ip, then the indexed q search. The resolve selector takes one exact identifier, which may be a username#discriminator tag, a user ID, an email address, or a Stripe subscription ID. The email and user_id selectors ignore limit and offset. Requires USER_LOOKUP permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserAcls = ctx.get('adminUserAcls');
const query = ctx.req.valid('query');
const userIds =
query.user_id === undefined ? undefined : Array.isArray(query.user_id) ? query.user_id : [query.user_id];
if (userIds || query.resolve !== undefined) {
if (!userIds && query.resolve?.includes('@')) {
requireSelectorACL(adminUserAcls, AdminACLs.USER_VIEW_EMAIL);
}
const parsed = LookupUserRequest.safeParse(userIds ? {user_ids: userIds} : {query: query.resolve});
if (!parsed.success) {
throw inputValidationErrorFromZodIssues(parsed.error.issues);
}
const {users} = await adminService.userService.lookupService.lookupUser(parsed.data, adminUserAcls);
await recordAdminRead(ctx, {
targetType: 'user',
targetId: 0n,
action: AdminAuditReadActions.SEARCH_USERS,
metadata: {
selector: userIds ? 'user_id' : 'resolve',
user_id: userIds?.length === 1 ? userIds[0] : undefined,
user_id_count: userIds?.length,
result_count: users.length,
total: users.length,
},
});
return ctx.json({users, total: users.length});
}
const selector = query.email?.trim() ? 'email' : query.last_active_ip?.trim() ? 'last_active_ip' : 'search';
if (selector === 'email') {
requireSelectorACL(adminUserAcls, AdminACLs.USER_VIEW_EMAIL);
} else if (selector === 'last_active_ip') {
requireSelectorACL(adminUserAcls, AdminACLs.USER_VIEW_IP);
}
const response = await adminService.searchService.searchUsers(
{
query: query.q,
email: query.email,
last_active_ip: query.last_active_ip,
limit: query.limit,
offset: query.offset,
},
adminUserAcls,
);
await recordAdminRead(ctx, {
targetType: 'user',
targetId: 0n,
action: AdminAuditReadActions.SEARCH_USERS,
metadata: {
selector,
has_query: selector === 'search' && query.q?.trim() ? true : undefined,
limit: selector === 'email' ? undefined : query.limit,
offset: selector === 'email' ? undefined : query.offset,
result_count: response.users.length,
total: response.total,
},
});
return ctx.json(response);
},
);
app.get(
'/admin/users/:user_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.USER_LOOKUP),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'get_admin_user',
summary: 'Get user',
responseSchema: LookupUserResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Look up one detailed user profile by ID. Returns account status, permissions, and metadata. The email address, date of birth, and IP address are redacted without the matching view permissions. Requires USER_LOOKUP permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
const response = await adminService.userService.lookupService.lookupUser({user_ids: [userId]}, adminUserAcls);
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.GET_USER,
metadata: {found: response.users.length > 0},
});
return ctx.json(response);
},
);
app.get(
'/admin/users/:user_id/guilds',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.USER_LIST_GUILDS),
Validator('param', UserIdParam),
Validator('query', AdminUserGuildListQuery),
OpenAPI({
operationId: 'list_admin_user_guilds',
summary: 'List user communities',
responseSchema: ListUserGuildsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'List all guilds a user is a member of, optionally with approximate member and presence counts. Shows roles and join dates. Requires USER_LIST_GUILDS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {user_id: userId} = ctx.req.valid('param');
const query = ctx.req.valid('query');
const response = await adminService.guildServiceAggregate.lookupService.listUserGuilds({
user_id: userId,
...query,
});
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.LIST_USER_GUILDS,
metadata: {
before_guild_id: query.before,
after_guild_id: query.after,
limit: query.limit,
with_counts: query.with_counts,
guild_count: response.guilds.length,
},
});
return ctx.json(response);
},
);
app.get(
'/admin/users/:user_id/dm-channels',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.USER_LIST_DM_CHANNELS),
Validator('param', UserIdParam),
Validator('query', AdminUserDmChannelListQuery),
OpenAPI({
operationId: 'list_admin_user_dm_channels',
summary: 'List user direct message channels',
responseSchema: AdminUserDmChannelListResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'List the historical one-to-one direct message channels of a user with cursor pagination, or the group direct message channels they are a recipient of when type is group_dm. Requires USER_LIST_DM_CHANNELS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {user_id: userId} = ctx.req.valid('param');
const {type, ...pagination} = ctx.req.valid('query');
if (type === 'group_dm') {
const response = await adminService.userService.listUserGroupDmChannels({user_id: userId});
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.LIST_USER_DM_CHANNELS,
metadata: {type, channel_count: response.channels.length},
});
return ctx.json(response);
}
const response = await adminService.userService.listUserDmChannels({user_id: userId, ...pagination});
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.LIST_USER_DM_CHANNELS,
metadata: {
type,
before_channel_id: pagination.before,
after_channel_id: pagination.after,
limit: pagination.limit,
channel_count: response.channels.length,
},
});
return ctx.json(response);
},
);
app.get(
'/admin/users/:user_id/change-log',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.USER_VIEW_CONTACT_LOG),
Validator('param', UserIdParam),
Validator('query', AdminUserChangeLogQuery),
OpenAPI({
operationId: 'list_admin_user_change_log',
summary: 'List user contact change log',
responseSchema: ListUserChangeLogResponseSchema,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Retrieve the identity and contact change log history for a user. Shows all profile modifications, admin actions, and account changes with timestamps. Email values are redacted without USER_VIEW_EMAIL. Requires USER_VIEW_CONTACT_LOG permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
const query = ctx.req.valid('query');
const response = await adminService.userService.listUserChangeLog({user_id: userId, ...query}, adminUserAcls);
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.LIST_USER_CHANGE_LOG,
metadata: {
limit: query.limit,
has_page_token: query.page_token === undefined ? undefined : true,
entry_count: response.entries.length,
},
});
return ctx.json(response);
},
);
app.get(
'/admin/users/:user_id/relationships',
RateLimitMiddleware(RateLimitConfigs.ADMIN_LOOKUP),
requireAdminACL(AdminACLs.USER_LIST_RELATIONSHIPS),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'list_admin_user_relationships',
summary: 'List user relationships',
responseSchema: ListUserRelationshipsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
"List a user's friends, incoming and outgoing friend requests, and blocked users. Requires USER_LIST_RELATIONSHIPS permission.",
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const {user_id: userId} = ctx.req.valid('param');
const response = await adminService.relationshipService.listRelationships({user_id: userId});
await recordAdminRead(ctx, {
targetType: 'user',
targetId: userId,
action: AdminAuditReadActions.LIST_USER_RELATIONSHIPS,
metadata: {
friend_count: response.friends.length,
incoming_request_count: response.incoming_requests.length,
outgoing_request_count: response.outgoing_requests.length,
blocked_count: response.blocked.length,
},
});
return ctx.json(response);
},
);
app.delete(
'/admin/users/:user_id/relationships',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_REMOVE_RELATIONSHIP),
Validator('param', UserIdParam),
Validator('query', AdminUserRelationshipCategoryQuery),
OpenAPI({
operationId: 'clear_admin_user_relationships',
summary: 'Clear user relationships',
responseSchema: RemoveUserRelationshipsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Bulk-remove every relationship of the chosen category (friend, incoming_request, outgoing_request, blocked) for a user. Mirror entries on the other party are removed for friend, incoming_request, and outgoing_request. Requires USER_REMOVE_RELATIONSHIP permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId} = ctx.req.valid('param');
const {category} = ctx.req.valid('query');
return ctx.json(
await adminService.relationshipService.removeRelationshipsByCategory(
{user_id: userId, category},
adminUserId,
auditLogReason,
),
);
},
);
app.delete(
'/admin/users/:user_id/relationships/:target_user_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_REMOVE_RELATIONSHIP),
Validator('param', AdminUserRelationshipParam),
Validator('query', AdminUserRelationshipCategoryQuery),
OpenAPI({
operationId: 'remove_admin_user_relationship',
summary: 'Remove user relationship',
responseSchema: null,
statusCode: 204,
security: 'adminApiKey',
tags: 'Admin',
description:
'Remove a single relationship row for a user. For friend and outgoing_request, the mirror entry on the other user is also removed. Dispatches RELATIONSHIP_REMOVE gateway events. Requires USER_REMOVE_RELATIONSHIP permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId, target_user_id: targetUserId} = ctx.req.valid('param');
const {category} = ctx.req.valid('query');
await adminService.relationshipService.removeRelationship(
{user_id: userId, target_user_id: targetUserId, category},
adminUserId,
auditLogReason,
);
return ctx.body(null, 204);
},
);
app.get(
'/admin/users/:user_id/sessions',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_LIST_SESSIONS),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'list_admin_user_sessions',
summary: 'List user sessions',
responseSchema: ListUserSessionsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'List all active user sessions across devices. Shows device info, IP, last activity, and creation time. Requires USER_LIST_SESSIONS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.securityService.listUserSessions(
userId,
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.delete(
'/admin/users/:user_id/sessions',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'terminate_admin_user_sessions',
summary: 'Terminate user sessions',
responseSchema: TerminateSessionsResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Terminate all active user sessions across devices. Forces user to re-authenticate on next connection. Creates audit log entry. Requires USER_UPDATE_FLAGS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.securityService.terminateSessions(
{user_id: userId},
adminUserId,
auditLogReason,
),
);
},
);
app.get(
'/admin/users/:user_id/webauthn-credentials',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_MFA),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'list_admin_user_webauthn_credentials',
summary: 'List user WebAuthn credentials',
responseSchema: WebAuthnCredentialListResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'List all WebAuthn credentials (passkeys/security keys) registered for a user. Returns credential names, creation dates, and last usage. Creates audit log entry. Requires USER_UPDATE_MFA permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.securityService.listWebAuthnCredentials(
{user_id: userId},
adminUserId,
auditLogReason,
),
);
},
);
app.delete(
'/admin/users/:user_id/webauthn-credentials/:credential_id',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_MFA),
Validator('param', AdminUserWebAuthnCredentialParam),
OpenAPI({
operationId: 'delete_admin_user_webauthn_credential',
summary: 'Delete user WebAuthn credential',
responseSchema: null,
statusCode: 204,
security: 'adminApiKey',
tags: 'Admin',
description:
'Delete a specific WebAuthn credential (passkey/security key) from a user account. Creates audit log entry. Requires USER_UPDATE_MFA permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId, credential_id: credentialId} = ctx.req.valid('param');
await adminService.userService.securityService.deleteWebAuthnCredential(
{user_id: userId, credential_id: credentialId},
adminUserId,
auditLogReason,
);
return ctx.body(null, 204);
},
);
app.delete(
'/admin/users/:user_id/mfa',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_MFA),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'disable_admin_user_mfa',
summary: 'Disable user MFA',
responseSchema: null,
statusCode: 204,
security: 'adminApiKey',
tags: 'Admin',
description:
'Disable two-factor authentication for user account. Removes all authenticators. Creates audit log entry. Requires USER_UPDATE_MFA permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId} = ctx.req.valid('param');
await adminService.userService.securityService.disableMfa({user_id: userId}, adminUserId, auditLogReason);
return ctx.body(null, 204);
},
);
app.delete(
'/admin/users/:user_id/profile-fields',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_PROFILE),
Validator('param', UserIdParam),
Validator('json', AdminUserClearFieldsRequest),
OpenAPI({
operationId: 'clear_admin_user_profile_fields',
summary: 'Clear user profile fields',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Clear or reset user profile fields such as bio, avatar, or status. Creates audit log entry. Requires USER_UPDATE_PROFILE permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.profileService.clearUserFields(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.patch(
'/admin/users/:user_id/username',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_USERNAME),
Validator('param', UserIdParam),
Validator('json', AdminUserUsernameUpdateRequest),
OpenAPI({
operationId: 'update_admin_user_username',
summary: 'Change user username',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Change user username. New username must meet requirements and be unique. Creates audit log entry. Requires USER_UPDATE_USERNAME permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.profileService.changeUsername(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.patch(
'/admin/users/:user_id/email',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_EMAIL),
Validator('param', UserIdParam),
Validator('json', AdminUserEmailUpdateRequest),
OpenAPI({
operationId: 'update_admin_user_email',
summary: 'Change user email',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Change user email address. New email must be valid and unique. Marks email as verified. Creates audit log entry. Requires USER_UPDATE_EMAIL permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.profileService.changeEmail(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.put(
'/admin/users/:user_id/email-verification',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_EMAIL),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'verify_admin_user_email',
summary: 'Verify user email',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Manually verify user email address without requiring confirmation link. Bypasses email verification requirement. Creates audit log entry. Requires USER_UPDATE_EMAIL permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.profileService.verifyUserEmail(
{user_id: userId},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.post(
'/admin/users/:user_id/verification-email',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_EMAIL),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'resend_admin_user_verification_email',
summary: 'Resend user verification email',
responseSchema: null,
statusCode: 204,
security: 'adminApiKey',
tags: 'Admin',
description:
'Resend the account verification email for a user. Creates audit log entry and honours email verification resend limits. Requires USER_UPDATE_EMAIL permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId} = ctx.req.valid('param');
await adminService.userService.securityService.resendVerificationEmail(
{user_id: userId},
adminUserId,
auditLogReason,
);
return ctx.body(null, 204);
},
);
app.post(
'/admin/users/:user_id/password-reset',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_EMAIL),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'send_admin_user_password_reset',
summary: 'Send user password reset',
responseSchema: null,
statusCode: 204,
security: 'adminApiKey',
tags: 'Admin',
description:
'Send password reset email to user with reset link. User must use link within expiry window. Creates audit log entry. Requires USER_UPDATE_EMAIL permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const {user_id: userId} = ctx.req.valid('param');
await adminService.userService.securityService.sendPasswordReset({user_id: userId}, adminUserId, auditLogReason);
return ctx.body(null, 204);
},
);
app.put(
'/admin/users/:user_id/ban',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_TEMP_BAN),
Validator('param', UserIdParam),
Validator('json', AdminUserBanRequest),
OpenAPI({
operationId: 'ban_admin_user',
summary: 'Ban user',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Apply temporary ban to user account for specified duration, or permanently with a duration of zero. Prevents login and guild operations. Automatically lifts after expiry. Creates audit log entry. Requires USER_TEMP_BAN permission. Emails the user for temporary bans unless notify_user is false. Permanent bans are never emailed.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.banService.tempBanUser(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.delete(
'/admin/users/:user_id/ban',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_TEMP_BAN),
Validator('param', UserIdParam),
Validator('json', AdminUserUnbanRequest),
OpenAPI({
operationId: 'unban_admin_user',
summary: 'Unban user',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Immediately remove the ban from the user account. Emails the user only when notify_user is true, the ban was still in force and the account is not closed or pending deletion. The email shows public_reason and never the audit log reason. Creates audit log entry. Requires USER_TEMP_BAN permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.banService.unbanUser(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.post(
'/admin/users/:user_id/ban/notes',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_TEMP_BAN),
Validator('param', UserIdParam),
Validator('json', AdminUserBanNoteRequest),
OpenAPI({
operationId: 'annotate_admin_user_ban',
summary: 'Add a note to a user ban',
responseSchema: null,
statusCode: 204,
security: 'adminApiKey',
tags: 'Admin',
description:
'Append a note to the current ban of a user. The note is recorded as the reason of a new annotate_ban audit log entry whose metadata names the ban audit log entry. Earlier entries are never changed. Requires USER_TEMP_BAN permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const {user_id: userId} = ctx.req.valid('param');
await adminService.userService.banService.annotateBan({user_id: userId, ...ctx.req.valid('json')}, adminUserId);
return ctx.body(null, 204);
},
);
app.put(
'/admin/users/:user_id/deletion',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_DELETE),
Validator('param', UserIdParam),
Validator('json', AdminUserDeletionScheduleRequest),
OpenAPI({
operationId: 'schedule_admin_user_deletion',
summary: 'Schedule user deletion',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Schedule user account for deletion after grace period. Account will be fully deleted with all content unless cancellation is executed. When a deletion is already scheduled, the request must name it in replace_pending_deletion_at or it returns 409. Records who scheduled the deletion. Creates audit log entry. Requires USER_DELETE permission. Emails the user unless notify_user is false. The email depends on reason_code: user requested and inactivity get neutral wording, other codes get enforcement wording with an appeal path.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.deletionService.scheduleAccountDeletion(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.delete(
'/admin/users/:user_id/deletion',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_DELETE),
Validator('param', UserIdParam),
Validator('json', AdminUserDeletionCancelRequest),
OpenAPI({
operationId: 'cancel_admin_user_deletion',
summary: 'Cancel user deletion',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Cancel the scheduled account deletion named by expected_pending_deletion_at. Returns 409 when a different deletion is pending and 400 when none is. The user is emailed only when notify_user is true, and the email never includes the audit log reason. Creates audit log entry recording the cancelled deletion. Requires USER_DELETE permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.deletionService.cancelAccountDeletion(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.delete(
'/admin/users/:user_id/message-deletion',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_CANCEL_BULK_MESSAGE_DELETION),
Validator('param', UserIdParam),
OpenAPI({
operationId: 'cancel_admin_user_message_deletion',
summary: 'Cancel bulk message deletion',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Cancel a scheduled bulk message deletion job for a user. Prevents deletion of user messages across guilds. Creates audit log entry. Requires USER_CANCEL_BULK_MESSAGE_DELETION permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.cancelBulkMessageDeletion(
{user_id: userId},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.put(
'/admin/users/:user_id/acls',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.ACL_SET_USER),
Validator('param', UserIdParam),
Validator('json', AdminUserAclsRequest),
OpenAPI({
operationId: 'set_admin_user_acls',
summary: 'Set user admin permissions',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Replace the admin ACL permissions granted to a user. Controls admin capabilities and panel access. The permissions accepted are the ones listed by GET /admin/acls. Creates audit log entry. Requires ACL_SET_USER permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.securityService.setUserAcls(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.put(
'/admin/users/:user_id/traits',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_TRAITS),
Validator('param', UserIdParam),
Validator('json', AdminUserTraitsRequest),
OpenAPI({
operationId: 'set_admin_user_traits',
summary: 'Set user traits',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Set or update user trait attributes and profile metadata. Traits customize user display and features. Creates audit log entry. Requires USER_UPDATE_TRAITS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.securityService.setUserTraits(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
app.patch(
'/admin/users/:user_id/flags',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
Validator('param', UserIdParam),
Validator('json', AdminUserFlagsUpdateRequest),
OpenAPI({
operationId: 'update_admin_user_flags',
summary: 'Update user flags',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Add or remove user flags to control account features and restrictions. Flags determine verification status and special properties. Creates audit log entry. Requires USER_UPDATE_FLAGS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const body = ctx.req.valid('json');
const userId = createUserID(ctx.req.valid('param').user_id);
const addFlags = body.add_flags.map((flag) => BigInt(flag));
const removeFlags = body.remove_flags.map((flag) => BigInt(flag));
return ctx.json(
await adminService.userService.securityService.updateUserFlags({
userId,
data: {addFlags, removeFlags},
adminUserId,
auditLogReason,
acls: adminUserAcls,
}),
);
},
);
app.patch(
'/admin/users/:user_id/premium-flags',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_FLAGS),
Validator('param', UserIdParam),
Validator('json', AdminUserPremiumFlagsUpdateRequest),
OpenAPI({
operationId: 'update_admin_user_premium_flags',
summary: 'Update user premium flags',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Add or remove premium-related flags on a user account (badge visibility, override, purchase block, etc). Creates audit log entry. Requires USER_UPDATE_FLAGS permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const body = ctx.req.valid('json');
const userId = createUserID(ctx.req.valid('param').user_id);
return ctx.json(
await adminService.userService.securityService.updatePremiumFlags({
userId,
data: {addFlags: body.add_flags, removeFlags: body.remove_flags},
adminUserId,
auditLogReason,
acls: adminUserAcls,
}),
);
},
);
app.patch(
'/admin/users/:user_id/date-of-birth',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_DOB),
Validator('param', UserIdParam),
Validator('json', AdminUserDobUpdateRequest),
OpenAPI({
operationId: 'update_admin_user_date_of_birth',
summary: 'Change user date of birth',
responseSchema: UserMutationResponse,
statusCode: 200,
security: 'adminApiKey',
tags: 'Admin',
description:
'Update user date of birth. May affect age-restricted content access. Creates audit log entry. Requires USER_UPDATE_DOB permission.',
}),
async (ctx) => {
const adminService = ctx.get('adminService');
const adminUserId = ctx.get('adminUserId');
const auditLogReason = ctx.get('auditLogReason');
const adminUserAcls = ctx.get('adminUserAcls');
const {user_id: userId} = ctx.req.valid('param');
return ctx.json(
await adminService.userService.profileService.changeDob(
{user_id: userId, ...ctx.req.valid('json')},
adminUserId,
auditLogReason,
adminUserAcls,
),
);
},
);
}