From 2b00db44ba607e98da4f62caeacedf3dcc76cc68 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Fri, 13 Oct 2023 04:14:02 +0200 Subject: [PATCH] Handle email verification for pods without notification support (#8) When notification subscription fails, we verify the email, but we inform user that email notifications won't arrive --- src/controllers/integration.ts | 20 ++++--- src/test/css-config-no-notifications.json | 41 +++++++++++++++ src/test/integration-finish.spec.ts | 42 ++++++++++++++- src/test/testSetup.spec.ts | 64 ++++++++++++++++++++++- 4 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 src/test/css-config-no-notifications.json diff --git a/src/controllers/integration.ts b/src/controllers/integration.ts index 262325b..f252342 100644 --- a/src/controllers/integration.ts +++ b/src/controllers/integration.ts @@ -47,7 +47,7 @@ export const initializeIntegration: Middleware = async ctx => { await sendMail({ from: config.emailSender, to: email, - subject: 'TODO', + subject: 'Verify your email for sleepy.bike notifications', html: `Please verify your email click here`, text: `Please verify your email ${emailVerificationLink}`, }) @@ -103,12 +103,18 @@ export const finishIntegration: Middleware = async ctx => { // save the integration to database await Integration.create(integrationData) - // subscribe to the inbox' webhook notifications - await subscribeForNotifications(integrationData.inbox) - - ctx.response.body = - 'Email notifications have been successfully integrated to your inbox' - ctx.response.status = 200 + try { + // subscribe to the inbox' webhook notifications + await subscribeForNotifications(integrationData.inbox) + ctx.response.body = + 'Email notifications have been successfully integrated to your inbox' + } catch (e) { + ctx.response.body = + "Email was successfully verified, but notifications won't work, yet. It looks like your Solid Pod doesn't support notifications. We'll implement a workaround in the future.\nError: " + + (e as Error).message + } finally { + ctx.response.status = 200 + } } type Fetch = typeof fetch diff --git a/src/test/css-config-no-notifications.json b/src/test/css-config-no-notifications.json new file mode 100644 index 0000000..17c089e --- /dev/null +++ b/src/test/css-config-no-notifications.json @@ -0,0 +1,41 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", + "import": [ + "css:config/app/main/default.json", + "css:config/app/init/initialize-prefilled-root.json", + "css:config/app/setup/optional.json", + "css:config/app/variables/default.json", + "css:config/http/handler/default.json", + "css:config/http/middleware/default.json", + "css:config/http/notifications/disabled.json", + "css:config/http/server-factory/http.json", + "css:config/http/static/default.json", + "css:config/identity/access/public.json", + "css:config/identity/email/default.json", + "css:config/identity/handler/default.json", + "css:config/identity/ownership/token.json", + "css:config/identity/pod/static.json", + "css:config/identity/registration/enabled.json", + "css:config/ldp/authentication/dpop-bearer.json", + "css:config/ldp/authorization/webacl.json", + "css:config/ldp/handler/default.json", + "css:config/ldp/metadata-parser/default.json", + "css:config/ldp/metadata-writer/default.json", + "css:config/ldp/modes/default.json", + "css:config/storage/backend/memory.json", + "css:config/storage/key-value/resource-store.json", + "css:config/storage/middleware/default.json", + "css:config/util/auxiliary/acl.json", + "css:config/util/identifiers/suffix.json", + "css:config/util/index/default.json", + "css:config/util/logging/winston.json", + "css:config/util/representation-conversion/default.json", + "css:config/util/resource-locker/memory.json", + "css:config/util/variables/default.json" + ], + "@graph": [ + { + "comment": "A Solid server that stores its resources in memory and uses WAC for authorization." + } + ] +} diff --git a/src/test/integration-finish.spec.ts b/src/test/integration-finish.spec.ts index 33ccc7b..8bdb6d9 100644 --- a/src/test/integration-finish.spec.ts +++ b/src/test/integration-finish.spec.ts @@ -6,7 +6,12 @@ import Mail from 'nodemailer/lib/mailer' import { SinonSandbox, SinonSpy, createSandbox } from 'sinon' import { baseUrl } from '../config' import * as mailerService from '../services/mailerService' -import { authenticatedFetch, person } from './testSetup.spec' +import { + authenticatedFetch, + authenticatedFetchNoNotifications, + person, + personNoNotifications, +} from './testSetup.spec' describe('email verification via /verify-email?id=webId&token=base64Token', () => { let sendMailSpy: SinonSpy<[options: Mail.Options], Promise> @@ -85,4 +90,39 @@ describe('email verification via /verify-email?id=webId&token=base64Token', () = }) it('when we send out multiple verification emails, the last link should work') + + context("server doesn't support webhook notifications", () => { + beforeEach(async () => { + // initialize the integration + const initResponse = await authenticatedFetchNoNotifications( + `${baseUrl}/inbox`, + { + method: 'post', + headers: { + 'content-type': + 'application/ld+json;profile="https://www.w3.org/ns/activitystreams"', + }, + body: JSON.stringify({ + '@context': 'https://www.w3.org/ns/activitystreams', + '@id': '', + '@type': 'Add', + actor: personNoNotifications.webId, + object: personNoNotifications.podUrl + 'profile/card', + target: 'email@example.com', + }), + }, + ) + + expect(initResponse.status).to.equal(200) + // email was sent + const emailMessage = sendMailSpy.secondCall.firstArg.html + const $ = cheerio.load(emailMessage) + verificationLink = $('a').first().attr('href') as string + expect(verificationLink).to.not.be.null + }) + it("should verify email, but inform user that notifications aren't supported", async () => { + const response = await fetch(verificationLink) + expect(response.status).to.equal(200) + }) + }) }) diff --git a/src/test/testSetup.spec.ts b/src/test/testSetup.spec.ts index a2750cc..05ca329 100644 --- a/src/test/testSetup.spec.ts +++ b/src/test/testSetup.spec.ts @@ -8,6 +8,7 @@ import { createRandomAccount, getAuthenticatedFetch } from '../helpers' let server: Server let authenticatedFetch: typeof fetch +let authenticatedFetchNoNotifications: typeof fetch let person: { idp: string podUrl: string @@ -16,7 +17,16 @@ let person: { password: string email: string } +let personNoNotifications: { + idp: string + podUrl: string + webId: string + username: string + password: string + email: string +} let cssServer: css.App +let cssServerNoNotifications: css.App before(async function () { this.timeout(60000) @@ -51,6 +61,39 @@ after(async () => { await cssServer.stop() }) +before(async function () { + this.timeout(60000) + const start = Date.now() + + // eslint-disable-next-line no-console + console.log('Starting CSS server without notifications') + // Community Solid Server (CSS) set up following example in https://github.com/CommunitySolidServer/hello-world-component/blob/main/test/integration/Server.test.ts + cssServerNoNotifications = await new css.AppRunner().create( + { + mainModulePath: css.joinFilePath(__dirname, '../../'), // ? + typeChecking: false, // ? + dumpErrorState: false, // disable CSS error dump + }, + css.joinFilePath(__dirname, './css-config-no-notifications.json'), // CSS config + {}, + // CSS cli options + // https://github.com/CommunitySolidServer/CommunitySolidServer/tree/main#-parameters + { + port: 3457, + loggingLevel: 'off', + // seededPodConfigJson: css.joinFilePath(__dirname, './css-pod-seed.json'), // set up some Solid accounts + }, + ) + await cssServerNoNotifications.start() + + // eslint-disable-next-line no-console + console.log('CSS server started in', (Date.now() - start) / 1000, 'seconds') +}) + +after(async () => { + await cssServerNoNotifications.stop() +}) + before(done => { server = app.listen(port, done) }) @@ -89,4 +132,23 @@ beforeEach(async () => { }) }) -export { authenticatedFetch, cssServer, person, server } +beforeEach(async () => { + personNoNotifications = await createRandomAccount({ + solidServer: 'http://localhost:3457', + }) + authenticatedFetchNoNotifications = await getAuthenticatedFetch({ + email: personNoNotifications.email, + password: personNoNotifications.password, + solidServer: 'http://localhost:3457', + }) +}) + +export { + authenticatedFetch, + authenticatedFetchNoNotifications, + cssServer, + cssServerNoNotifications, + person, + personNoNotifications, + server, +}