From a87a21c8a0ebac5da26d6ff84e6566dc8a05785c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Belli?= Date: Thu, 16 Nov 2023 11:14:48 +0100 Subject: [PATCH] feat: express endpoints for mail verification and password reset --- .../graphql-server-typescript/package.json | 5 + .../graphql-server-typescript/src/index.ts | 53 +++++++-- packages/password/package.json | 5 +- packages/password/src/endpoints/Request.d.ts | 10 ++ packages/password/src/endpoints/express.ts | 109 ++++++++++++++++++ packages/password/src/endpoints/index.ts | 1 + packages/password/src/index.ts | 1 + yarn.lock | 31 ++++- 8 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 packages/password/src/endpoints/Request.d.ts create mode 100644 packages/password/src/endpoints/express.ts create mode 100644 packages/password/src/endpoints/index.ts diff --git a/examples/graphql-server-typescript/package.json b/examples/graphql-server-typescript/package.json index 01bff74c6..8f4dd9260 100644 --- a/examples/graphql-server-typescript/package.json +++ b/examples/graphql-server-typescript/package.json @@ -19,11 +19,16 @@ "@envelop/graphql-modules": "6.0.0", "@graphql-tools/merge": "9.0.0", "@graphql-tools/schema": "10.0.0", + "express": "^4.18.2", "graphql": "16.8.1", "graphql-modules": "3.0.0-alpha-20231106133212-0b04b56e", "graphql-tag": "2.12.6", "graphql-yoga": "5.0.0", + "helmet": "^7.1.0", "mongoose": "7.6.4", "tslib": "2.6.2" + }, + "devDependencies": { + "@types/express": "^4.17.21" } } diff --git a/examples/graphql-server-typescript/src/index.ts b/examples/graphql-server-typescript/src/index.ts index 8d88988c3..5f9a027a4 100644 --- a/examples/graphql-server-typescript/src/index.ts +++ b/examples/graphql-server-typescript/src/index.ts @@ -6,15 +6,22 @@ import { createAccountsCoreModule, } from '@accounts/module-core'; import { createAccountsPasswordModule } from '@accounts/module-password'; -import { AccountsPassword } from '@accounts/password'; +import { + AccountsPassword, + infosMiddleware, + resetPassword, + resetPasswordForm, + verifyEmail, +} from '@accounts/password'; import { AccountsServer, AuthenticationServicesToken, ServerHooks } from '@accounts/server'; import gql from 'graphql-tag'; import mongoose from 'mongoose'; import { createApplication } from 'graphql-modules'; import { createAccountsMongoModule } from '@accounts/module-mongo'; -import { createServer } from 'node:http'; import { createYoga } from 'graphql-yoga'; import { useGraphQLModules } from '@envelop/graphql-modules'; +import express from 'express'; +import helmet from 'helmet'; void (async () => { // Create database connection @@ -79,10 +86,14 @@ void (async () => { }, }; + const port = 4000; + const siteUrl = `http://localhost:${port}`; const app = createApplication({ modules: [ - createAccountsCoreModule({ tokenSecret: 'secret' }), + createAccountsCoreModule({ tokenSecret: 'secret', siteUrl }), createAccountsPasswordModule({ + requireEmailVerification: true, + sendVerificationEmailAfterSignup: true, // This option is called when a new user create an account // Inside we can apply our logic to validate the user fields validateNewUser: (user) => { @@ -127,11 +138,39 @@ void (async () => { context: (ctx) => context(ctx, { createOperationController }), }); - // Pass it into a server to hook into request handlers. - const server = createServer(yoga); + const yogaRouter = express.Router(); + // GraphiQL specefic CSP configuration + yogaRouter.use( + helmet({ + contentSecurityPolicy: { + directives: { + 'style-src': ["'self'", 'unpkg.com'], + 'script-src': ["'self'", 'unpkg.com', "'unsafe-inline'"], + 'img-src': ["'self'", 'raw.githubusercontent.com'], + }, + }, + }) + ); + yogaRouter.use(yoga); + + const router = express.Router(); + // By adding the GraphQL Yoga router before the global helmet middleware, + // you can be sure that the global CSP configuration will not be applied to the GraphQL Yoga endpoint + router.use(yoga.graphqlEndpoint, yogaRouter); + // Add the global CSP configuration for the rest of your server. + router.use(helmet()); + router.use(express.urlencoded({ extended: true })); + + router.use(infosMiddleware); + router.get('/verify-email/:token', verifyEmail(app.injector)); + router.get('/reset-password/:token', resetPasswordForm); + router.post('/resetPassword', resetPassword(app.injector)); + + const expressApp = express(); + expressApp.use(router); // Start the server and you're done! - server.listen(4000, () => { - console.info('Server is running on http://localhost:4000/graphql'); + expressApp.listen(port, () => { + console.info(`Server is running on ${siteUrl}/graphql`); }); })(); diff --git a/packages/password/package.json b/packages/password/package.json index 898ea22db..f21250a70 100644 --- a/packages/password/package.json +++ b/packages/password/package.json @@ -24,13 +24,16 @@ "dependencies": { "@accounts/two-factor": "^0.32.4", "bcryptjs": "2.4.3", - "tslib": "2.6.2" + "tslib": "2.6.2", + "validator": "^13.11.0" }, "devDependencies": { "@accounts/server": "^0.33.1", "@accounts/types": "^0.33.1", "@types/bcryptjs": "2.4.6", + "@types/express": "^4.17.21", "@types/lodash.set": "4.3.9", + "@types/validator": "^13", "graphql": "16.8.1", "graphql-modules": "3.0.0-alpha-20231106133212-0b04b56e", "lodash.set": "4.3.2", diff --git a/packages/password/src/endpoints/Request.d.ts b/packages/password/src/endpoints/Request.d.ts new file mode 100644 index 000000000..843a19180 --- /dev/null +++ b/packages/password/src/endpoints/Request.d.ts @@ -0,0 +1,10 @@ +declare namespace Express { + export interface Request { + userAgent: string; + ip: string; + infos: { + userAgent: string; + ip: string; + }; + } +} diff --git a/packages/password/src/endpoints/express.ts b/packages/password/src/endpoints/express.ts new file mode 100644 index 000000000..3ae27f625 --- /dev/null +++ b/packages/password/src/endpoints/express.ts @@ -0,0 +1,109 @@ +import { type Injector } from 'graphql-modules'; +import type { Request, Response, NextFunction } from 'express'; +import validator from 'validator'; +import AccountsPassword from '../accounts-password'; + +function getHtml(title: string, body: string) { + return ` + + + + ${title} + + + + + + ${body} + + + `; +} + +export const infosMiddleware = (req: Request, _res: Response, next: NextFunction) => { + const userAgent = 'userAgent'; + const ip = 'ip'; + req.infos = { + userAgent, + ip, + }; + next(); +}; + +export const verifyEmail = (injector: Injector) => async (req: Request, res: Response) => { + try { + const { token } = req.params; + if (token == null) { + throw new Error('Token is missing'); + } + await injector.get(AccountsPassword).verifyEmail(token); + res.send( + getHtml( + 'Email successfully verified', + ` +

The email address has been successfully verified.

+ ` + ) + ); + } catch (err: any) { + res.send( + //codeql[js/xss-through-exception] + getHtml( + 'Email verification error', + ` +

The email address couldn't be verified: ${err.message ?? 'unknown error'}

+ ` + ) + ); + } +}; + +export const resetPassword = (injector: Injector) => async (req: Request, res: Response) => { + try { + const { token, newPassword } = req.body; + if (token == null) { + throw new Error('Token is missing'); + } + if (newPassword == null) { + throw new Error('New password is missing'); + } + await injector.get(AccountsPassword).resetPassword(token, newPassword, req.infos); + //codeql[js/xss-through-exception] + res.send( + getHtml( + 'Password successfully changed', + ` +

The password has been successfully changed.

+ ` + ) + ); + } catch (err: any) { + res.send( + getHtml( + 'Password reset error', + ` +

The password couldn't be changed: ${err.message ?? 'unknown error'}

+ ` + ) + ); + } +}; + +export const resetPasswordForm = (req: Request, res: Response): Response => + res.send( + getHtml( + 'Reset password', + ` +
+

Reset your password

+
+ +
+ + +
+ +
+ ` + ) + ); diff --git a/packages/password/src/endpoints/index.ts b/packages/password/src/endpoints/index.ts new file mode 100644 index 000000000..30bdee857 --- /dev/null +++ b/packages/password/src/endpoints/index.ts @@ -0,0 +1 @@ +export * from './express'; diff --git a/packages/password/src/index.ts b/packages/password/src/index.ts index 2357958e3..2adbc5f87 100644 --- a/packages/password/src/index.ts +++ b/packages/password/src/index.ts @@ -1,5 +1,6 @@ import AccountsPassword, { AccountsPasswordOptions } from './accounts-password'; export * from './types'; +export * from './endpoints'; export { AddEmailErrors, ChangePasswordErrors, diff --git a/yarn.lock b/yarn.lock index 793fa99ae..4a5b6a03f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -536,13 +536,16 @@ __metadata: "@accounts/two-factor": "npm:^0.32.4" "@accounts/types": "npm:^0.33.1" "@types/bcryptjs": "npm:2.4.6" + "@types/express": "npm:^4.17.21" "@types/lodash.set": "npm:4.3.9" + "@types/validator": "npm:^13" bcryptjs: "npm:2.4.3" graphql: "npm:16.8.1" graphql-modules: "npm:3.0.0-alpha-20231106133212-0b04b56e" lodash.set: "npm:4.3.2" reflect-metadata: "npm:0.1.13" tslib: "npm:2.6.2" + validator: "npm:^13.11.0" peerDependencies: "@accounts/server": ^0.32.0 || ^0.33.0 graphql: ^16.0.0 @@ -4142,10 +4145,13 @@ __metadata: "@envelop/graphql-modules": "npm:6.0.0" "@graphql-tools/merge": "npm:9.0.0" "@graphql-tools/schema": "npm:10.0.0" + "@types/express": "npm:^4.17.21" + express: "npm:^4.18.2" graphql: "npm:16.8.1" graphql-modules: "npm:3.0.0-alpha-20231106133212-0b04b56e" graphql-tag: "npm:2.12.6" graphql-yoga: "npm:5.0.0" + helmet: "npm:^7.1.0" mongoose: "npm:7.6.4" tslib: "npm:2.6.2" languageName: unknown @@ -7911,7 +7917,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:4.17.21, @types/express@npm:^4.17.13": +"@types/express@npm:*, @types/express@npm:4.17.21, @types/express@npm:^4.17.13, @types/express@npm:^4.17.21": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -8507,6 +8513,13 @@ __metadata: languageName: node linkType: hard +"@types/validator@npm:^13": + version: 13.11.6 + resolution: "@types/validator@npm:13.11.6" + checksum: 3201902a8e5d4784d1c67f5a5a796d1500bae10fe5413ed75fdbdf5d6b5572952445f3482ffe64908531b20171d4c5cfe94934de3fd401781bb6cf9f95766b02 + languageName: node + linkType: hard + "@types/webidl-conversions@npm:*": version: 7.0.3 resolution: "@types/webidl-conversions@npm:7.0.3" @@ -14160,7 +14173,7 @@ __metadata: languageName: node linkType: hard -"express@npm:4.18.2, express@npm:^4.17.1, express@npm:^4.17.3": +"express@npm:4.18.2, express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" dependencies: @@ -15783,6 +15796,13 @@ __metadata: languageName: node linkType: hard +"helmet@npm:^7.1.0": + version: 7.1.0 + resolution: "helmet@npm:7.1.0" + checksum: 8c3370d07487be11ac918577c68952e05d779a1a2c037023c1ba763034c381a025899bc52f8acfab5209304a1dc618a3764dbfd26386a0d1173befe4fb932e84 + languageName: node + linkType: hard + "highlight.js@npm:^10.7.1": version: 10.7.3 resolution: "highlight.js@npm:10.7.3" @@ -28766,6 +28786,13 @@ __metadata: languageName: node linkType: hard +"validator@npm:^13.11.0": + version: 13.11.0 + resolution: "validator@npm:13.11.0" + checksum: 0107da3add5a4ebc6391dac103c55f6d8ed055bbcc29a4c9cbf89eacfc39ba102a5618c470bdc33c6487d30847771a892134a8c791f06ef0962dd4b7a60ae0f5 + languageName: node + linkType: hard + "value-equal@npm:^1.0.1": version: 1.0.1 resolution: "value-equal@npm:1.0.1"