feat(admin): add resend verification email op

This commit is contained in:
Hampus Kraft 2026-02-18 19:04:25 +00:00
parent 2be274e762
commit 08eaad6f76
No known key found for this signature in database
GPG Key ID: 6090864C465A454D
5 changed files with 78 additions and 0 deletions

View File

@ -106,6 +106,7 @@ import type {
ListUserDmChannelsRequest,
ListWebAuthnCredentialsRequest,
LookupUserRequest,
ResendVerificationEmailRequest,
SendPasswordResetRequest,
SetUserAclsRequest,
SetUserBotStatusRequest,
@ -378,6 +379,14 @@ export class AdminService {
return this.userService.verifyUserEmail(data, adminUserId, auditLogReason);
}
async resendVerificationEmail(
data: ResendVerificationEmailRequest,
adminUserId: UserID,
auditLogReason: string | null,
) {
return this.userService.resendVerificationEmail(data, adminUserId, auditLogReason);
}
async sendPasswordReset(data: SendPasswordResetRequest, adminUserId: UserID, auditLogReason: string | null) {
return this.userService.sendPasswordReset(data, adminUserId, auditLogReason);
}

View File

@ -46,6 +46,7 @@ import {
ListWebAuthnCredentialsRequest,
LookupUserRequest,
LookupUserResponse,
ResendVerificationEmailRequest,
ScheduleAccountDeletionRequest,
SendPasswordResetRequest,
SetUserAclsRequest,
@ -312,6 +313,30 @@ export function UserAdminController(app: HonoApp) {
},
);
app.post(
'/admin/users/resend-verification-email',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
requireAdminACL(AdminACLs.USER_UPDATE_EMAIL),
Validator('json', ResendVerificationEmailRequest),
OpenAPI({
operationId: 'admin_resend_verification_email',
summary: 'Resend 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');
await adminService.resendVerificationEmail(ctx.req.valid('json'), adminUserId, auditLogReason);
return ctx.body(null, 204);
},
);
app.post(
'/admin/users/send-password-reset',
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),

View File

@ -41,6 +41,7 @@ import type {
DisableForSuspiciousActivityRequest,
DisableMfaRequest,
ListWebAuthnCredentialsRequest,
ResendVerificationEmailRequest,
SendPasswordResetRequest,
SetUserAclsRequest,
SetUserTraitsRequest,
@ -186,6 +187,34 @@ export class AdminUserSecurityService {
});
}
async resendVerificationEmail(
data: ResendVerificationEmailRequest,
adminUserId: UserID,
auditLogReason: string | null,
) {
const {userRepository, authService, auditService} = this.deps;
const userId = createUserID(data.user_id);
const user = await userRepository.findUnique(userId);
if (!user) {
throw new UnknownUserError();
}
if (!user.email) {
throw InputValidationError.fromCode('email', ValidationErrorCodes.USER_DOES_NOT_HAVE_AN_EMAIL_ADDRESS);
}
await authService.resendVerificationEmail(user);
await auditService.createAuditLog({
adminUserId,
targetType: 'user',
targetId: BigInt(userId),
action: 'resend_verification_email',
auditLogReason,
metadata: new Map([['email', user.email]]),
});
}
async terminateSessions(data: TerminateSessionsRequest, adminUserId: UserID, auditLogReason: string | null) {
const {userRepository, authService, auditService} = this.deps;
const userId = createUserID(data.user_id);

View File

@ -55,6 +55,7 @@ import type {
ListUserDmChannelsRequest,
ListWebAuthnCredentialsRequest,
LookupUserRequest,
ResendVerificationEmailRequest,
ScheduleAccountDeletionRequest,
SendPasswordResetRequest,
SetUserAclsRequest,
@ -221,6 +222,14 @@ export class AdminUserService {
return this.securityService.sendPasswordReset(data, adminUserId, auditLogReason);
}
async resendVerificationEmail(
data: ResendVerificationEmailRequest,
adminUserId: UserID,
auditLogReason: string | null,
) {
return this.securityService.resendVerificationEmail(data, adminUserId, auditLogReason);
}
async terminateSessions(data: TerminateSessionsRequest, adminUserId: UserID, auditLogReason: string | null) {
return this.securityService.terminateSessions(data, adminUserId, auditLogReason);
}

View File

@ -272,6 +272,12 @@ export const VerifyUserEmailRequest = z.object({
export type VerifyUserEmailRequest = z.infer<typeof VerifyUserEmailRequest>;
export const ResendVerificationEmailRequest = z.object({
user_id: SnowflakeType.describe('ID of the user to resend verification email to'),
});
export type ResendVerificationEmailRequest = z.infer<typeof ResendVerificationEmailRequest>;
export const SendPasswordResetRequest = z.object({
user_id: SnowflakeType.describe('ID of the user to send password reset to'),
});