Skip to content

Commit e3aba87

Browse files
committed
feat: express endpoints for mail verification and password reset
Closes: #1262
1 parent b297d77 commit e3aba87

File tree

8 files changed

+205
-10
lines changed

8 files changed

+205
-10
lines changed

examples/graphql-server-typescript/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
"@envelop/graphql-modules": "6.0.0",
2020
"@graphql-tools/merge": "9.0.0",
2121
"@graphql-tools/schema": "10.0.0",
22+
"express": "^4.18.2",
2223
"graphql": "16.8.1",
2324
"graphql-modules": "3.0.0-alpha-20231106133212-0b04b56e",
2425
"graphql-tag": "2.12.6",
2526
"graphql-yoga": "5.0.0",
27+
"helmet": "^7.1.0",
2628
"mongoose": "7.6.4",
2729
"tslib": "2.6.2"
30+
},
31+
"devDependencies": {
32+
"@types/express": "^4.17.21"
2833
}
2934
}

examples/graphql-server-typescript/src/index.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@ import {
66
createAccountsCoreModule,
77
} from '@accounts/module-core';
88
import { createAccountsPasswordModule } from '@accounts/module-password';
9-
import { AccountsPassword } from '@accounts/password';
9+
import {
10+
AccountsPassword,
11+
infosMiddleware,
12+
resetPassword,
13+
resetPasswordForm,
14+
verifyEmail,
15+
} from '@accounts/password';
1016
import { AccountsServer, AuthenticationServicesToken, ServerHooks } from '@accounts/server';
1117
import gql from 'graphql-tag';
1218
import mongoose from 'mongoose';
1319
import { createApplication } from 'graphql-modules';
1420
import { createAccountsMongoModule } from '@accounts/module-mongo';
15-
import { createServer } from 'node:http';
1621
import { createYoga } from 'graphql-yoga';
1722
import { useGraphQLModules } from '@envelop/graphql-modules';
23+
import express from 'express';
24+
import helmet from 'helmet';
1825

1926
void (async () => {
2027
// Create database connection
@@ -79,10 +86,14 @@ void (async () => {
7986
},
8087
};
8188

89+
const port = 4000;
90+
const siteUrl = `http://localhost:${port}`;
8291
const app = createApplication({
8392
modules: [
84-
createAccountsCoreModule({ tokenSecret: 'secret' }),
93+
createAccountsCoreModule({ tokenSecret: 'secret', siteUrl }),
8594
createAccountsPasswordModule({
95+
requireEmailVerification: true,
96+
sendVerificationEmailAfterSignup: true,
8697
// This option is called when a new user create an account
8798
// Inside we can apply our logic to validate the user fields
8899
validateNewUser: (user) => {
@@ -127,11 +138,39 @@ void (async () => {
127138
context: (ctx) => context(ctx, { createOperationController }),
128139
});
129140

130-
// Pass it into a server to hook into request handlers.
131-
const server = createServer(yoga);
141+
const yogaRouter = express.Router();
142+
// GraphiQL specefic CSP configuration
143+
yogaRouter.use(
144+
helmet({
145+
contentSecurityPolicy: {
146+
directives: {
147+
'style-src': ["'self'", 'unpkg.com'],
148+
'script-src': ["'self'", 'unpkg.com', "'unsafe-inline'"],
149+
'img-src': ["'self'", 'raw.githubusercontent.com'],
150+
},
151+
},
152+
})
153+
);
154+
yogaRouter.use(yoga);
155+
156+
const router = express.Router();
157+
// By adding the GraphQL Yoga router before the global helmet middleware,
158+
// you can be sure that the global CSP configuration will not be applied to the GraphQL Yoga endpoint
159+
router.use(yoga.graphqlEndpoint, yogaRouter);
160+
// Add the global CSP configuration for the rest of your server.
161+
router.use(helmet());
162+
router.use(express.urlencoded({ extended: true }));
163+
164+
router.use(infosMiddleware);
165+
router.get('/verify-email/:token', verifyEmail(app.injector));
166+
router.get('/reset-password/:token', resetPasswordForm);
167+
router.post('/resetPassword', resetPassword(app.injector));
168+
169+
const expressApp = express();
170+
expressApp.use(router);
132171

133172
// Start the server and you're done!
134-
server.listen(4000, () => {
135-
console.info('Server is running on http://localhost:4000/graphql');
173+
expressApp.listen(port, () => {
174+
console.info(`Server is running on ${siteUrl}/graphql`);
136175
});
137176
})();

packages/password/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@
2424
"dependencies": {
2525
"@accounts/two-factor": "^0.32.4",
2626
"bcryptjs": "2.4.3",
27-
"tslib": "2.6.2"
27+
"tslib": "2.6.2",
28+
"validator": "^13.11.0"
2829
},
2930
"devDependencies": {
3031
"@accounts/server": "^0.33.1",
3132
"@accounts/types": "^0.33.1",
3233
"@types/bcryptjs": "2.4.6",
34+
"@types/express": "^4.17.21",
3335
"@types/lodash.set": "4.3.9",
36+
"@types/validator": "^13",
3437
"graphql": "16.8.1",
3538
"graphql-modules": "3.0.0-alpha-20231106133212-0b04b56e",
3639
"lodash.set": "4.3.2",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare namespace Express {
2+
export interface Request {
3+
userAgent: string;
4+
ip: string;
5+
infos: {
6+
userAgent: string;
7+
ip: string;
8+
};
9+
}
10+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { type Injector } from 'graphql-modules';
2+
import type { Request, Response, NextFunction } from 'express';
3+
import validator from 'validator';
4+
import AccountsPassword from '../accounts-password';
5+
6+
function getHtml(title: string, body: string) {
7+
return `
8+
<!DOCTYPE html>
9+
<html lang="en">
10+
<head>
11+
<title>${title}</title>
12+
<meta charset="UTF-8">
13+
<meta name="viewport" content="width=device-width, initial-scale=1">
14+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
15+
</head>
16+
<body>
17+
${body}
18+
</body>
19+
</html>
20+
`;
21+
}
22+
23+
export const infosMiddleware = (req: Request, _res: Response, next: NextFunction) => {
24+
const userAgent = 'userAgent';
25+
const ip = 'ip';
26+
req.infos = {
27+
userAgent,
28+
ip,
29+
};
30+
next();
31+
};
32+
33+
export const verifyEmail = (injector: Injector) => async (req: Request, res: Response) => {
34+
try {
35+
const { token } = req.params;
36+
if (token == null) {
37+
throw new Error('Token is missing');
38+
}
39+
await injector.get(AccountsPassword).verifyEmail(token);
40+
res.send(
41+
getHtml(
42+
'Email successfully verified',
43+
`
44+
<h3>The email address has been successfully verified.</h3>
45+
`
46+
)
47+
);
48+
} catch (err: any) {
49+
res.send(
50+
//codeql[js/xss-through-exception]
51+
getHtml(
52+
'Email verification error',
53+
`
54+
<h3>The email address couldn't be verified: ${err.message ?? 'unknown error'}</h3>
55+
`
56+
)
57+
);
58+
}
59+
};
60+
61+
export const resetPassword = (injector: Injector) => async (req: Request, res: Response) => {
62+
try {
63+
const { token, newPassword } = req.body;
64+
if (token == null) {
65+
throw new Error('Token is missing');
66+
}
67+
if (newPassword == null) {
68+
throw new Error('New password is missing');
69+
}
70+
await injector.get(AccountsPassword).resetPassword(token, newPassword, req.infos);
71+
res.send(
72+
getHtml(
73+
'Password successfully changed',
74+
`
75+
<h3>The password has been successfully changed.</h3>
76+
`
77+
)
78+
);
79+
} catch (err: any) {
80+
//codeql[js/xss-through-exception]
81+
res.send(
82+
getHtml(
83+
'Password reset error',
84+
`
85+
<h3>The password couldn't be changed: ${err.message ?? 'unknown error'}</h3>
86+
`
87+
)
88+
);
89+
}
90+
};
91+
92+
export const resetPasswordForm = (req: Request, res: Response): Response =>
93+
res.send(
94+
getHtml(
95+
'Reset password',
96+
`
97+
<div class="container">
98+
<h1>Reset your password</h1>
99+
<form action="/resetPassword" method="POST">
100+
<input type="hidden" name="token" value=${validator.escape(req.params.token)} />
101+
<div class="form-group">
102+
<label for="newPassword">New password</label>
103+
<input type="text" class="form-control" id="newPassword" value="" placeholder="Enter your new password" name="newPassword">
104+
</div>
105+
<button type="submit" class="btn btn-primary">Submit</button>
106+
</form>
107+
`
108+
)
109+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './express';

packages/password/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AccountsPassword, { AccountsPasswordOptions } from './accounts-password';
22
export * from './types';
3+
export * from './endpoints';
34
export {
45
AddEmailErrors,
56
ChangePasswordErrors,

yarn.lock

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,13 +536,16 @@ __metadata:
536536
"@accounts/two-factor": "npm:^0.32.4"
537537
"@accounts/types": "npm:^0.33.1"
538538
"@types/bcryptjs": "npm:2.4.6"
539+
"@types/express": "npm:^4.17.21"
539540
"@types/lodash.set": "npm:4.3.9"
541+
"@types/validator": "npm:^13"
540542
bcryptjs: "npm:2.4.3"
541543
graphql: "npm:16.8.1"
542544
graphql-modules: "npm:3.0.0-alpha-20231106133212-0b04b56e"
543545
lodash.set: "npm:4.3.2"
544546
reflect-metadata: "npm:0.1.13"
545547
tslib: "npm:2.6.2"
548+
validator: "npm:^13.11.0"
546549
peerDependencies:
547550
"@accounts/server": ^0.32.0 || ^0.33.0
548551
graphql: ^16.0.0
@@ -4142,10 +4145,13 @@ __metadata:
41424145
"@envelop/graphql-modules": "npm:6.0.0"
41434146
"@graphql-tools/merge": "npm:9.0.0"
41444147
"@graphql-tools/schema": "npm:10.0.0"
4148+
"@types/express": "npm:^4.17.21"
4149+
express: "npm:^4.18.2"
41454150
graphql: "npm:16.8.1"
41464151
graphql-modules: "npm:3.0.0-alpha-20231106133212-0b04b56e"
41474152
graphql-tag: "npm:2.12.6"
41484153
graphql-yoga: "npm:5.0.0"
4154+
helmet: "npm:^7.1.0"
41494155
mongoose: "npm:7.6.4"
41504156
tslib: "npm:2.6.2"
41514157
languageName: unknown
@@ -7911,7 +7917,7 @@ __metadata:
79117917
languageName: node
79127918
linkType: hard
79137919

7914-
"@types/express@npm:*, @types/express@npm:4.17.21, @types/express@npm:^4.17.13":
7920+
"@types/express@npm:*, @types/express@npm:4.17.21, @types/express@npm:^4.17.13, @types/express@npm:^4.17.21":
79157921
version: 4.17.21
79167922
resolution: "@types/express@npm:4.17.21"
79177923
dependencies:
@@ -8507,6 +8513,13 @@ __metadata:
85078513
languageName: node
85088514
linkType: hard
85098515

8516+
"@types/validator@npm:^13":
8517+
version: 13.11.6
8518+
resolution: "@types/validator@npm:13.11.6"
8519+
checksum: 3201902a8e5d4784d1c67f5a5a796d1500bae10fe5413ed75fdbdf5d6b5572952445f3482ffe64908531b20171d4c5cfe94934de3fd401781bb6cf9f95766b02
8520+
languageName: node
8521+
linkType: hard
8522+
85108523
"@types/webidl-conversions@npm:*":
85118524
version: 7.0.3
85128525
resolution: "@types/webidl-conversions@npm:7.0.3"
@@ -14160,7 +14173,7 @@ __metadata:
1416014173
languageName: node
1416114174
linkType: hard
1416214175

14163-
"express@npm:4.18.2, express@npm:^4.17.1, express@npm:^4.17.3":
14176+
"express@npm:4.18.2, express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.2":
1416414177
version: 4.18.2
1416514178
resolution: "express@npm:4.18.2"
1416614179
dependencies:
@@ -15783,6 +15796,13 @@ __metadata:
1578315796
languageName: node
1578415797
linkType: hard
1578515798

15799+
"helmet@npm:^7.1.0":
15800+
version: 7.1.0
15801+
resolution: "helmet@npm:7.1.0"
15802+
checksum: 8c3370d07487be11ac918577c68952e05d779a1a2c037023c1ba763034c381a025899bc52f8acfab5209304a1dc618a3764dbfd26386a0d1173befe4fb932e84
15803+
languageName: node
15804+
linkType: hard
15805+
1578615806
"highlight.js@npm:^10.7.1":
1578715807
version: 10.7.3
1578815808
resolution: "highlight.js@npm:10.7.3"
@@ -28766,6 +28786,13 @@ __metadata:
2876628786
languageName: node
2876728787
linkType: hard
2876828788

28789+
"validator@npm:^13.11.0":
28790+
version: 13.11.0
28791+
resolution: "validator@npm:13.11.0"
28792+
checksum: 0107da3add5a4ebc6391dac103c55f6d8ed055bbcc29a4c9cbf89eacfc39ba102a5618c470bdc33c6487d30847771a892134a8c791f06ef0962dd4b7a60ae0f5
28793+
languageName: node
28794+
linkType: hard
28795+
2876928796
"value-equal@npm:^1.0.1":
2877028797
version: 1.0.1
2877128798
resolution: "value-equal@npm:1.0.1"

0 commit comments

Comments
 (0)