feat(intake-emails): when deleting intake email in organization, delete in OwlRelay too (#192)

* feat(intake-emails): delete email in owlrelay too

* Update apps/papra-server/src/modules/intake-emails/drivers/random-username/random-username.intake-email-driver.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Corentin Thomasset
2025-04-04 16:23:55 +02:00
committed by GitHub
parent 979df5dad8
commit 79eafdb3ee
10 changed files with 64 additions and 14 deletions

View File

@@ -37,7 +37,7 @@
"@crowlog/logger": "^1.1.0",
"@hono/node-server": "^1.13.7",
"@libsql/client": "^0.14.0",
"@owlrelay/api-sdk": "^0.0.1",
"@owlrelay/api-sdk": "^0.0.2",
"@owlrelay/webhook": "^0.0.3",
"@papra/lecture": "^0.0.4",
"@paralleldrive/cuid2": "^2.2.2",

View File

@@ -3,6 +3,7 @@ import type { Config } from '../../config/config.types';
export type IntakeEmailsServices = {
name: string;
generateEmailAddress: () => Promise<{ emailAddress: string }>;
deleteEmailAddress: ({ emailAddress }: { emailAddress: string }) => Promise<void>;
};
export type IntakeEmailDriverFactory = (args: { config: Config }) => IntakeEmailsServices;

View File

@@ -1,12 +1,15 @@
import { buildUrl } from '@corentinth/chisels';
import { buildUrl, safely } from '@corentinth/chisels';
import { generateId as generateHumanReadableId } from '@corentinth/friendly-ids';
import { createClient } from '@owlrelay/api-sdk';
import { createLogger } from '../../../shared/logger/logger';
import { INTAKE_EMAILS_INGEST_ROUTE } from '../../intake-emails.constants';
import { buildEmailAddress } from '../../intake-emails.models';
import { defineIntakeEmailDriver } from '../intake-emails.drivers.models';
export const OWLRELAY_INTAKE_EMAIL_DRIVER_NAME = 'owlrelay';
const logger = createLogger({ namespace: 'intake-emails.drivers.owlrelay' });
export const owlrelayIntakeEmailDriverFactory = defineIntakeEmailDriver(({ config }) => {
const { baseUrl } = config.server;
const { webhookSecret } = config.intakeEmails;
@@ -21,7 +24,7 @@ export const owlrelayIntakeEmailDriverFactory = defineIntakeEmailDriver(({ confi
return {
name: OWLRELAY_INTAKE_EMAIL_DRIVER_NAME,
generateEmailAddress: async () => {
const { domain, username } = await client.createEmail({
const { domain, username, id: owlrelayEmailId } = await client.createEmail({
username: generateHumanReadableId(),
webhookUrl,
webhookSecret,
@@ -29,9 +32,21 @@ export const owlrelayIntakeEmailDriverFactory = defineIntakeEmailDriver(({ confi
const emailAddress = buildEmailAddress({ username, domain });
logger.info({ emailAddress, owlrelayEmailId }, 'Created email address in OwlRelay');
return {
emailAddress,
};
},
deleteEmailAddress: async ({ emailAddress }) => {
const [, error] = await safely(client.deleteEmail({ emailAddress }));
if (error) {
logger.error({ error }, 'Failed to delete email address in OwlRelay');
return;
}
logger.info({ emailAddress }, 'Deleted email address in OwlRelay');
},
};
});

View File

@@ -15,5 +15,7 @@ export const randomUsernameIntakeEmailDriverFactory = defineIntakeEmailDriver(({
emailAddress: `${randomUsername}@${domain}`,
};
},
// Deletion functionality is not required for this driver
deleteEmailAddress: async () => {},
};
});

View File

@@ -5,3 +5,9 @@ export const createIntakeEmailLimitReachedError = createErrorFactory({
code: 'intake_email.limit_reached',
statusCode: 403,
});
export const createIntakeEmailNotFoundError = createErrorFactory({
message: 'Intake email not found',
code: 'intake_email.not_found',
statusCode: 404,
});

View File

@@ -44,6 +44,7 @@ describe('intake-emails repository', () => {
const { intakeEmail: retrievedIntakeEmail } = await intakeEmailsRepository.getIntakeEmail({
intakeEmailId: intakeEmail.id,
organizationId: 'organization-1',
});
expect(

View File

@@ -47,12 +47,15 @@ async function updateIntakeEmail({ intakeEmailId, organizationId, isEnabled, all
return { intakeEmail };
}
async function getIntakeEmail({ intakeEmailId, db }: { intakeEmailId: string; db: Database }) {
async function getIntakeEmail({ intakeEmailId, organizationId, db }: { intakeEmailId: string; organizationId: string; db: Database }) {
const [intakeEmail] = await db
.select()
.from(intakeEmailsTable)
.where(
eq(intakeEmailsTable.id, intakeEmailId),
and(
eq(intakeEmailsTable.id, intakeEmailId),
eq(intakeEmailsTable.organizationId, organizationId),
),
);
return { intakeEmail };

View File

@@ -18,7 +18,7 @@ import { INTAKE_EMAILS_INGEST_ROUTE } from './intake-emails.constants';
import { createIntakeEmailsRepository } from './intake-emails.repository';
import { intakeEmailsIngestionMetaSchema, parseJson } from './intake-emails.schemas';
import { createIntakeEmailsServices } from './intake-emails.services';
import { createIntakeEmail, processIntakeEmailIngestion } from './intake-emails.usecases';
import { createIntakeEmail, deleteIntakeEmail, processIntakeEmailIngestion } from './intake-emails.usecases';
const logger = createLogger({ namespace: 'intake-emails.routes' });
@@ -86,7 +86,7 @@ function setupCreateIntakeEmailRoute({ app, db, config }: RouteDefinitionContext
);
}
function setupDeleteIntakeEmailRoute({ app, db }: RouteDefinitionContext) {
function setupDeleteIntakeEmailRoute({ app, db, config }: RouteDefinitionContext) {
app.delete(
'/api/organizations/:organizationId/intake-emails/:intakeEmailId',
validateParams(z.object({
@@ -99,10 +99,11 @@ function setupDeleteIntakeEmailRoute({ app, db }: RouteDefinitionContext) {
const organizationsRepository = createOrganizationsRepository({ db });
const intakeEmailsRepository = createIntakeEmailsRepository({ db });
const intakeEmailsServices = createIntakeEmailsServices({ config });
await ensureUserIsInOrganization({ userId, organizationId, organizationsRepository });
await intakeEmailsRepository.deleteIntakeEmail({ intakeEmailId, organizationId });
await deleteIntakeEmail({ intakeEmailId, organizationId, intakeEmailsRepository, intakeEmailsServices });
return context.body(null, 204);
},

View File

@@ -10,7 +10,7 @@ import { safely } from '@corentinth/chisels';
import { createDocument } from '../documents/documents.usecases';
import { getOrganizationPlan } from '../plans/plans.usecases';
import { addLogContext, createLogger } from '../shared/logger/logger';
import { createIntakeEmailLimitReachedError } from './intake-emails.errors';
import { createIntakeEmailLimitReachedError, createIntakeEmailNotFoundError } from './intake-emails.errors';
import { getIsFromAllowedOrigin } from './intake-emails.models';
export async function createIntakeEmail({
@@ -165,3 +165,24 @@ export async function checkIfOrganizationCanCreateNewIntakeEmail({
throw createIntakeEmailLimitReachedError();
}
}
export async function deleteIntakeEmail({
intakeEmailId,
organizationId,
intakeEmailsRepository,
intakeEmailsServices,
}: {
intakeEmailId: string;
organizationId: string;
intakeEmailsRepository: IntakeEmailsRepository;
intakeEmailsServices: IntakeEmailsServices;
}) {
const { intakeEmail } = await intakeEmailsRepository.getIntakeEmail({ intakeEmailId, organizationId });
if (!intakeEmail) {
throw createIntakeEmailNotFoundError();
}
await intakeEmailsRepository.deleteIntakeEmail({ organizationId: intakeEmail.organizationId, intakeEmailId });
await intakeEmailsServices.deleteEmailAddress({ emailAddress: intakeEmail.emailAddress });
}

10
pnpm-lock.yaml generated
View File

@@ -236,8 +236,8 @@ importers:
specifier: ^0.14.0
version: 0.14.0
'@owlrelay/api-sdk':
specifier: ^0.0.1
version: 0.0.1
specifier: ^0.0.2
version: 0.0.2
'@owlrelay/webhook':
specifier: ^0.0.3
version: 0.0.3
@@ -2064,8 +2064,8 @@ packages:
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
'@owlrelay/api-sdk@0.0.1':
resolution: {integrity: sha512-/4x/J4ktb9z3Zf3VfX7tjQYYqgn8bMXk7hbYePoGknijk90QRjsfsVjpPjl2fgUs+ZGFeOQqdsgLmpxq2d1NGA==}
'@owlrelay/api-sdk@0.0.2':
resolution: {integrity: sha512-7WzTd/IKiT/h9Y90O7aCdrdZVk26Tb4f4EkIGRzzF0hvujmrp98R3TNji/ZD9As0v6OOilMlMYm4xlZGQzu+bQ==}
engines: {node: '>=20.0.0'}
'@owlrelay/webhook@0.0.3':
@@ -8551,7 +8551,7 @@ snapshots:
'@oslojs/encoding@1.1.0': {}
'@owlrelay/api-sdk@0.0.1':
'@owlrelay/api-sdk@0.0.2':
dependencies:
ofetch: 1.4.1