Skip to content

Commit

Permalink
feat: express endpoints for mail verification and password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
darkbasic committed Nov 16, 2023
1 parent b297d77 commit 78ff184
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 10 deletions.
5 changes: 5 additions & 0 deletions examples/graphql-server-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
53 changes: 46 additions & 7 deletions examples/graphql-server-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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));

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
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`);
});
})();
5 changes: 4 additions & 1 deletion packages/password/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions packages/password/src/endpoints/Request.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare namespace Express {
export interface Request {
userAgent: string;
ip: string;
infos: {
userAgent: string;
ip: string;
};
}
}
108 changes: 108 additions & 0 deletions packages/password/src/endpoints/express.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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 `
<!DOCTYPE html>
<html lang="en">
<head>
<title>${title}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
${body}
</body>
</html>
`;
}

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',
`
<h3>The email address has been successfully verified.</h3>
`
)
);
} catch (err: any) {
res.send(
//codeql[js/xss-through-exception]
getHtml(
'Email verification error',
`
<h3>The email address couldn't be verified: ${err.message ?? 'unknown error'}</h3>
`
)

Check warning

Code scanning / CodeQL

Exception text reinterpreted as HTML Medium

Exception text
is reinterpreted as HTML without escaping meta-characters.
);
}
};

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);
res.send(
getHtml(
'Password successfully changed',
`
<h3>The password has been successfully changed.</h3>
`
)
);
} catch (err: any) {
res.send(
getHtml(
'Password reset error',
`
<h3>The password couldn't be changed: ${err.message ?? 'unknown error'}</h3>
`
)

Check warning

Code scanning / CodeQL

Exception text reinterpreted as HTML Medium

Exception text
is reinterpreted as HTML without escaping meta-characters.
);
}
};

export const resetPasswordForm = (req: Request, res: Response): Response =>
res.send(
getHtml(
'Reset password',
`
<div class="container">
<h1>Reset your password</h1>
<form action="/resetPassword" method="POST">
<input type="hidden" name="token" value=${validator.escape(req.params.token)} />
<div class="form-group">
<label for="newPassword">New password</label>
<input type="text" class="form-control" id="newPassword" value="" placeholder="Enter your new password" name="newPassword">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
`
)
);
1 change: 1 addition & 0 deletions packages/password/src/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './express';
1 change: 1 addition & 0 deletions packages/password/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AccountsPassword, { AccountsPasswordOptions } from './accounts-password';
export * from './types';
export * from './endpoints';
export {
AddEmailErrors,
ChangePasswordErrors,
Expand Down
31 changes: 29 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 78ff184

Please sign in to comment.