mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(api): send correct staff emails and allow suppressing them (#3048)
This commit is contained in:
1 parent
f0b3c82cfd
commit
39f9beda5a
113 files changed
+2496
-206
No files matched your search
@@ -86,6 +86,7 @@ async function queueBulkJob(
|
||||
reason_code: body.reason_code,
|
||||
days_until_deletion: body.days_until_deletion,
|
||||
public_reason: body.public_reason ?? null,
|
||||
notify_user: body.notify_user,
|
||||
admin_user_id: adminUserId.toString(),
|
||||
audit_log_reason: auditLogReason,
|
||||
},
|
||||
|
||||
@@ -167,7 +167,7 @@ export function ReportAdminController(app: HonoApp) {
|
||||
operationId: 'update_admin_report',
|
||||
summary: 'Update report',
|
||||
description:
|
||||
'Moves a report to the resolved status with an optional public comment shown to the reporter. Marks the report as handled, notifies the reporter, and creates an audit log entry. Requires REPORT_RESOLVE permission.',
|
||||
'Moves a report to the resolved status with an optional public comment shown to the reporter. Marks the report as handled, notifies the reporter by system DM and email unless notify_reporter is false, and creates an audit log entry. Requires REPORT_RESOLVE permission.',
|
||||
responseSchema: ResolveReportResponse,
|
||||
statusCode: 200,
|
||||
security: 'adminApiKey',
|
||||
@@ -178,13 +178,14 @@ export function ReportAdminController(app: HonoApp) {
|
||||
const adminUserId = ctx.get('adminUserId');
|
||||
const auditLogReason = ctx.get('auditLogReason');
|
||||
const {report_id} = ctx.req.valid('param');
|
||||
const {public_comment} = ctx.req.valid('json');
|
||||
const {public_comment, notify_reporter} = ctx.req.valid('json');
|
||||
return ctx.json(
|
||||
await adminService.reportServiceAggregate.resolveReport(
|
||||
createReportID(report_id),
|
||||
adminUserId,
|
||||
public_comment || null,
|
||||
auditLogReason,
|
||||
notify_reporter,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
AdminUserSystemStatusRequest,
|
||||
AdminUsersMeResponse,
|
||||
AdminUserTraitsRequest,
|
||||
AdminUserUnbanRequest,
|
||||
AdminUserUsernameUpdateRequest,
|
||||
AdminUserWebAuthnCredentialParam,
|
||||
ListUserChangeLogResponseSchema,
|
||||
@@ -825,7 +826,7 @@ export function UserAdminController(app: HonoApp) {
|
||||
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.',
|
||||
'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');
|
||||
@@ -848,6 +849,7 @@ export function UserAdminController(app: HonoApp) {
|
||||
RateLimitMiddleware(RateLimitConfigs.ADMIN_USER_MODIFY),
|
||||
requireAdminACL(AdminACLs.USER_TEMP_BAN),
|
||||
Validator('param', UserIdParam),
|
||||
Validator('json', AdminUserUnbanRequest),
|
||||
OpenAPI({
|
||||
operationId: 'unban_admin_user',
|
||||
summary: 'Unban user',
|
||||
@@ -856,7 +858,7 @@ export function UserAdminController(app: HonoApp) {
|
||||
security: 'adminApiKey',
|
||||
tags: 'Admin',
|
||||
description:
|
||||
'Immediately remove temporary ban from user account. User can log in and access guilds again. Creates audit log entry. Requires USER_TEMP_BAN permission.',
|
||||
'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');
|
||||
@@ -866,7 +868,7 @@ export function UserAdminController(app: HonoApp) {
|
||||
const {user_id: userId} = ctx.req.valid('param');
|
||||
return ctx.json(
|
||||
await adminService.userService.banService.unbanUser(
|
||||
{user_id: userId},
|
||||
{user_id: userId, ...ctx.req.valid('json')},
|
||||
adminUserId,
|
||||
auditLogReason,
|
||||
adminUserAcls,
|
||||
@@ -912,7 +914,7 @@ export function UserAdminController(app: HonoApp) {
|
||||
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.',
|
||||
'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');
|
||||
@@ -1237,7 +1239,7 @@ export function UserAdminController(app: HonoApp) {
|
||||
security: 'adminApiKey',
|
||||
tags: 'Admin',
|
||||
description:
|
||||
'Disable user account due to suspicious activity or abuse. Account is locked pending review. User cannot access services. Creates audit log entry. Requires USER_DISABLE_SUSPICIOUS permission.',
|
||||
'Disable user account due to suspicious activity or abuse. Account is locked pending review. User cannot access services. Emails the user unless notify_user is false. Creates audit log entry. Requires USER_DISABLE_SUSPICIOUS permission.',
|
||||
}),
|
||||
async (ctx) => {
|
||||
const adminService = ctx.get('adminService');
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Logger} from '@app/api/Logger';
|
||||
|
||||
export async function trySendAdminNotification(
|
||||
send: () => Promise<boolean>,
|
||||
context: {action: string; targetId: string},
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const sent = await send();
|
||||
if (!sent) {
|
||||
Logger.warn(context, 'Admin action notification was not sent');
|
||||
}
|
||||
return sent;
|
||||
} catch (error) {
|
||||
Logger.warn({error, ...context}, 'Failed to send admin action notification');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type {ApiContext} from '@app/api/ApiContext';
|
||||
import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService';
|
||||
import {trySendAdminNotification} from '@app/api/admin/services/AdminNotification';
|
||||
import {
|
||||
type ChannelID,
|
||||
createReportID,
|
||||
@@ -103,10 +104,37 @@ export class AdminReportService {
|
||||
adminUserId: UserID,
|
||||
publicComment: string | null,
|
||||
auditLogReason: string | null,
|
||||
notifyReporter: boolean,
|
||||
) {
|
||||
const {reportService, auditService} = this.deps;
|
||||
const {users: userRepository, email: emailService} = this.deps.apiContext.services;
|
||||
const resolvedReport = await reportService.resolveReport(reportId, adminUserId, publicComment, auditLogReason);
|
||||
let reporterDmSent = false;
|
||||
let reporterEmailSent = false;
|
||||
const reporter =
|
||||
notifyReporter && resolvedReport.reporterId ? await userRepository.findUnique(resolvedReport.reporterId) : null;
|
||||
if (reporter) {
|
||||
const commentForTemplate = publicComment ?? '';
|
||||
reporterDmSent = await this.sendResolvedReportSystemDm({
|
||||
reporter,
|
||||
reportId,
|
||||
publicComment: commentForTemplate,
|
||||
});
|
||||
const email = reporter.email;
|
||||
if (email) {
|
||||
reporterEmailSent = await trySendAdminNotification(
|
||||
() =>
|
||||
emailService.sendReportResolvedEmail(
|
||||
email,
|
||||
reporter.username,
|
||||
reportId.toString(),
|
||||
commentForTemplate,
|
||||
reporter.locale,
|
||||
),
|
||||
{action: 'resolve_report', targetId: reportId.toString()},
|
||||
);
|
||||
}
|
||||
}
|
||||
await auditService.createAuditLog({
|
||||
adminUserId,
|
||||
targetType: 'report',
|
||||
@@ -116,28 +144,11 @@ export class AdminReportService {
|
||||
metadata: new Map([
|
||||
['report_id', reportId.toString()],
|
||||
['report_type', resolvedReport.reportType.toString()],
|
||||
['notify_reporter', notifyReporter ? 'true' : 'false'],
|
||||
['reporter_dm_sent', reporterDmSent ? 'true' : 'false'],
|
||||
['reporter_email_sent', reporterEmailSent ? 'true' : 'false'],
|
||||
]),
|
||||
});
|
||||
if (resolvedReport.reporterId) {
|
||||
const reporter = await userRepository.findUnique(resolvedReport.reporterId);
|
||||
if (reporter) {
|
||||
const commentForTemplate = publicComment ?? '';
|
||||
await this.sendResolvedReportSystemDm({
|
||||
reporter,
|
||||
reportId,
|
||||
publicComment: commentForTemplate,
|
||||
});
|
||||
if (reporter.email) {
|
||||
await emailService.sendReportResolvedEmail(
|
||||
reporter.email,
|
||||
reporter.username,
|
||||
reportId.toString(),
|
||||
commentForTemplate,
|
||||
reporter.locale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
report_id: resolvedReport.reportId.toString(),
|
||||
status: resolvedReport.status,
|
||||
@@ -154,9 +165,8 @@ export class AdminReportService {
|
||||
reporter: User;
|
||||
reportId: ReportID;
|
||||
publicComment: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<boolean> {
|
||||
const {users: userRepository} = this.deps.apiContext.services;
|
||||
const systemUser = await userRepository.findUniqueAssert(SYSTEM_USER_ID);
|
||||
const template = getEmailTemplate('report_resolved', reporter.locale, {
|
||||
username: reporter.username,
|
||||
reportId: reportId.toString(),
|
||||
@@ -173,10 +183,11 @@ export class AdminReportService {
|
||||
},
|
||||
'Skipping report review system DM because the email template could not be resolved',
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const requestCache = createRequestCache();
|
||||
try {
|
||||
const systemUser = await userRepository.findUniqueAssert(SYSTEM_USER_ID);
|
||||
const dmChannel = await this.deps.userChannelService.ensureDmOpenForBothUsers({
|
||||
userId: systemUser.id,
|
||||
recipientId: reporter.id,
|
||||
@@ -191,11 +202,13 @@ export class AdminReportService {
|
||||
},
|
||||
requestCache,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
{reportId: reportId.toString(), reporterId: reporter.id.toString(), error},
|
||||
'Failed to send report review system DM',
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
requestCache.clear();
|
||||
}
|
||||
|
||||
@@ -3,16 +3,22 @@
|
||||
import type {ApiContext} from '@app/api/ApiContext';
|
||||
import {mapUserToAdminResponse} from '@app/api/admin/models/UserTypes';
|
||||
import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService';
|
||||
import {trySendAdminNotification} from '@app/api/admin/services/AdminNotification';
|
||||
import type {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator';
|
||||
import * as AuthSession from '@app/api/auth/AuthSession';
|
||||
import {createUserID, type UserID} from '@app/api/BrandedTypes';
|
||||
import {emitAdminAction} from '@app/api/infrastructure/activity/AccountChangeEvents';
|
||||
import {isAccountClosed, isTemporarilyBanned} from '@app/api/user/UserHelpers';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
import {ConflictError} from '@fluxer/errors/src/domains/core/ConflictError';
|
||||
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
|
||||
import type {AdminUserBanNoteRequest, TempBanUserRequest} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import type {
|
||||
AdminUserBanNoteRequest,
|
||||
AdminUserUnbanRequest,
|
||||
TempBanUserRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
|
||||
interface AdminUserBanServiceDeps {
|
||||
apiContext: ApiContext;
|
||||
@@ -52,16 +58,22 @@ export class AdminUserBanService {
|
||||
);
|
||||
await AuthSession.terminateAllUserSessions(this.deps.apiContext, userId);
|
||||
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
||||
if (user.email && data.duration_hours > 0) {
|
||||
await emailService.sendAccountTempBannedEmail(
|
||||
user.email,
|
||||
user.username,
|
||||
data.reason ?? null,
|
||||
data.duration_hours,
|
||||
tempBannedUntil,
|
||||
user.locale,
|
||||
);
|
||||
}
|
||||
const email = user.email;
|
||||
const notificationSent =
|
||||
data.notify_user && email && data.duration_hours > 0
|
||||
? await trySendAdminNotification(
|
||||
() =>
|
||||
emailService.sendAccountTempBannedEmail(
|
||||
email,
|
||||
user.username,
|
||||
data.reason ?? null,
|
||||
data.duration_hours,
|
||||
tempBannedUntil,
|
||||
user.locale,
|
||||
),
|
||||
{action: 'temp_ban', targetId: userId.toString()},
|
||||
)
|
||||
: false;
|
||||
await auditService.createAuditLog({
|
||||
adminUserId,
|
||||
targetType: 'user',
|
||||
@@ -72,6 +84,8 @@ export class AdminUserBanService {
|
||||
['duration_hours', data.duration_hours.toString()],
|
||||
['reason', data.reason ?? 'null'],
|
||||
['banned_until', tempBannedUntil.toISOString()],
|
||||
['notify_user', data.notify_user ? 'true' : 'false'],
|
||||
['notification_sent', notificationSent ? 'true' : 'false'],
|
||||
]),
|
||||
});
|
||||
await emitAdminAction(adminUserId, userId, 'temp_ban', {durationHours: data.duration_hours});
|
||||
@@ -118,9 +132,7 @@ export class AdminUserBanService {
|
||||
}
|
||||
|
||||
async unbanUser(
|
||||
data: {
|
||||
user_id: bigint;
|
||||
},
|
||||
data: AdminUserUnbanRequest & {user_id: bigint},
|
||||
adminUserId: UserID,
|
||||
auditLogReason: string | null,
|
||||
acls: ReadonlySet<string>,
|
||||
@@ -132,6 +144,7 @@ export class AdminUserBanService {
|
||||
if (!user) {
|
||||
throw new UnknownUserError();
|
||||
}
|
||||
const wasTempBanned = isTemporarilyBanned(user) && !isAccountClosed(user);
|
||||
const updatedUser = await userRepository.patchUpsert(
|
||||
userId,
|
||||
{
|
||||
@@ -141,16 +154,25 @@ export class AdminUserBanService {
|
||||
user.toRow(),
|
||||
);
|
||||
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
||||
if (user.email) {
|
||||
await emailService.sendUnbanNotification(user.email, user.username, auditLogReason || null, user.locale);
|
||||
}
|
||||
const email = user.email;
|
||||
const notificationSent =
|
||||
data.notify_user && email && wasTempBanned
|
||||
? await trySendAdminNotification(
|
||||
() => emailService.sendUnbanNotification(email, user.username, data.public_reason ?? null, user.locale),
|
||||
{action: 'unban', targetId: userId.toString()},
|
||||
)
|
||||
: false;
|
||||
await auditService.createAuditLog({
|
||||
adminUserId,
|
||||
targetType: 'user',
|
||||
targetId: BigInt(userId),
|
||||
action: 'unban',
|
||||
auditLogReason,
|
||||
metadata: new Map(),
|
||||
metadata: new Map([
|
||||
['notify_user', data.notify_user ? 'true' : 'false'],
|
||||
['notification_sent', notificationSent ? 'true' : 'false'],
|
||||
['public_reason', data.public_reason ?? 'null'],
|
||||
]),
|
||||
});
|
||||
await emitAdminAction(adminUserId, userId, 'unban');
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {ApiContext} from '@app/api/ApiContext';
|
||||
import {mapUserToAdminResponse} from '@app/api/admin/models/UserTypes';
|
||||
import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService';
|
||||
import type {AdminBanManagementService} from '@app/api/admin/services/AdminBanManagementService';
|
||||
import {trySendAdminNotification} from '@app/api/admin/services/AdminNotification';
|
||||
import type {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator';
|
||||
import * as AuthSession from '@app/api/auth/AuthSession';
|
||||
import {createReportID, createUserID, type UserID} from '@app/api/BrandedTypes';
|
||||
@@ -65,6 +66,25 @@ export function resolveDeletionDays(reasonCode: number, requestedDays: number):
|
||||
return Math.max(requestedDays, minDays);
|
||||
}
|
||||
|
||||
type ScheduledDeletionEmailTemplate =
|
||||
| 'account_deletion_scheduled_requested'
|
||||
| 'account_deletion_scheduled_inactivity'
|
||||
| 'scheduled_deletion_notification'
|
||||
| 'account_scheduled_deletion';
|
||||
|
||||
export function scheduledDeletionEmailTemplate(reasonCode: number): ScheduledDeletionEmailTemplate {
|
||||
switch (reasonCode) {
|
||||
case DeletionReasons.USER_REQUESTED:
|
||||
return 'account_deletion_scheduled_requested';
|
||||
case DeletionReasons.INACTIVITY:
|
||||
return 'account_deletion_scheduled_inactivity';
|
||||
case DeletionReasons.OTHER:
|
||||
return 'scheduled_deletion_notification';
|
||||
default:
|
||||
return 'account_scheduled_deletion';
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminUserDeletionService {
|
||||
constructor(private readonly deps: AdminUserDeletionServiceDeps) {}
|
||||
|
||||
@@ -86,7 +106,7 @@ export class AdminUserDeletionService {
|
||||
adminUserId: UserID,
|
||||
auditLogReason: string | null,
|
||||
): Promise<User> {
|
||||
const {users: userRepository, email: emailService} = this.deps.apiContext.services;
|
||||
const {users: userRepository} = this.deps.apiContext.services;
|
||||
const {auditService, updatePropagator} = this.deps;
|
||||
const userId = createUserID(data.user_id);
|
||||
const user = await userRepository.findUnique(userId);
|
||||
@@ -186,6 +206,23 @@ export class AdminUserDeletionService {
|
||||
);
|
||||
}
|
||||
}
|
||||
const email = user.email;
|
||||
const notificationTemplate = scheduledDeletionEmailTemplate(data.reason_code);
|
||||
const notificationAttempted = Boolean(data.notify_user && email);
|
||||
const notificationSent =
|
||||
data.notify_user && email
|
||||
? await trySendAdminNotification(
|
||||
() =>
|
||||
this.sendScheduledDeletionEmail(notificationTemplate, {
|
||||
email,
|
||||
username: user.username,
|
||||
reason: data.public_reason ?? null,
|
||||
deletionDate: pendingDeletionAt,
|
||||
locale: user.locale,
|
||||
}),
|
||||
{action: 'schedule_deletion', targetId: userId.toString()},
|
||||
)
|
||||
: false;
|
||||
await auditService.createAuditLog({
|
||||
adminUserId,
|
||||
targetType: 'user',
|
||||
@@ -197,6 +234,9 @@ export class AdminUserDeletionService {
|
||||
['reason_code', data.reason_code.toString()],
|
||||
['pending_deletion_at', pendingDeletionAt.toISOString()],
|
||||
...describePendingDeletion(user, 'replaced'),
|
||||
['notify_user', data.notify_user ? 'true' : 'false'],
|
||||
['notification_sent', notificationSent ? 'true' : 'false'],
|
||||
...(notificationAttempted ? [['notification_template', notificationTemplate] as [string, string]] : []),
|
||||
]),
|
||||
});
|
||||
let knownIps: ReadonlySet<string> = new Set();
|
||||
@@ -206,25 +246,27 @@ export class AdminUserDeletionService {
|
||||
}
|
||||
await emitAdminAction(adminUserId, userId, 'schedule_deletion', {reasonCode: data.reason_code, ips: knownIps});
|
||||
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
||||
if (user.email) {
|
||||
try {
|
||||
await emailService.sendAccountScheduledForDeletionEmail(
|
||||
user.email,
|
||||
user.username,
|
||||
data.public_reason ?? null,
|
||||
pendingDeletionAt,
|
||||
user.locale,
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
{error, userId: userId.toString()},
|
||||
'Failed to send scheduled deletion email after the deletion was scheduled',
|
||||
);
|
||||
}
|
||||
}
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
private sendScheduledDeletionEmail(
|
||||
template: ScheduledDeletionEmailTemplate,
|
||||
params: {email: string; username: string; reason: string | null; deletionDate: Date; locale: string | null},
|
||||
): Promise<boolean> {
|
||||
const {email: emailService} = this.deps.apiContext.services;
|
||||
const {email, username, reason, deletionDate, locale} = params;
|
||||
switch (template) {
|
||||
case 'account_deletion_scheduled_requested':
|
||||
return emailService.sendAccountDeletionRequestedEmail(email, username, reason, deletionDate, locale);
|
||||
case 'account_deletion_scheduled_inactivity':
|
||||
return emailService.sendAccountDeletionInactivityEmail(email, username, reason, deletionDate, locale);
|
||||
case 'scheduled_deletion_notification':
|
||||
return emailService.sendScheduledDeletionNotification(email, username, deletionDate, reason, locale);
|
||||
case 'account_scheduled_deletion':
|
||||
return emailService.sendAccountScheduledForDeletionEmail(email, username, reason, deletionDate, locale);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelAccountDeletion(
|
||||
data: AdminUserDeletionCancelRequest & {user_id: bigint},
|
||||
adminUserId: UserID,
|
||||
@@ -261,9 +303,14 @@ export class AdminUserDeletionService {
|
||||
deletionQueue: this.deps.kvDeletionQueue,
|
||||
});
|
||||
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
||||
if (data.notify_user && user.email) {
|
||||
await emailService.sendUnbanNotification(user.email, user.username, null, user.locale);
|
||||
}
|
||||
const email = user.email;
|
||||
const notificationSent =
|
||||
data.notify_user && email
|
||||
? await trySendAdminNotification(
|
||||
() => emailService.sendAccountDeletionCancelledEmail(email, user.username, user.locale),
|
||||
{action: 'cancel_deletion', targetId: userId.toString()},
|
||||
)
|
||||
: false;
|
||||
await auditService.createAuditLog({
|
||||
adminUserId,
|
||||
targetType: 'user',
|
||||
@@ -273,6 +320,7 @@ export class AdminUserDeletionService {
|
||||
metadata: new Map([
|
||||
...describePendingDeletion(user, 'cancelled'),
|
||||
['notify_user', data.notify_user ? 'true' : 'false'],
|
||||
['notification_sent', notificationSent ? 'true' : 'false'],
|
||||
]),
|
||||
});
|
||||
await emitAdminAction(adminUserId, userId, 'cancel_deletion');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type {ApiContext} from '@app/api/ApiContext';
|
||||
import {mapUserToAdminResponse} from '@app/api/admin/models/UserTypes';
|
||||
import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService';
|
||||
import {trySendAdminNotification} from '@app/api/admin/services/AdminNotification';
|
||||
import type {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator';
|
||||
import * as AuthEmail from '@app/api/auth/AuthEmail';
|
||||
import * as AuthMfa from '@app/api/auth/AuthMfa';
|
||||
@@ -508,16 +509,25 @@ export class AdminUserSecurityService {
|
||||
);
|
||||
await AuthSession.terminateAllUserSessions(this.deps.apiContext, userId);
|
||||
await updatePropagator.propagateUserUpdate({userId, oldUser: user, updatedUser: updatedUser});
|
||||
if (user.email) {
|
||||
await emailService.sendAccountDisabledForSuspiciousActivityEmail(user.email, user.username, null, user.locale);
|
||||
}
|
||||
const email = user.email;
|
||||
const notificationSent =
|
||||
data.notify_user && email
|
||||
? await trySendAdminNotification(
|
||||
() => emailService.sendAccountDisabledForSuspiciousActivityEmail(email, user.username, null, user.locale),
|
||||
{action: 'disable_suspicious_activity', targetId: userId.toString()},
|
||||
)
|
||||
: false;
|
||||
await auditService.createAuditLog({
|
||||
adminUserId,
|
||||
targetType: 'user',
|
||||
targetId: BigInt(userId),
|
||||
action: 'disable_suspicious_activity',
|
||||
auditLogReason,
|
||||
metadata: new Map([['flags', data.flags.toString()]]),
|
||||
metadata: new Map([
|
||||
['flags', data.flags.toString()],
|
||||
['notify_user', data.notify_user ? 'true' : 'false'],
|
||||
['notification_sent', notificationSent ? 'true' : 'false'],
|
||||
]),
|
||||
});
|
||||
await emitAdminAction(adminUserId, userId, 'disable_suspicious');
|
||||
return {
|
||||
|
||||
@@ -380,7 +380,7 @@ describe('Admin Deletion Queue', () => {
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(
|
||||
findLastTestEmail(await listTestEmails(harness, {recipient: target.email}), 'unban_notification'),
|
||||
findLastTestEmail(await listTestEmails(harness, {recipient: target.email}), 'account_deletion_cancelled'),
|
||||
).toBeNull();
|
||||
const logs = await getAdminRepository().listAllAuditLogsPaginated(1000);
|
||||
const log = logs.find(
|
||||
@@ -390,6 +390,7 @@ describe('Admin Deletion Queue', () => {
|
||||
expect(log?.metadata.get('cancelled_scheduled_by')).toBe(scheduler.userId);
|
||||
expect(log?.metadata.get('cancelled_reason_code')).toBe(DeletionReasons.SPAM.toString());
|
||||
expect(log?.metadata.get('notify_user')).toBe('false');
|
||||
expect(log?.metadata.get('notification_sent')).toBe('false');
|
||||
const rescheduled = await scheduleAs(scheduler, target, DeletionReasons.SPAM, 'Review again');
|
||||
await createBuilder(harness, canceller.token)
|
||||
.delete(`/admin/users/${target.userId}/deletion`)
|
||||
@@ -397,9 +398,11 @@ describe('Admin Deletion Queue', () => {
|
||||
.body({expected_pending_deletion_at: rescheduled.user.pending_deletion_at, notify_user: true})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const email = findLastTestEmail(await listTestEmails(harness, {recipient: target.email}), 'unban_notification');
|
||||
const emails = await listTestEmails(harness, {recipient: target.email});
|
||||
const email = findLastTestEmail(emails, 'account_deletion_cancelled');
|
||||
expect(email).not.toBeNull();
|
||||
expect(JSON.stringify(email)).not.toContain('Private cancel note');
|
||||
expect(findLastTestEmail(emails, 'unban_notification')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,326 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {scheduledDeletionEmailTemplate} from '@app/api/admin/services/AdminUserDeletionService';
|
||||
import {
|
||||
clearTestEmails,
|
||||
createTestAccount,
|
||||
listTestEmails,
|
||||
setUserACLs,
|
||||
type TestAccount,
|
||||
} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {createUserID} from '@app/api/BrandedTypes';
|
||||
import {getAdminRepository, getUserRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder} from '@app/api/test/TestRequestBuilder';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {DeletionReasons} from '@fluxer/constants/src/Core';
|
||||
import {SuspiciousActivityFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
const DELETION_TEMPLATES = [
|
||||
'account_deletion_scheduled_requested',
|
||||
'account_deletion_scheduled_inactivity',
|
||||
'scheduled_deletion_notification',
|
||||
'account_scheduled_deletion',
|
||||
];
|
||||
|
||||
const NON_ENFORCEMENT_TEMPLATES: Record<number, string> = {
|
||||
1: 'account_deletion_scheduled_requested',
|
||||
2: 'scheduled_deletion_notification',
|
||||
19: 'account_deletion_scheduled_inactivity',
|
||||
};
|
||||
|
||||
describe('scheduledDeletionEmailTemplate', () => {
|
||||
test.each(Object.entries(NON_ENFORCEMENT_TEMPLATES))('code %s picks %s', (code, template) => {
|
||||
expect(scheduledDeletionEmailTemplate(Number(code))).toBe(template);
|
||||
});
|
||||
|
||||
test.each(Object.entries(DeletionReasons).filter(([, code]) => !(code in NON_ENFORCEMENT_TEMPLATES)))(
|
||||
'%s picks the enforcement template',
|
||||
(_name, code) => {
|
||||
expect(scheduledDeletionEmailTemplate(code)).toBe('account_scheduled_deletion');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Admin staff notifications', () => {
|
||||
let harness: ApiTestHarness;
|
||||
let admin: TestAccount;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
admin = await setUserACLs(harness, await createTestAccount(harness), [AdminACLs.WILDCARD]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
async function auditMetadata(action: string, targetId: string): Promise<Map<string, string> | undefined> {
|
||||
const logs = await getAdminRepository().listAllAuditLogsPaginated(1000);
|
||||
return logs.find((log) => log.action === action && log.targetId.toString() === targetId)?.metadata;
|
||||
}
|
||||
|
||||
async function emailsTo(account: TestAccount) {
|
||||
return listTestEmails(harness, {recipient: account.email});
|
||||
}
|
||||
|
||||
async function systemDmMessages(account: TestAccount): Promise<Array<{content: string | null}>> {
|
||||
const channels = await createBuilder<Array<{id: string; recipients?: Array<{id: string}>}>>(harness, account.token)
|
||||
.get('/users/@me/channels')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const systemChannels = channels.filter((channel) => channel.recipients?.some((recipient) => recipient.id === '0'));
|
||||
const messages = await Promise.all(
|
||||
systemChannels.map((channel) =>
|
||||
createBuilder<Array<{content: string | null}>>(harness, account.token)
|
||||
.get(`/channels/${channel.id}/messages?limit=50`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute(),
|
||||
),
|
||||
);
|
||||
return messages.flat();
|
||||
}
|
||||
|
||||
async function expireTempBan(target: TestAccount) {
|
||||
const users = getUserRepository();
|
||||
const user = (await users.findUnique(createUserID(BigInt(target.userId))))!;
|
||||
await users.patchUpsert(user.id, {temp_banned_until: new Date(Date.now() - 3_600_000)}, user.toRow());
|
||||
}
|
||||
|
||||
async function scheduleDeletion(target: TestAccount, body: Record<string, unknown>) {
|
||||
await createBuilder(harness, admin.token)
|
||||
.put(`/admin/users/${target.userId}/deletion`)
|
||||
.body({days_until_deletion: 60, ...body})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function tempBan(target: TestAccount, body: Record<string, unknown>) {
|
||||
await createBuilder(harness, admin.token)
|
||||
.put(`/admin/users/${target.userId}/ban`)
|
||||
.body(body)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe('schedule deletion', () => {
|
||||
test.each([
|
||||
[DeletionReasons.USER_REQUESTED, 'account_deletion_scheduled_requested'],
|
||||
[DeletionReasons.INACTIVITY, 'account_deletion_scheduled_inactivity'],
|
||||
[DeletionReasons.OTHER, 'scheduled_deletion_notification'],
|
||||
[DeletionReasons.SPAM, 'account_scheduled_deletion'],
|
||||
])('reason code %s sends %s', async (reasonCode, template) => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await scheduleDeletion(target, {reason_code: reasonCode, public_reason: 'Shown to the user'});
|
||||
const sent = (await emailsTo(target)).filter((email) => DELETION_TEMPLATES.includes(email.type));
|
||||
expect(sent.map((email) => email.type)).toEqual([template]);
|
||||
expect(sent[0]?.metadata.reason).toBe('Shown to the user');
|
||||
const metadata = await auditMetadata('schedule_deletion', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('true');
|
||||
expect(metadata?.get('notification_sent')).toBe('true');
|
||||
expect(metadata?.get('notification_template')).toBe(template);
|
||||
});
|
||||
|
||||
test('notify_user false sends nothing and records it', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await scheduleDeletion(target, {reason_code: DeletionReasons.SPAM, notify_user: false});
|
||||
expect((await emailsTo(target)).filter((email) => DELETION_TEMPLATES.includes(email.type))).toEqual([]);
|
||||
const metadata = await auditMetadata('schedule_deletion', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('false');
|
||||
expect(metadata?.get('notification_sent')).toBe('false');
|
||||
expect(metadata?.has('notification_template')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('temp ban', () => {
|
||||
test('emails by default', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await tempBan(target, {duration_hours: 24, reason: 'Spam'});
|
||||
expect((await emailsTo(target)).map((email) => email.type)).toContain('account_temp_banned');
|
||||
const metadata = await auditMetadata('temp_ban', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('true');
|
||||
expect(metadata?.get('notification_sent')).toBe('true');
|
||||
});
|
||||
|
||||
test('notify_user false sends nothing', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await tempBan(target, {duration_hours: 24, notify_user: false});
|
||||
expect((await emailsTo(target)).map((email) => email.type)).not.toContain('account_temp_banned');
|
||||
const metadata = await auditMetadata('temp_ban', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('false');
|
||||
expect(metadata?.get('notification_sent')).toBe('false');
|
||||
});
|
||||
|
||||
test('a permanent ban sends nothing', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await tempBan(target, {duration_hours: 0});
|
||||
expect((await emailsTo(target)).map((email) => email.type)).not.toContain('account_temp_banned');
|
||||
const metadata = await auditMetadata('temp_ban', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('true');
|
||||
expect(metadata?.get('notification_sent')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unban', () => {
|
||||
test('emails the public reason and never the audit log reason', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await tempBan(target, {duration_hours: 24, notify_user: false});
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.delete(`/admin/users/${target.userId}/ban`)
|
||||
.header('X-Audit-Log-Reason', 'Private staff note')
|
||||
.body({public_reason: 'Appeal accepted'})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const email = (await emailsTo(target)).find((entry) => entry.type === 'unban_notification');
|
||||
expect(email?.metadata.reason).toBe('Appeal accepted');
|
||||
expect(JSON.stringify(email)).not.toContain('Private staff note');
|
||||
const metadata = await auditMetadata('unban', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('true');
|
||||
expect(metadata?.get('notification_sent')).toBe('true');
|
||||
expect(metadata?.get('public_reason')).toBe('Appeal accepted');
|
||||
});
|
||||
|
||||
test('accepts a request without a body', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await tempBan(target, {duration_hours: 24, notify_user: false});
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.delete(`/admin/users/${target.userId}/ban`)
|
||||
.header('X-Audit-Log-Reason', 'Private staff note')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const email = (await emailsTo(target)).find((entry) => entry.type === 'unban_notification');
|
||||
expect(email?.metadata.reason).toBe('');
|
||||
expect(JSON.stringify(email)).not.toContain('Private staff note');
|
||||
});
|
||||
|
||||
test('notify_user false sends nothing', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await tempBan(target, {duration_hours: 24, notify_user: false});
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.delete(`/admin/users/${target.userId}/ban`)
|
||||
.body({notify_user: false})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect((await emailsTo(target)).map((email) => email.type)).not.toContain('unban_notification');
|
||||
const metadata = await auditMetadata('unban', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('false');
|
||||
expect(metadata?.get('notification_sent')).toBe('false');
|
||||
});
|
||||
|
||||
test('a temp ban that already expired is not emailed', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await tempBan(target, {duration_hours: 24, notify_user: false});
|
||||
await expireTempBan(target);
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.delete(`/admin/users/${target.userId}/ban`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect((await emailsTo(target)).map((email) => email.type)).not.toContain('unban_notification');
|
||||
const metadata = await auditMetadata('unban', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe('true');
|
||||
expect(metadata?.get('notification_sent')).toBe('false');
|
||||
});
|
||||
|
||||
test('an account pending deletion is not told it can log back in', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await tempBan(target, {duration_hours: 24, notify_user: false});
|
||||
await scheduleDeletion(target, {reason_code: DeletionReasons.SPAM, notify_user: false});
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.delete(`/admin/users/${target.userId}/ban`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect((await emailsTo(target)).map((email) => email.type)).not.toContain('unban_notification');
|
||||
expect((await auditMetadata('unban', target.userId))?.get('notification_sent')).toBe('false');
|
||||
});
|
||||
|
||||
test('a user who was never banned is not emailed', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.delete(`/admin/users/${target.userId}/ban`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect((await emailsTo(target)).map((email) => email.type)).not.toContain('unban_notification');
|
||||
expect((await auditMetadata('unban', target.userId))?.get('notification_sent')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('suspicious activity disable', () => {
|
||||
test.each([
|
||||
[undefined, true],
|
||||
[false, false],
|
||||
])('notify_user %s emails %s', async (notifyUser, expected) => {
|
||||
const target = await createTestAccount(harness);
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.put(`/admin/users/${target.userId}/suspicious-activity-disablement`)
|
||||
.body({
|
||||
flags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE,
|
||||
...(notifyUser === undefined ? {} : {notify_user: notifyUser}),
|
||||
})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const types = (await emailsTo(target)).map((email) => email.type);
|
||||
expect(types.includes('account_disabled_suspicious')).toBe(expected);
|
||||
const metadata = await auditMetadata('disable_suspicious_activity', target.userId);
|
||||
expect(metadata?.get('notify_user')).toBe(expected ? 'true' : 'false');
|
||||
expect(metadata?.get('notification_sent')).toBe(expected ? 'true' : 'false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('report resolve', () => {
|
||||
async function fileReport(): Promise<{reporter: TestAccount; reportId: string}> {
|
||||
const reporter = await createTestAccount(harness);
|
||||
const reported = await createTestAccount(harness);
|
||||
const report = await createBuilder<{report_id: string}>(harness, reporter.token)
|
||||
.post('/reports/user')
|
||||
.body({user_id: reported.userId, category: 'harassment'})
|
||||
.execute();
|
||||
return {reporter, reportId: report.report_id};
|
||||
}
|
||||
|
||||
test('notifies the reporter by default', async () => {
|
||||
const {reporter, reportId} = await fileReport();
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.patch(`/admin/reports/${reportId}`)
|
||||
.body({status: 'resolved', public_comment: 'Handled'})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect((await emailsTo(reporter)).map((email) => email.type)).toContain('report_resolved');
|
||||
expect(await systemDmMessages(reporter)).toHaveLength(1);
|
||||
const metadata = await auditMetadata('resolve_report', reportId);
|
||||
expect(metadata?.get('notify_reporter')).toBe('true');
|
||||
expect(metadata?.get('reporter_dm_sent')).toBe('true');
|
||||
expect(metadata?.get('reporter_email_sent')).toBe('true');
|
||||
});
|
||||
|
||||
test('notify_reporter false sends neither DM nor email', async () => {
|
||||
const {reporter, reportId} = await fileReport();
|
||||
await clearTestEmails(harness);
|
||||
await createBuilder(harness, admin.token)
|
||||
.patch(`/admin/reports/${reportId}`)
|
||||
.body({status: 'resolved', notify_reporter: false})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect((await emailsTo(reporter)).map((email) => email.type)).not.toContain('report_resolved');
|
||||
expect(await systemDmMessages(reporter)).toEqual([]);
|
||||
const metadata = await auditMetadata('resolve_report', reportId);
|
||||
expect(metadata?.get('notify_reporter')).toBe('false');
|
||||
expect(metadata?.get('reporter_dm_sent')).toBe('false');
|
||||
expect(metadata?.get('reporter_email_sent')).toBe('false');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,7 +109,13 @@ export const ReportAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
|
||||
action: 'resolve_report',
|
||||
targetType: 'report',
|
||||
targetId: reportId,
|
||||
metadata: {report_id: reportId, report_type: '1'},
|
||||
metadata: {
|
||||
report_id: reportId,
|
||||
report_type: '1',
|
||||
notify_reporter: 'true',
|
||||
reporter_dm_sent: 'true',
|
||||
reporter_email_sent: 'true',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -318,7 +318,13 @@ export const UserWriteAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
|
||||
action: 'temp_ban',
|
||||
targetType: 'user',
|
||||
targetId: target.userId,
|
||||
metadata: {duration_hours: '24', reason: 'Coverage ban', banned_until: expect.any(String)},
|
||||
metadata: {
|
||||
duration_hours: '24',
|
||||
reason: 'Coverage ban',
|
||||
banned_until: expect.any(String),
|
||||
notify_user: 'true',
|
||||
notification_sent: 'true',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -335,7 +341,7 @@ export const UserWriteAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
|
||||
action: 'unban',
|
||||
targetType: 'user',
|
||||
targetId: target.userId,
|
||||
metadata: {},
|
||||
metadata: {notify_user: 'true', notification_sent: 'true', public_reason: 'null'},
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -386,6 +392,9 @@ export const UserWriteAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
|
||||
days: '30',
|
||||
reason_code: DeletionReasons.USER_REQUESTED.toString(),
|
||||
pending_deletion_at: expect.any(String),
|
||||
notify_user: 'true',
|
||||
notification_sent: 'true',
|
||||
notification_template: 'account_deletion_scheduled_requested',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -416,6 +425,7 @@ export const UserWriteAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
|
||||
cancelled_scheduled_at: expect.any(String),
|
||||
cancelled_reason_code: DeletionReasons.USER_REQUESTED.toString(),
|
||||
notify_user: 'false',
|
||||
notification_sent: 'false',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -596,7 +606,7 @@ export const UserWriteAdminAuditCases: ReadonlyArray<AdminAuditCoverageCase> = [
|
||||
action: 'disable_suspicious_activity',
|
||||
targetType: 'user',
|
||||
targetId: target.userId,
|
||||
metadata: {flags: flags.toString()},
|
||||
metadata: {flags: flags.toString(), notify_user: 'true', notification_sent: 'true'},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user