Skip to content

Commit 7da7462

Browse files
authored
Merge pull request #73 from neonexus/master
Built out PnwedPasswords.com API functionality into `is-password-valid` helper.
2 parents 670a60c + 18a64dc commit 7da7462

File tree

81 files changed

+2312
-4756
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+2312
-4756
lines changed

.idea/dictionaries/neonexusdemortis.xml

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [v3.2.0](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v3.1.1...v3.2.0) (2022-11-16)
4+
5+
### Features
6+
7+
* Built out PnwedPasswords.com (HaveIBeenPwned.com) API functionality into `is-password-valid` helper.
8+
* Can be disabled in [config/security.js](config/security.js).
9+
* FINALLY removed the usage of `res._headers`, so no more annoying deprecation message.
10+
* Simplified stored session data.
11+
* Updated dependencies.
12+
313
## [v3.1.1](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v3.1.0...v3.1.1) (2022-09-08)
414

515
### Features

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:16.17
1+
FROM node:18.12
22
MAINTAINER NeoNexus DeMortis
33

44
RUN apt-get update && apt-get upgrade -y
@@ -22,7 +22,7 @@ COPY assets /var/www/myapp/assets
2222
COPY webpack /var/www/myapp/webpack
2323
RUN npm run build
2424

25-
# Copy the reset of the app
25+
# Copy the rest of the app
2626
COPY . /var/www/myapp/
2727

2828
# Expose the compiled public assets, so Nginx can route to them, instead of using Sails to do the file serving.

README.md

+22-16
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ Need help? Want to hire me to build your next app or prototype? You can contact
88

99
## Main Features
1010

11-
+ Automatic (incoming) request logging (manual outgoing), via Sails models / hooks.
12-
+ Setup for Webpack auto-reload dev server.
13-
+ Setup so Sails will serve Webpack-built bundles as separate apps (so, a marketing site, and an admin site can live side-by-side).
14-
+ Includes [react-bootstrap](https://www.npmjs.com/package/react-bootstrap) to make using Bootstrap styles / features with React easier.
15-
+ Schema validation and enforcement for `PRODUCTION`. This repo is set up for `MySQL`. If you plan to use a different datastore, you will likely want to disable the schema validation and enforcement feature inside [`config/bootstrap.js`](config/bootstrap.js). See [schema validation and enforcement](#schema-validation-and-enforcement) for more info.
11+
* Automatic (incoming) request logging (manual outgoing), via Sails models / hooks.
12+
* Setup for Webpack auto-reload dev server.
13+
* Setup so Sails will serve Webpack-built bundles as separate apps (so, a marketing site, and an admin site can live side-by-side).
14+
* Includes [react-bootstrap](https://www.npmjs.com/package/react-bootstrap) to make using Bootstrap styles / features with React easier.
15+
* Schema validation and enforcement for `PRODUCTION`. This repo is set up for `MySQL`. If you plan to use a different datastore, you will likely want to disable the schema validation and enforcement feature inside [`config/bootstrap.js`](config/bootstrap.js). See [schema validation and enforcement](#schema-validation-and-enforcement) for more info.
16+
* New passwords can be checked against the [PwnedPasswords API](https://haveibeenpwned.com/API/v3#PwnedPasswords). If there is a single hit for the password, an error will be given, and the user will be forced to choose another. See [PwnedPasswords integration](#pwnedpasswordscom-integration) for more info.
1617

1718
## Branch Warning
1819
The `master` branch is experimental, and the [release branch](https://github.com/neonexus/sails-react-bootstrap-webpack/tree/release) (or the [`releases section`](https://github.com/neonexus/sails-react-bootstrap-webpack/releases)) is where one should base their use of this template.
@@ -70,10 +71,10 @@ If you DO NOT like this behavior, and would prefer the variables stay the same a
7071
|-------------------------------------------------------------------------|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
7172
| ASSETS_URL | "" (empty string) | Webpack is configured to modify static asset URLs to point to a CDN, like CloudFront. MUST end with a slash " / ", or be empty. |
7273
| BASE_URL | https://myapi.app | The address of the Sails instance. |
73-
| **DEV:**&nbsp;&nbsp;&nbsp;&nbsp;DB_HOST<br />**PROD:**&nbsp;DB_HOSTNAME | localhost | The hostname of the datastore. |
74-
| **DEV:**&nbsp;&nbsp;&nbsp;&nbsp;DB_USER<br />**PROD:**&nbsp;DB_USERNAME | **DEV:**&nbsp;&nbsp;&nbsp;&nbsp;root <br /> **PROD:**&nbsp;produser | Username of the datastore. |
75-
| **DEV:**&nbsp;&nbsp;&nbsp;&nbsp;DB_PASS<br />**PROD:**&nbsp;DB_PASSWORD | **DEV:**&nbsp;&nbsp;&nbsp;&nbsp;mypass <br /> **PROD:**&nbsp;prodpass | Password of the datastore. |
76-
| DB_NAME | **DEV:**&nbsp;&nbsp;&nbsp;&nbsp;myapp <br /> **PROD:**&nbsp;prod | The name of the database inside the datastore. |
74+
| &nbsp;&nbsp;&nbsp;**DEV:**&nbsp;DB_HOST<br />**PROD:**&nbsp;DB_HOSTNAME | localhost | The hostname of the datastore. |
75+
| &nbsp;&nbsp;&nbsp;**DEV:**&nbsp;DB_USER<br />**PROD:**&nbsp;DB_USERNAME | &nbsp;&nbsp;&nbsp;**DEV:**&nbsp;root <br /> **PROD:**&nbsp;produser | Username of the datastore. |
76+
| &nbsp;&nbsp;&nbsp;**DEV:**&nbsp;DB_PASS<br />**PROD:**&nbsp;DB_PASSWORD | &nbsp;&nbsp;&nbsp;**DEV:**&nbsp;mypass <br /> **PROD:**&nbsp;prodpass | Password of the datastore. |
77+
| DB_NAME | &nbsp;&nbsp;&nbsp;**DEV:**&nbsp;myapp <br /> **PROD:**&nbsp;prod | The name of the database inside the datastore. |
7778
| DB_PORT | 3306 | The port number for the datastore. |
7879
| DB_SSL | true | If the datastore requires SSL, set this to "true". |
7980
| SESSION_SECRET | "" (empty string) | Used to sign cookies, and SHOULD be set, especially on PRODUCTION environments. |
@@ -115,6 +116,11 @@ module.exports.bootstrap = function(next) {
115116
};
116117
```
117118

119+
## PwnedPasswords.com Integration
120+
When a new password is being created, it is checked with the [PwnedPasswords.com API](https://haveibeenpwned.com/API/v3#PwnedPasswords). This API uses a k-anonymity model, so the password that is searched for is never exposed to the API. Basically, the password is hashed, then the first 5 characters are sent to the API, and the API returns any hashes that start with those 5 characters, including the amount of times that hash (aka password) has been found in known security breaches.
121+
122+
This functionality is turned on by default, and can be shutoff per-use, or globally throughout the app. `sails.helpers.isPasswordValid` can be used with `skipPwned` option set to `true`, to disable the check per use. Inside of [`config/security.js`](config/security.js), the variable `checkPwned` can be set to `false` to disable it globally.
123+
118124
## What about SEO?
119125
I recommend looking at [prerender.io](https://prerender.io). They offer a service (free up to 250 pages) that caches the end result of a JavaScript-rendered view (React, Vue, Angular), allowing search engines to crawl otherwise un-crawlable web views. You can use the service in a number of ways. One way, is to use the [prerender-node](https://www.npmjs.com/package/prerender-node) package. To use it with Sails, you'll have to add it to the [HTTP Middleware](https://sailsjs.com/documentation/concepts/middleware#?http-middleware). Here's a quick example:
120126

@@ -142,13 +148,13 @@ middleware: {
142148

143149
### Useful Links
144150

145-
+ [Sails Framework Documentation](https://sailsjs.com/get-started)
146-
+ [Sails Deployment Tips](https://sailsjs.com/documentation/concepts/deployment)
147-
+ [Sails Community Support Options](https://sailsjs.com/support)
148-
+ [Sails Professional / Enterprise Options](https://sailsjs.com/enterprise)
149-
+ [`react-bootstrap` Documentation](https://react-bootstrap.netlify.app/)
150-
+ [Webpack Documentation](https://webpack.js.org/)
151-
+ [Simple data fixtures for testing Sails.js (the npm package `fixted`)](https://www.npmjs.com/package/fixted)
151+
* [Sails Framework Documentation](https://sailsjs.com/get-started)
152+
* [Sails Deployment Tips](https://sailsjs.com/documentation/concepts/deployment)
153+
* [Sails Community Support Options](https://sailsjs.com/support)
154+
* [Sails Professional / Enterprise Options](https://sailsjs.com/enterprise)
155+
* [`react-bootstrap` Documentation](https://react-bootstrap.netlify.app/)
156+
* [Webpack Documentation](https://webpack.js.org/)
157+
* [Simple data fixtures for testing Sails.js (the npm package `fixted`)](https://www.npmjs.com/package/fixted)
152158

153159

154160
### Version info

api/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ See: [Actions and Controllers](https://sailsjs.com/documentation/concepts/action
1010

1111
## Helpers
1212

13-
Helpers are generic, reusable functions used by multiple controllers (or hooks, policies, etc).
13+
Helpers are generic, reusable functions that can be used by controllers, hooks, models, policies, or responses.
1414

1515
See: [Helpers](https://sailsjs.com/documentation/concepts/helpers)
1616

api/controllers/admin/create-user.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ module.exports = {
6060
let isPasswordValid;
6161

6262
if (inputs.setPassword) {
63-
isPasswordValid = sails.helpers.isPasswordValid.with({
63+
isPasswordValid = await sails.helpers.isPasswordValid.with({
6464
password: inputs.password,
6565
user: {firstName: inputs.firstName, lastName: inputs.lastName, email: inputs.email}
6666
});

api/controllers/admin/delete-user.js

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ module.exports = {
2626
},
2727

2828
fn: async (inputs, exits, env) => {
29+
if (inputs.id === env.req.session.user.id) {
30+
return exits.badRequest('One does not simply delete themselves...');
31+
}
32+
2933
const foundUser = await sails.models.user.findOne({id: inputs.id, deletedAt: null});
3034

3135
if (!foundUser) {

api/controllers/admin/edit-user.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
module.exports = {
2+
friendlyName: 'Edit User',
3+
4+
description: 'Edit an active user.',
5+
6+
inputs: {
7+
id: {
8+
type: 'string',
9+
required: true,
10+
isUUID: true
11+
},
12+
13+
firstName: {
14+
type: 'string',
15+
required: true,
16+
maxLength: 70
17+
},
18+
19+
lastName: {
20+
type: 'string',
21+
required: true,
22+
maxLength: 70
23+
},
24+
25+
password: {
26+
type: 'string',
27+
maxLength: 70
28+
},
29+
30+
email: {
31+
type: 'string',
32+
isEmail: true,
33+
required: true,
34+
maxLength: 191
35+
},
36+
37+
role: {
38+
type: 'string',
39+
defaultsTo: 'user',
40+
isIn: [
41+
'user',
42+
'admin'
43+
]
44+
},
45+
46+
setPassword: {
47+
type: 'boolean',
48+
defaultsTo: true
49+
}
50+
},
51+
52+
exits: {
53+
ok: {
54+
responseType: 'created'
55+
},
56+
badRequest: {
57+
responseType: 'badRequest'
58+
},
59+
serverError: {
60+
responseType: 'serverError'
61+
}
62+
},
63+
64+
fn: async (inputs, exits) => {
65+
let isPasswordValid = true;
66+
const foundUser = await sails.models.user.findOne({id: inputs.id});
67+
68+
if (!foundUser) {
69+
return exits.badRequest('There is no user with that ID.');
70+
}
71+
72+
if (foundUser.deletedAt !== null) {
73+
return exits.badRequest('This user has been deleted, and can not be edited until reactivated.');
74+
}
75+
76+
if (inputs.setPassword) {
77+
isPasswordValid = await sails.helpers.isPasswordValid.with({
78+
password: inputs.password,
79+
user: {firstName: inputs.firstName, lastName: inputs.lastName, email: inputs.email}
80+
});
81+
}
82+
83+
if (isPasswordValid !== true) {
84+
return exits.badRequest(isPasswordValid);
85+
}
86+
87+
let updatedUser = {
88+
firstName: inputs.firstName,
89+
lastName: inputs.lastName,
90+
role: inputs.role,
91+
email: inputs.email
92+
};
93+
94+
if (inputs.setPassword) {
95+
updatedUser.password = inputs.password;
96+
}
97+
98+
const user = await sails.models.user.updateOne({id: inputs.id}).set(updatedUser);
99+
100+
return exits.ok({user});
101+
}
102+
};
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
module.exports = {
2+
friendlyName: 'Get Deleted Users',
3+
4+
description: 'Get paginated list of soft-deleted users',
5+
6+
inputs: {
7+
page: {
8+
description: 'The page number to return',
9+
type: 'number',
10+
defaultsTo: 1,
11+
min: 1
12+
},
13+
14+
limit: {
15+
description: 'The amount of users to return',
16+
type: 'number',
17+
defaultsTo: 25,
18+
min: 1,
19+
max: 500
20+
}
21+
},
22+
23+
exits: {
24+
ok: {
25+
responseType: 'ok'
26+
},
27+
badRequest: {
28+
responseType: 'badRequest'
29+
},
30+
serverError: {
31+
responseType: 'serverError'
32+
}
33+
},
34+
35+
fn: async (inputs, exits) => {
36+
const query = sails.helpers.paginateForQuery.with({
37+
limit: inputs.limit,
38+
page: inputs.page,
39+
where: {
40+
deletedAt: {'!=': null} // get all soft-deleted users
41+
},
42+
sort: 'deletedAt DESC'
43+
});
44+
45+
let out = await sails.helpers.paginateForJson.with({
46+
model: sails.models.user,
47+
objToWrap: {users: []}, // this is the object that will be output to "out", and will contain additional pagination info,
48+
query
49+
});
50+
51+
// We assign the users to the object afterward, so we can run our safety checks.
52+
// Otherwise, if we were to put the users object into "objToWrap", they would be transformed, and the "customToJSON" feature would no longer work, and hashed passwords would leak.
53+
out.users = await sails.models.user.find(_.omit(query, ['page'])).populate('deletedBy');
54+
55+
return exits.ok(out);
56+
}
57+
};

api/controllers/admin/get-users.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = {
1515
description: 'The amount of users to return',
1616
type: 'number',
1717
defaultsTo: 25,
18-
min: 10,
18+
min: 1,
1919
max: 500
2020
}
2121
},
@@ -38,8 +38,6 @@ module.exports = {
3838
page: inputs.page
3939
});
4040

41-
const users = await sails.models.user.find(_.omit(pagination, ['page']));
42-
4341
let out = await sails.helpers.paginateForJson.with({
4442
model: sails.models.user,
4543
query: pagination,
@@ -48,7 +46,7 @@ module.exports = {
4846

4947
// We assign the users to the object afterward, so we can run our safety checks.
5048
// Otherwise, if we were to put the users object into "objToWrap", they would be transformed, and the "customToJSON" feature would no longer work, and hashed passwords would leak.
51-
out.users = users;
49+
out.users = await sails.models.user.find(_.omit(pagination, ['page']));
5250

5351
return exits.ok(out);
5452
}

api/controllers/common/login.js

+5-7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ module.exports = {
3636
}
3737

3838
const badEmailPass = 'Bad email / password combination.';
39+
40+
if (await sails.helpers.isPasswordValid.with({password: inputs.password, skipPwned: true}) !== true) {
41+
return exits.badRequest(badEmailPass);
42+
}
43+
3944
const foundUser = await sails.models.user.findOne({email: inputs.email, deletedAt: null});
4045

4146
if (!foundUser) {
@@ -51,13 +56,6 @@ module.exports = {
5156
id: 'c', // required, auto-generated
5257
user: foundUser.id,
5358
data: {
54-
user: {
55-
id: foundUser.id,
56-
firstName: foundUser.firstName,
57-
lastName: foundUser.lastName,
58-
email: foundUser.email,
59-
role: foundUser.role
60-
},
6159
_csrfSecret: csrf.secret
6260
}
6361
}).fetch();

api/helpers/create-log.js

-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@ module.exports = {
2525

2626
fn: function(inputs, exits){
2727
const user = (inputs.req.session && inputs.req.session.user) ? inputs.req.session.user.id : null,
28-
account = (inputs.req.session && inputs.req.session.account) ? inputs.req.session.account.id : null,
2928
request = (inputs.req.requestId) ? inputs.req.requestId : null;
3029

3130
const newLog = {
3231
data: inputs.data,
3332
user,
34-
account,
3533
request,
3634
description: inputs.description
3735
};

api/helpers/finalize-request-log.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ module.exports = {
3333
fn: async function(inputs, exits) {
3434
if (inputs.req.requestId) {
3535
let out = _.merge({}, inputs.body),
36-
headers = _.merge({}, inputs.res._headers), // copy the object
36+
headers = _.merge({}, inputs.res.getHeaders()), // copy the object
3737
bleep = '*******';
3838

3939
if (!sails.config.logSensitiveData) { // a custom configuration option, for the request logger hook

0 commit comments

Comments
 (0)