fix(api): send correct staff emails and allow suppressing them (#3048)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-29 23:55:54 +02:00
1 parent f0b3c82cfd
commit 39f9beda5a
113 files changed
+2496 -206

No files matched your search

@@ -16,6 +16,7 @@ import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils';
import {getWorkerDependencies} from '@app/api/worker/WorkerContext';
import {DeletionReasons} from '@fluxer/constants/src/Core';
import {UserFlags} from '@fluxer/constants/src/UserConstants';
import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake';
import type {IEmailService} from '@pkgs/email/src/IEmailService';
import {TestEmailService} from '@pkgs/email/src/TestEmailService';
import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider';
@@ -146,7 +147,7 @@ async function processUser(
user.email,
user.username,
deletionDate,
lastActivity || new Date(0),
lastActivity ?? snowflakeToDate(BigInt(userId)),
user.locale,
);
if (sent) {
@@ -13,6 +13,7 @@ interface Payload {
reason_code: ScheduleAccountDeletionRequest['reason_code'];
days_until_deletion: number;
public_reason: string | null;
notify_user: boolean;
admin_user_id: string;
audit_log_reason: string | null;
}
@@ -23,6 +24,7 @@ const handler: WorkerTaskHandler = async (rawPayload, helpers) => {
reason_code: rawPayload.reason_code as ScheduleAccountDeletionRequest['reason_code'],
days_until_deletion: rawPayload.days_until_deletion as number,
public_reason: (rawPayload.public_reason as string | null) ?? null,
notify_user: rawPayload.notify_user !== false,
admin_user_id: rawPayload.admin_user_id as string,
audit_log_reason: (rawPayload.audit_log_reason as string | null) ?? null,
};
@@ -43,6 +45,7 @@ const handler: WorkerTaskHandler = async (rawPayload, helpers) => {
reason_code: payload.reason_code,
public_reason: payload.public_reason ?? undefined,
days_until_deletion: payload.days_until_deletion,
notify_user: payload.notify_user,
},
adminUserId,
payload.audit_log_reason,
@@ -57,6 +60,7 @@ const handler: WorkerTaskHandler = async (rawPayload, helpers) => {
['user_count', total.toString()],
['reason_code', payload.reason_code.toString()],
['days', daysUntilDeletion.toString()],
['notify_user', payload.notify_user ? 'true' : 'false'],
],
},
});
@@ -2,9 +2,15 @@
import {AdminRepository} from '@app/api/admin/AdminRepository';
import type {AdminAuditLog} from '@app/api/admin/IAdminRepository';
import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils';
import {
clearTestEmails,
createTestAccount,
listTestEmails,
setUserACLs,
type TestAccount,
} from '@app/api/auth/tests/AuthTestUtils';
import {createReportID, createUserID} from '@app/api/BrandedTypes';
import {getGatewayService, getSnowflakeService} from '@app/api/middleware/ServiceRegistry';
import {getGatewayService, getSnowflakeService, setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry';
import {
createUserCacheService,
getAdminRepository,
@@ -26,6 +32,7 @@ import {clearWorkerDependencies, setWorkerDependenciesForTest} from '@app/api/wo
import {DeletionReasons} from '@fluxer/constants/src/Core';
import {UserFlags} from '@fluxer/constants/src/UserConstants';
import type {WorkerTaskHelpers, WorkerTaskResult} from '@pkgs/worker/src/contracts/WorkerTask';
import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
interface ReportResponse {
@@ -70,6 +77,7 @@ async function runBulkJob(
userIds: Array<string>,
adminUserId: string,
reasonCode: number = DeletionReasons.SPAM,
extraPayload: Record<string, unknown> = {},
): Promise<BulkJobResult> {
installWorkerDependencies();
const result = (await bulkScheduleUserDeletion(
@@ -80,6 +88,7 @@ async function runBulkJob(
public_reason: null,
admin_user_id: adminUserId,
audit_log_reason: AUDIT_LOG_REASON,
...extraPayload,
},
createHelpers(),
)) as WorkerTaskResult;
@@ -212,6 +221,65 @@ describe('bulkScheduleUserDeletion', () => {
const perUserLogs = await listAuditLogs('schedule_deletion');
expect(perUserLogs.filter((log) => log.targetId === BigInt(target.userId))).toHaveLength(1);
});
test('a payload with notify_user false emails nobody and records it in the summary', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'bulk:delete:users']);
const first = await createTestAccount(harness);
const second = await createTestAccount(harness);
await clearTestEmails(harness);
const result = await runBulkJob([first.userId, second.userId], admin.userId, DeletionReasons.SPAM, {
notify_user: false,
});
expect(result.successful_count).toBe(2);
expect(await listTestEmails(harness, {recipient: first.email})).toEqual([]);
expect(await listTestEmails(harness, {recipient: second.email})).toEqual([]);
const perUserLogs = await listAuditLogs('schedule_deletion');
expect(perUserLogs.every((log) => log.metadata.get('notify_user') === 'false')).toBe(true);
const summaryLogs = await listAuditLogs('bulk_schedule_deletion');
expect(summaryLogs[0]!.metadata.get('notify_user')).toBe('false');
});
test('a payload queued without notify_user emails every user', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'bulk:delete:users']);
const first = await createTestAccount(harness);
const second = await createTestAccount(harness);
await clearTestEmails(harness);
const result = await runBulkJob([first.userId, second.userId], admin.userId);
expect(result.successful_count).toBe(2);
for (const target of [first, second]) {
const emails = await listTestEmails(harness, {recipient: target.email});
expect(emails.map((email) => email.type)).toEqual(['account_scheduled_deletion']);
}
const summaryLogs = await listAuditLogs('bulk_schedule_deletion');
expect(summaryLogs[0]!.metadata.get('notify_user')).toBe('true');
});
test('the bulk job route queues notify_user in the payload', async () => {
const admin = await createTestAccount(harness);
await setUserACLs(harness, admin, ['admin:authenticate', 'bulk:delete:users']);
const target = await createTestAccount(harness);
const queued: Array<{task: string; payload: WorkerJobPayload}> = [];
setInjectedWorkerService({
addJob: async (task, payload) => {
queued.push({task, payload});
return 1n;
},
cancelJob: async () => false,
retryDeadLetterJob: async () => false,
});
await createBuilder(harness, admin.token)
.post('/admin/bulk-jobs')
.body({
task: 'schedule_user_deletion',
user_ids: [target.userId],
reason_code: DeletionReasons.SPAM,
notify_user: false,
})
.expect(HTTP_STATUS.OK)
.execute();
expect(queued).toHaveLength(1);
expect(queued[0]!.task).toBe('bulkScheduleUserDeletion');
expect(queued[0]!.payload.notify_user).toBe(false);
});
test('leaves a deletion another admin already scheduled in place and reports the user as not scheduled', async () => {
const scheduler = await createTestAccount(harness);
await setUserACLs(harness, scheduler, ['admin:authenticate', 'user:delete']);