Skip to content

Commit 4a485b4

Browse files
authored
Merge pull request #2 from neonexus/master
Fixed webpack merge issue; started using React context; updates to API handling on frontend; created "sails run create-admin" console function.
2 parents 821e573 + 858241f commit 4a485b4

Some content is hidden

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

41 files changed

+1045
-207
lines changed

.sailsrc

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
{
2-
"generators": {
3-
"modules": {}
4-
},
5-
"_generatedWith": {
6-
"sails": "1.2.3",
7-
"sails-generate": "1.16.13"
8-
},
9-
"hooks": {
10-
"grunt": false
11-
}
2+
"generators": {
3+
"modules": {}
4+
},
5+
"_generatedWith": {
6+
"sails": "1.2.3",
7+
"sails-generate": "1.16.13"
8+
},
9+
"hooks": {
10+
"grunt": false,
11+
"session": false,
12+
"csrf": false
13+
}
1214
}

README.md

+19-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
This is an opinionated base [Sails v1](https://sailsjs.com) application, using Webpack to handle Bootstrap (SASS) and React.
66

7+
# Branch Warning
8+
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.
9+
10+
`master` is **volatile**, likely to change at any time, for any reason; this includes `git push --force` updates.
11+
12+
**FINAL WARNING: DO NOT RELY ON THE MASTER BRANCH!**
13+
714
## Main Features
815

916
+ Automatic (incoming) request logging, via Sails models / hooks.
@@ -31,16 +38,18 @@ This repo is not installable via `npm`. Instead, Github provides a handy "Use th
3138
|npm run coverage | Runs [NYC](https://www.npmjs.com/package/nyc) coverage reporting of the Mocha tests, which generates HTML in `test/coverage`.
3239

3340
### Environment Variables used for remote servers:
34-
| Variable | DEV default | PROD default | Description
35-
|------------|----------------------|-------------------------|----------------------
36-
| ASSETS_URL | "" (empty string) | "" (empty string) | Webpack is configured to modify static asset URLs to point to a CDN, like CloudFront. MUST end with a slash " / ".
37-
| BASE_URL | https://myapi.app | https://myapi.app | The address of the Sails instance.
38-
| DB_HOST | localhost | localhost | The hostname of the datastore.
39-
| DB_USER | root | produser | Username for the datastore.
40-
| DB_PASS | mypass | myprodpassword | Password for the datastore.
41-
| DB_NAME | myapp | proddatabase | The name of the database inside the datastore.
42-
| DB_PORT | 3306 | 3306 | The port number for datastore.
43-
| DB_SSL | false | false | If the datastore requires SSL, set this to "true".
41+
| Variable | DEV default | PROD default | Description
42+
|-----------------------|-------------------|-------------------|----------------------
43+
| ASSETS_URL | "" (empty string) | "" (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.
44+
| BASE_URL | https://myapi.app | https://myapi.app | The address of the Sails instance.
45+
| DB_HOST | localhost | localhost | The hostname of the datastore.
46+
| DB_USER | root | produser | Username for the datastore.
47+
| DB_PASS | mypass | myprodpassword | Password for the datastore.
48+
| DB_NAME | myapp | proddatabase | The name of the database inside the datastore.
49+
| DB_PORT | 3306 | 3306 | The port number for datastore.
50+
| DB_SSL | false | false | If the datastore requires SSL, set this to "true".
51+
| SESSION_SECRET | "" (empty string) | "" (empty string) | This is used to sign cookies, and SHOULD be set, especially on PRODUCTION environments.
52+
| DATA_ENCRYPTION_KEY | "" (empty string) | "" (empty string) | **Currently unused; intended for future use.**
4453

4554
## Request Logging
4655
Automatic incoming request logging, is a 2 part process. First, the [`request-logger` hook](api/hooks/request-logger.js) gathers info from the request, and creates a new [`RequestLog` record](api/models/RequestLog.js), making sure to mask anything that may be sensitive, such as passwords. Then, a custom response gathers information from the response, again, scrubbing sensitive data (using the [customToJSON](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings?identity=#customtojson) feature of Sails models) to prevent leaking of password hashes, or anything else that should never be publicly accessible. The [`keepModelsSafe` helper](api/helpers/keep-models-safe.js) and the custom responses (such as [ok](api/responses/ok.js) or [serverError](api/responses/serverError.js)) are responsible for the final leg of request logs.

api/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# API
2+
3+
This is where all API controllers, custom hooks, helpers, models, policies and responses live.
4+
5+
## Controllers
6+
7+
Controllers handle API requests, and decide how to respond, using the custom responses, which track API requests in our datastore.
8+
9+
## Helpers
10+
11+
Helpers are generic, reusable functions used by multiple controllers (or hooks, policies, etc).
12+
13+
## Hooks
14+
15+
Custom hooks (currently only the 1), are used to tap into different parts of a request. The custom hook setup in this repo, is designed to start recording an API request, while the custom responses finalize the record.
16+
17+
## Models
18+
19+
The data models inform Sails (really Waterline) how to create / modify tables (if `sails.config.models.migrate === 'alter'`), or our custom [schema validation and enforcement](../README.md#schema-validation-and-enforcement) on how it should behave.
20+
21+
## Policies
22+
23+
Policies are simple functions, that determine if a request should continue, or fail. These are configured in [`config/policies.js`](../config/policies.js)
24+
25+
## Responses
26+
27+
These custom responses handle the final leg of request logging. They are responsible for ensuring data isn't leaked, by utilizing the `customToJSON` functionality of models.
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
friendlyName: 'Create API Token',
3+
4+
description: 'Get an API token, which replaces CSRF token usage.',
5+
6+
inputs: {},
7+
8+
exits: {
9+
ok: {
10+
responseType: 'ok'
11+
},
12+
badRequest: {
13+
responseType: 'badRequest'
14+
},
15+
serverError: {
16+
responseType: 'serverError'
17+
}
18+
},
19+
20+
fn: (inputs, exits) => {
21+
22+
return exits.ok();
23+
}
24+
};

api/controllers/admin/create-user.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ module.exports = {
1818

1919
password: {
2020
type: 'string',
21-
required: true
21+
required: true,
22+
maxLength: 70
2223
},
2324

2425
email: {
@@ -31,7 +32,7 @@ module.exports = {
3132

3233
exits: {
3334
ok: {
34-
responseType: 'ok'
35+
responseType: 'created'
3536
},
3637
badRequest: {
3738
responseType: 'badRequest'
@@ -51,7 +52,13 @@ module.exports = {
5152
return exits.badRequest(isPasswordValid);
5253
}
5354

54-
sails.models.user.create({
55+
const foundUser = await User.findOne({email: inputs.email, deletedAt: null});
56+
57+
if (foundUser) {
58+
return exits.badRequest('Email is already in-use.');
59+
}
60+
61+
User.create({
5562
id: 'c', // required, but auto-generated
5663
firstName: inputs.firstName,
5764
lastName: inputs.lastName,

api/controllers/admin/delete-user.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const moment = require('moment-timezone');
2+
3+
module.exports = {
4+
friendlyName: 'Delete User',
5+
6+
description: 'Delete a user, give its ID',
7+
8+
inputs: {
9+
id: {
10+
type: 'string',
11+
required: true,
12+
isUUID: true
13+
}
14+
},
15+
16+
exits: {
17+
ok: {
18+
responseType: 'ok'
19+
},
20+
badRequest: {
21+
responseType: 'badRequest'
22+
},
23+
serverError: {
24+
responseType: 'serverError'
25+
}
26+
},
27+
28+
fn: async (inputs, exits, env) => {
29+
const foundUser = await User.findOne({id: inputs.id, deletedAt: null});
30+
31+
if (!foundUser) {
32+
return exits.badRequest('User does not exist');
33+
}
34+
35+
await Session.destroy({user: foundUser.id}); // force logout any active sessions user might have
36+
37+
await User.update({id: foundUser.id}).set(_.merge({}, foundUser, {
38+
deletedAt: moment.tz(sails.config.datastores.default.timezone).toDate(),
39+
deletedBy: env.req.session.user.id
40+
}));
41+
42+
return exits.ok();
43+
}
44+
};

api/controllers/admin/get-me.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module.exports = {
2+
friendlyName: 'Get me',
3+
4+
description: 'Get the currently logged in user.',
5+
6+
inputs: {},
7+
8+
exits: {
9+
ok: {
10+
responseType: 'ok'
11+
},
12+
badRequest: {
13+
responseType: 'badRequest'
14+
},
15+
serverError: {
16+
responseType: 'serverError'
17+
}
18+
},
19+
20+
fn: async (inputs, exits, env) => {
21+
const foundUser = await User.findOne({id: env.req.session.user.id}); // req.session.user is filled in by the isLoggedIn policy
22+
23+
if (!foundUser) {
24+
// this should not happen
25+
return exits.serverError();
26+
}
27+
28+
return exits.ok({user: foundUser});
29+
}
30+
};

api/controllers/admin/login.js

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
module.exports = {
2+
friendlyName: 'Admin Login',
3+
4+
description: 'Basic authentication for admin panel.',
5+
6+
inputs: {
7+
email: {
8+
type: 'string',
9+
required: true,
10+
isEmail: true,
11+
maxLength: 191 // max length of UTF8-MB4 varchar
12+
},
13+
14+
password: {
15+
type: 'string',
16+
required: true,
17+
maxLength: 70
18+
}
19+
},
20+
21+
exits: {
22+
ok: {
23+
responseType: 'ok'
24+
},
25+
badRequest: {
26+
responseType: 'badRequest'
27+
},
28+
serverError: {
29+
responseType: 'serverError'
30+
}
31+
},
32+
33+
fn: async (inputs, exits, env) => {
34+
if (env.req.signedCookies[sails.config.session.name]) {
35+
return exits.badRequest('Already logged in.');
36+
}
37+
38+
const badEmailPass = 'Bad email / password combination.';
39+
const foundUser = await User.findOne({email: inputs.email, deletedAt: null});
40+
41+
if (!foundUser) {
42+
return exits.badRequest(badEmailPass);
43+
}
44+
45+
if (!await User.doPasswordsMatch(inputs.password, foundUser.password)) {
46+
return exits.badRequest(badEmailPass);
47+
}
48+
49+
const csrf = sails.helpers.generateCsrfToken();
50+
const newSession = await Session.create({
51+
id: 'c', // required, auto-generated
52+
user: foundUser.id,
53+
data: {
54+
user: {
55+
id: foundUser.id,
56+
firstName: foundUser.firstName,
57+
lastName: foundUser.lastName,
58+
email: foundUser.email,
59+
role: foundUser.role
60+
},
61+
_csrfSecret: csrf.secret
62+
}
63+
}).fetch();
64+
65+
return exits.ok({
66+
cookies: [
67+
{
68+
name: sails.config.session.name,
69+
value: newSession.id,
70+
isSession: true
71+
}
72+
],
73+
user: foundUser,
74+
_csrf: csrf.token
75+
});
76+
}
77+
};

api/controllers/admin/logout.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module.exports = {
2+
friendlyName: 'Logout',
3+
4+
description: 'Destroy current user session.',
5+
6+
inputs: {},
7+
8+
exits: {
9+
ok: {
10+
responseType: 'ok'
11+
},
12+
badRequest: {
13+
responseType: 'badRequest'
14+
},
15+
serverError: {
16+
responseType: 'serverError'
17+
}
18+
},
19+
20+
fn: async (inputs, exits, env) => {
21+
const foundSession = await Session.findOne({id: env.req.session.id});
22+
23+
if (!foundSession) {
24+
return exits.ok();
25+
}
26+
27+
await Session.destroy({id: foundSession.id});
28+
29+
return exits.ok({
30+
cookies: [
31+
{
32+
name: sails.config.session.name,
33+
value: null, // setting null will tell sails.helpers.setCookies to remove it
34+
isSession: true
35+
}
36+
]
37+
});
38+
}
39+
};

api/helpers/generate-csrf-token.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const Tokens = require('csrf');
2+
3+
module.exports = {
4+
sync: true,
5+
6+
friendlyName: 'Generate CSRF token',
7+
8+
description: 'Generate a CSRF token, and a secret.',
9+
10+
inputs: {
11+
saltLength: {
12+
type: 'number',
13+
defaultsTo: 8
14+
},
15+
16+
secretLength: {
17+
type: 'number',
18+
defaultsTo: 18
19+
}
20+
},
21+
22+
exits: {},
23+
24+
fn: (inputs, exits) => {
25+
const tokens = new Tokens({
26+
saltLength: inputs.saltLength,
27+
secretLength: inputs.secretLength
28+
});
29+
30+
const secret = tokens.secretSync();
31+
32+
return exits.success({
33+
token: tokens.create(secret),
34+
secret
35+
});
36+
}
37+
};

0 commit comments

Comments
 (0)