Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/controllers/DataExportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { Request, Response } from 'express';

import BaseController from './BaseController';
import DataExportRequestResource from 'src/resources/DataExportRequestResource';
import { config } from 'src/config';
import { prisma } from 'src/db';
import { dataExportQueue } from 'src/services/DataExportQueue';
import { DataExportService } from 'src/services/DataExportService';
import { sendMail } from 'src/mailer/mailer';
import { logAuditEvent } from 'src/utils/auditLogger';
import { RequestError } from 'src/utils/errors';
import argon2 from 'argon2';
Expand Down Expand Up @@ -360,9 +362,22 @@ export default class extends BaseController {
},
});

// 4. Queue confirmation email (wire in your mailer/queue here)
// e.g. emailQueue.enqueue({ type: 'DELETION_REQUESTED', userId, scheduledAt, token })
void token; // token is passed to the email queue — suppress unused-var lint
// 4. Send confirmation email with cancel link
const cancelUrl = `${config('app.front_url')}/account/cancel-deletion?token=${token}`;
await sendMail({
to: user.email,
subject: 'Your account deletion has been scheduled',
text:
`You requested deletion of your Artisyn account.\n\n` +
`Your account will be permanently deleted on ${scheduledAt.toDateString()}.\n\n` +
`If you change your mind, cancel before that date:\n${cancelUrl}`,
temp: 'auth',
caption: 'Account deletion scheduled',
data: {
link: cancelUrl,
linkTitle: 'Cancel Deletion',
},
});

await logAuditEvent(userId, 'DATA_DELETE', {
req,
Expand Down Expand Up @@ -405,8 +420,8 @@ export default class extends BaseController {

// Resolve record by token (email-link flow) or userId (in-app flow)
const pending = token
? await prisma.pendingDeletion.findUnique({ where: { token } })
: await prisma.pendingDeletion.findUnique({ where: { userId } });
? await prisma.pendingDeletion.findUnique({ where: { token }, include: { user: true } })
: await prisma.pendingDeletion.findUnique({ where: { userId }, include: { user: true } });

RequestError.assertFound(pending, 'No pending deletion request found', 404);

Expand All @@ -418,8 +433,13 @@ export default class extends BaseController {
// Remove the pending-deletion record
await prisma.pendingDeletion.delete({ where: { userId: pending.userId } });

// Queue cancellation confirmation email
// e.g. emailQueue.enqueue({ type: 'DELETION_CANCELLED', userId: pending.userId })
await sendMail({
to: pending.user.email,
subject: 'Your account deletion has been cancelled',
text: 'Your account deletion request has been cancelled. Your Artisyn account is safe and will not be deleted.',
temp: 'auth',
caption: 'Account deletion cancelled',
});

await logAuditEvent(pending.userId, 'DATA_DELETE', {
req,
Expand Down
90 changes: 90 additions & 0 deletions src/controllers/__tests__/data-export-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,4 +580,94 @@ describe('Data Export & Account Deletion — HTTP integration', () => {
expect(stillPending).not.toBeNull();
});
});

// =======================================================================
// Email notifications
// =======================================================================

describe('Email notifications', () => {
beforeEach(() => {
vi.clearAllMocks();
// Re-apply the mock after clearAllMocks resets it
vi.spyOn(mailer, 'sendMail').mockResolvedValue(null);
});

it('sends a confirmation email to the user when deletion is requested', async () => {
await request(app)
.post('/api/account/deletion-request')
.set(authHeaders(owner.token))
.send({ password: owner.plainPassword })
.expect(202);

expect(mailer.sendMail).toHaveBeenCalledOnce();
const [call] = (mailer.sendMail as ReturnType<typeof vi.fn>).mock.calls;
const mailArgs = call[0];

expect(mailArgs.to).toBe(owner.user.email);
expect(mailArgs.subject).toMatch(/deletion/i);
// Body must mention the cancel link and deletion window
expect(mailArgs.text).toMatch(/cancel/i);
expect(mailArgs.data?.linkTitle).toMatch(/cancel/i);
});

it('sends a cancellation email after a deletion is cancelled in-app', async () => {
await request(app)
.post('/api/account/deletion-request')
.set(authHeaders(owner.token))
.send({ password: owner.plainPassword })
.expect(202);

vi.clearAllMocks();
vi.spyOn(mailer, 'sendMail').mockResolvedValue(null);

await request(app)
.post('/api/account/cancel-deletion')
.set(authHeaders(owner.token))
.send({})
.expect(200);

expect(mailer.sendMail).toHaveBeenCalledOnce();
const [call] = (mailer.sendMail as ReturnType<typeof vi.fn>).mock.calls;
const mailArgs = call[0];

expect(mailArgs.to).toBe(owner.user.email);
expect(mailArgs.subject).toMatch(/cancelled/i);
});

it('sends a cancellation email after a deletion is cancelled via email token', async () => {
await request(app)
.post('/api/account/deletion-request')
.set(authHeaders(owner.token))
.send({ password: owner.plainPassword })
.expect(202);

const record = await prisma.pendingDeletion.findUnique({
where: { userId: owner.user.id },
});

vi.clearAllMocks();
vi.spyOn(mailer, 'sendMail').mockResolvedValue(null);

await request(app)
.post('/api/account/cancel-deletion')
.set(authHeaders(owner.token))
.send({ token: record!.token })
.expect(200);

expect(mailer.sendMail).toHaveBeenCalledOnce();
const mailArgs = (mailer.sendMail as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(mailArgs.to).toBe(owner.user.email);
expect(mailArgs.subject).toMatch(/cancelled/i);
});

it('does not send an email when deletion request fails (wrong password)', async () => {
await request(app)
.post('/api/account/deletion-request')
.set(authHeaders(owner.token))
.send({ password: 'WrongPassword!' })
.expect(401);

expect(mailer.sendMail).not.toHaveBeenCalled();
});
});
});
2 changes: 2 additions & 0 deletions src/utils/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import passport from "passport";
import path from "path";
import { startAnalyticsScheduler } from "./analyticsScheduler";
import { startDeletionPurgeScheduler } from "./deletionPurgeScheduler";

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/profile.controller.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/preferences.controller.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/data-export-http.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/curators.controller.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/curator-verification.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/categories.controller.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/auth.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/analytics.controller.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/controllers/__tests__/account-linking.controller.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }

Check failure on line 36 in src/utils/initialize.ts

View workflow job for this annotation

GitHub Actions / test

src/__tests__/listings.test.ts

Error: Cannot find module './deletionPurgeScheduler' imported from /home/runner/work/artisyn-api/artisyn-api/src/utils/initialize.ts ❯ src/utils/initialize.ts:36:1 ❯ src/index.ts:7:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }
import { startMediaScheduler } from "./mediaScheduler";
import { startMonitoringScheduler } from "src/services/monitoringService";

Expand Down Expand Up @@ -156,6 +157,7 @@
startLogCleanupScheduler();
startAnalyticsScheduler();
startMediaScheduler();
startDeletionPurgeScheduler();

if (process.env.NODE_ENV !== "test") {
console.log("[Security] All security services initialized successfully");
Expand Down
Loading