Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: express endpoints for mail verification and password reset #1262

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/@accounts_password-1262-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@accounts/password': patch
---
dependencies updates:
- Added dependency [`validator@^13.11.0` ↗︎](https://www.npmjs.com/package/validator/v/13.11.0) (to `dependencies`)
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;
};
}
}
109 changes: 109 additions & 0 deletions packages/password/src/endpoints/express.ts
Original file line number Diff line number Diff line change
@@ -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 `
<!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>
`
)
Comment on lines +51 to +56

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) {
//codeql[js/xss-through-exception]
res.send(
getHtml(
'Password reset error',
`
<h3>The password couldn't be changed: ${err.message ?? 'unknown error'}</h3>
`
)
Comment on lines +82 to +87

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