Skip to content

Commit cdbc04e

Browse files
committed
Built OTP capabilities. Built session expiration handling. Made session data saving automatic, and work across API tokens and cookie sessions. Moved CSRF secret storage, to utilize Sails' built-in encryption handling. Fixed some README quirks. Changed how API tokens are handled; again for encryption purposes.
1 parent c598b46 commit cdbc04e

Some content is hidden

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

54 files changed

+1154
-306
lines changed

CHANGELOG.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
11
# Changelog
22

3+
## [v4.2.0](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v4.1.1...v4.2.0) (2023-03-19)
4+
### Features
5+
6+
* Built 2FA (2-Factor Authentication) capabilities.
7+
* Added `createdBy` to the [`User`](api/models/User.js) model.
8+
* Built session expiration handling.
9+
* Built password changing modal / API.
10+
* Made session data saving automatic, and work with both sessions / API tokens.
11+
* Fixed some README quirks.
12+
* Updated React links to use their new domain.
13+
* Updated dependencies.
14+
15+
### Breaking Changes
16+
17+
* Moved CSRF secret storage from the `data` column, to its own column, so it can easily be encrypted/decrypted in the [`Session`](api/models/Session.js) model.
18+
* Changed how API tokens are handled. So now, when using an API token, the ID must be given first, then the token, seperated by a colon.<br />Example: `Authorization` header is: `tokenID:apiToken` (or `Bearer tokenID:apiToken`).
19+
* Renamed `sails.helpers.updateCsrf` -> `sails.helpers.updateCsrfAndExpiry` to reflect the session expiry update.
20+
321
## [v4.1.1](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v4.1.0...v4.1.1) (2023-03-14)
422
### Features
523

6-
* Fixed a stupid mistake in `api/controllers/get-users.js`.
24+
* Fixed a stupid mistake in [`api/controllers/admin/get-users.js`](api/controllers/admin/get-users.js).
725
* Updated dependencies.
826

927
## [v4.1.0](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v4.0.1...v4.1.0) (2023-03-13)
28+
1029
### Features
1130

1231
* Alphabetized the scripts in `package.json`.
@@ -48,7 +67,7 @@
4867

4968
### Breaking Changes
5069

51-
* Renamed tests entry point (`hooks.js` -> `startTests.js`).
70+
* Renamed tests entry point (`test/hooks.js` -> `test/startTests.js`).
5271

5372
## [v3.2.1](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v3.2.0...v3.2.1) (2022-11-16)
5473

@@ -119,7 +138,7 @@
119138

120139
### Breaking Changes
121140

122-
* Updated to React v18. See: [the upgrade guide to React 18](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html).
141+
* Updated to React v18. See: [the upgrade guide to React 18](https://react.dev/blog/2022/03/08/react-18-upgrade-guide).
123142
* Updated to React Router DOM v6. See: [the v5 -> v6 migration guide](https://reactrouter.com/docs/en/v6/upgrading/v5). This requires a **MAJOR** overhaul of how routes are handled.
124143
* Moved some controllers into a "common" folder, instead of the "admin" folder (as they could be used outside of admin controls).
125144

README.md

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Travis CI status](https://app.travis-ci.com/neonexus/sails-react-bootstrap-webpack.svg?branch=release)](https://app.travis-ci.com/github/neonexus/sails-react-bootstrap-webpack)
44

55
This is an opinionated base [Sails v1](https://sailsjs.com) application, using [Webpack](https://webpack.js.org) to handle [Bootstrap](https://getbootstrap.com) (using [SASS](https://sass-lang.com))
6-
and [React](https://reactjs.org) builds. It is designed such that, one can build multiple React frontends (an admin panel, and a customer site maybe), that use the same API backend. This allows
6+
and [React](https://react.dev) builds. It is designed such that, one can build multiple React frontends (an admin panel, and a customer site maybe), that use the same API backend. This allows
77
developers to easily share React components across different frontends / applications. Also, because the backend and frontend are in the same repo (and the frontend is compiled before it is handed to
88
the end user), they can share [NPM](http://npmjs.com) libraries, like [Moment.js](https://momentjs.com)
99

@@ -46,6 +46,7 @@ Gitter: [![Join the chat at https://gitter.im/sails-react-bootstrap-webpack/comm
4646
feature inside [`config/bootstrap.js`](config/bootstrap.js). See [schema validation and enforcement](#schema-validation-and-enforcement) for more info.
4747
* New passwords will 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
4848
be forced to choose another. See [PwnedPasswords integration](#pwnedpasswordscom-integration) for more info.
49+
* Google Authenticator-style OTP (One-Time Password) functionality.
4950

5051
## Branch Warning
5152

@@ -58,25 +59,25 @@ the [`releases section`](https://github.com/neonexus/sails-react-bootstrap-webpa
5859

5960
## Current Dependencies
6061

61-
* [Sails](https://sailsjs.com/) **v1**
62-
* [React](https://reactjs.org/) **v18**
63-
* [React Router](https://reactrouter.com/) **v6**
64-
* [Bootstrap](https://getbootstrap.com/) **v5**
65-
* [React-Bootstrap](https://react-bootstrap.github.io/) **v2**
66-
* [Webpack](https://webpack.js.org/) **v5**
62+
* [Sails](https://sailsjs.com) **v1**
63+
* [React](https://react.dev) **v18**
64+
* [React Router](https://reactrouter.com) **v6**
65+
* [Bootstrap](https://getbootstrap.com) **v5**
66+
* [React-Bootstrap](https://react-bootstrap.github.io) **v2**
67+
* [Webpack](https://webpack.js.org) **v5**
6768

6869
See the [`package.json` for more details](package.json).
6970

7071
## How to Use
7172

72-
This repo is not installable via `npm`. Instead, GitHub provides a handy "Use this template" (green) button at the top of this page. That will create a special fork of this repo (so there is a single,
73+
This repo is not installable via `npm`. Instead, GitHub provides a handy "Use this template" (green) button at the top of this page. That will create a special clone of this repo (so there is a single,
7374
init commit, instead of the commit history from this repo).
7475

7576
## Configuration
7677

7778
In the `config` folder, there is the [`local.js.sample`](config/local.js.sample) file, which is meant to be copied to `local.js`. This file (`local.js`, not the sample) is ignored by Git, and intended
7879
for use in local development, NOT remote servers. Generally one would use environment variables for remote server configuration (and this repo is already setup to handle environment variable
79-
configuration for both DEV and PROD). See: [config/env/development.js](config/env/development.js) and [config/env/production.js](config/env/production.js).
80+
configuration for both DEV and PROD). See [Environment Variables](#environment-variables) for more.
8081

8182
### Custom Configuration Options
8283

@@ -94,11 +95,40 @@ option. If the option path is `sails.config.security.checkPwnedPasswords`, then
9495

9596
... to your `config/local.js` to overwrite the option on your local machine only.
9697

97-
| Option Name (`sails.config.`) | Initially Defined In | Default | Description |
98-
|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
99-
| `models.validateOnBootstrap` | [`config/bootstrap.js`](config/bootstrap.js) | `true` | When enabled, and `models.migrate === 'safe'` (aka PRODUCTION), then the SQL schemas of the default datastore will be validated against the model definitions. <br /><br />See [schema validation and enforcement](#schema-validation-and-enforcement) for more info. |
100-
| `security.checkPwnedPasswords` | [`config/security.js`](config/security.js) | `true` | When enabled, [`sails.helpers.isPasswordValid()`](api/helpers/is-password-valid.js) will run it's normal checks, before checking with the PwnedPasswords.com API to verify the password has not been found in a known security breach. If it has, it will consider the password invalid. |
101-
| `security.requestLogger.logSensitiveData` | [`config/security.js`](config/security.js) <br /> [`config/env/development.js`](config/env/development.js) <br /> [`config/env/production.js`](config/env/production.js) | `false` | If enabled, and NOT a PRODUCTION environment, the [request logger](#request-logging) will log sensitive info, such as passwords. <br /><br /> This will ALWAYS be false on PRODUCTION. It is in the PRODUCTION configuration file only as a reminder. |
98+
<table>
99+
<thead>
100+
<tr>
101+
<th>Option Name (<code>sails.config.</code>)</th>
102+
<th>Initially Defined In</th>
103+
<th>Default</th>
104+
<th>Description</th>
105+
</tr>
106+
</thead>
107+
<tbody>
108+
<tr>
109+
<td><code>models.validateOnBootstrap</code></td>
110+
<td><a href="/neonexus/sails-react-bootstrap-webpack/blob/release/config/bootstrap.js"><code>config/bootstrap.js</code></a></td>
111+
<td><code>true</code></td>
112+
<td>When enabled, and <code>models.migrate === 'safe'</code> (aka PRODUCTION), then the SQL schemas of the default datastore will be validated against the model definitions. <br><br>See <a href="#schema-validation-and-enforcement">schema validation and enforcement</a> for more info.</td>
113+
</tr>
114+
<tr>
115+
<td><code>security.checkPwnedPasswords</code></td>
116+
<td><a href="/neonexus/sails-react-bootstrap-webpack/blob/release/config/security.js"><code>config/security.js</code></a></td>
117+
<td><code>true</code></td>
118+
<td>When enabled, <a href="/neonexus/sails-react-bootstrap-webpack/blob/release/api/helpers/is-password-valid.js"><code>sails.helpers.isPasswordValid()</code></a> will run it's normal checks, before checking with the PwnedPasswords.com API to verify the password has not been found in a known security breach. If it has, it will consider the password invalid.</td>
119+
</tr>
120+
<tr>
121+
<td>
122+
<code>security.</code><br/>
123+
<code>requestLogger.</code><br/>
124+
<code>logSensitiveData</code>
125+
</td>
126+
<td><a href="/neonexus/sails-react-bootstrap-webpack/blob/release/config/security.js"><code>config/security.js</code></a> <br> <a href="/neonexus/sails-react-bootstrap-webpack/blob/release/config/env/development.js"><code>config/env/development.js</code></a> <br> <a href="/neonexus/sails-react-bootstrap-webpack/blob/release/config/env/production.js"><code>config/env/production.js</code></a></td>
127+
<td><code>false</code></td>
128+
<td>If enabled, and NOT a PRODUCTION environment, the <a href="#request-logging">request logger</a> will log sensitive info, such as passwords. <br><br> This will ALWAYS be false on PRODUCTION. It is in the PRODUCTION configuration file only as a reminder.</td>
129+
</tr>
130+
</tbody>
131+
</table>
102132

103133
### Want to configure the `X-Powered-By` header?
104134

@@ -322,9 +352,7 @@ middleware: {
322352
'favicon' // default hook to serve favicon
323353
],
324354

325-
prerender
326-
:
327-
require('prerender-node').set('prerenderToken', 'YOUR_TOKEN')
355+
prerender: require('prerender-node').set('prerenderToken', 'YOUR_TOKEN')
328356

329357
}
330358
```
@@ -335,9 +363,9 @@ middleware: {
335363
* [Sails Deployment Tips](https://sailsjs.com/documentation/concepts/deployment)
336364
* [Sails Community Support Options](https://sailsjs.com/support)
337365
* [Sails Professional / Enterprise Options](https://sailsjs.com/enterprise)
338-
* [`react-bootstrap` Documentation](https://react-bootstrap.netlify.app/)
339-
* [Webpack Documentation](https://webpack.js.org/)
340-
* [React Documentation](https://beta.reactjs.org/)
366+
* [`react-bootstrap` Documentation](https://react-bootstrap.netlify.app)
367+
* [Webpack Documentation](https://webpack.js.org)
368+
* [React Documentation](https://react.dev)
341369
* [Bootstrap Documentation](https://getbootstrap.com/docs/5.3/getting-started/introduction/)
342370
* [Simple data fixtures for testing Sails.js (the npm package `fixted`)](https://www.npmjs.com/package/fixted)
343371

api/controllers/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Controllers
2+
3+
Here is where all of our API actions live. A controller in this context is a folder, and an action of the controller is an individual file. Each action is using the new "actions2" style, as opposed to the classic.
4+
5+
See: https://sailsjs.com/documentation/concepts/actions-and-controllers

api/controllers/admin/create-api-token.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ module.exports = {
1414
fn: async (inputs, exits, env) => {
1515
const newToken = await sails.models.apitoken.create({
1616
id: 'c', // required, auto-generated
17-
user: env.req.session.user.id
18-
}).fetch();
17+
user: env.req.session.user.id,
18+
token: sails.helpers.generateToken(),
19+
data: {} // Used to store things that are temporary, or only apply to this session.
20+
}).fetch().decrypt();
1921

2022
return exits.created({
23+
id: newToken.id,
2124
token: newToken.token,
25+
header: 'Bearer ' + newToken.id + ':' + newToken.token,
2226
__skipCSRF: true // this tells our "created" response to ignore the CSRF token update
2327
});
2428
}

api/controllers/admin/create-user.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ module.exports = {
4545
},
4646

4747
exits: {
48-
ok: {
48+
created: {
4949
responseType: 'created'
5050
},
5151
badRequest: {
@@ -56,7 +56,7 @@ module.exports = {
5656
}
5757
},
5858

59-
fn: async (inputs, exits) => {
59+
fn: async (inputs, exits, env) => {
6060
let password = inputs.password;
6161
let isPasswordValid;
6262

@@ -69,7 +69,7 @@ module.exports = {
6969
isPasswordValid = true;
7070
password = sails.helpers.generateToken().substring(0, 42);
7171

72-
// should probably send password somehow; it will be scrubbed in the custom response
72+
// should probably send password somehow; it will be scrubbed in the custom response (would be hashed anyway...)
7373
}
7474

7575
if (isPasswordValid !== true) {
@@ -88,8 +88,9 @@ module.exports = {
8888
lastName: inputs.lastName,
8989
password,
9090
role: inputs.role,
91-
email: inputs.email
92-
}).meta({fetch: true}).exec((err, newUser) => {
91+
email: inputs.email,
92+
createdBy: env.req.session.user.id
93+
}).fetch().exec((err, user) => {
9394
/* istanbul ignore if */
9495
if (err) {
9596
console.error(err);
@@ -98,10 +99,10 @@ module.exports = {
9899
}
99100

100101
/**
101-
* We should probably email the new user their new account info here if the password was generated (!inputs.setPassword)...
102+
* TODO: We should probably email the new user their new account info here if the password was generated (!inputs.setPassword)...
102103
*/
103104

104-
return exits.ok({user: newUser});
105+
return exits.created({user});
105106
});
106107
}
107108
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module.exports = {
2+
friendlyName: 'Change Password',
3+
4+
description: 'Change password of currently logged-in user.',
5+
6+
inputs: {
7+
currentPassword: {
8+
type: 'string',
9+
maxLength: 70,
10+
required: true
11+
},
12+
13+
newPassword: {
14+
type: 'string',
15+
maxLength: 70,
16+
required: true
17+
},
18+
19+
confirmPassword: {
20+
type: 'string',
21+
maxLength: 70,
22+
required: true
23+
}
24+
},
25+
26+
exits: {
27+
ok: {
28+
responseType: 'ok'
29+
},
30+
badRequest: {
31+
responseType: 'badRequest'
32+
}
33+
},
34+
35+
fn: async (inputs, exits, env) => {
36+
if (inputs.newPassword !== inputs.confirmPassword) {
37+
return exits.badRequest('New passwords do not match.');
38+
}
39+
40+
if (inputs.currentPassword === inputs.newPassword) {
41+
return exits.badRequest('New password can\'t be the same as the old password.');
42+
}
43+
44+
const foundUser = await sails.models.user.findOne({id: env.req.session.user.id});
45+
46+
/* istanbul ignore if */
47+
if (!foundUser) {
48+
console.error('Could NOT find user while changing password!');
49+
50+
return exits.serverError('Unknown error occurred. Please contact support.');
51+
}
52+
53+
if (!await sails.models.user.doPasswordsMatch(inputs.currentPassword, foundUser.password)) {
54+
return exits.badRequest('Current password is incorrect.');
55+
}
56+
57+
const isPasswordValid = await sails.helpers.isPasswordValid(inputs.newPassword, false, foundUser);
58+
59+
if (isPasswordValid !== true) {
60+
return exits.badRequest(isPasswordValid);
61+
}
62+
63+
await sails.models.user.update({id: env.req.session.user.id}).set({password: inputs.newPassword});
64+
65+
return exits.ok();
66+
}
67+
};

api/controllers/common/enable-2fa.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const {authenticator} = require('otplib');
2+
const qrcode = require('qrcode');
3+
4+
module.exports = {
5+
friendlyName: 'Enable 2-Factor Authentication',
6+
7+
description: 'Enable 2FA (2-Factor Authentication) for the current user.',
8+
9+
inputs: {},
10+
11+
exits: {
12+
ok: {
13+
responseType: 'ok'
14+
},
15+
badRequest: {
16+
responseType: 'badRequest'
17+
}
18+
},
19+
20+
fn: async (inputs, exits, env) => {
21+
const secret = authenticator.generateSecret();
22+
const image = await qrcode.toDataURL(authenticator.keyuri(env.req.session.user.email, 'My Service', secret));
23+
24+
const foundOTP = await sails.models.otp.findOne({user: env.req.session.user.id});
25+
26+
if (foundOTP) {
27+
if (foundOTP.isEnabled) {
28+
return exits.badRequest('OTP is already enabled.');
29+
}
30+
31+
await sails.models.otp.update(foundOTP.id).set({secret});
32+
33+
return exits.ok({secret, image});
34+
}
35+
36+
await sails.models.otp.create({
37+
id: 'c', // required, auto-generated
38+
user: env.req.session.user.id,
39+
secret: secret
40+
});
41+
42+
return exits.ok({secret, image});
43+
}
44+
};

0 commit comments

Comments
 (0)