Skip to content

Commit 3103df2

Browse files
Initial code base - contains api only
1 parent 0c9f309 commit 3103df2

23 files changed

+6682
-1
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea
2+
node_modules/
3+
*.log
4+
.DS_Store
5+
.nyc_output
6+
coverage/
7+
.env

README.md

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,150 @@
1-
# member-preferences-service
1+
# Member preferences service
2+
3+
## Prerequisites
4+
5+
- NodeJS (v10)
6+
- AWS DynamoDB
7+
- Java 6+ (if using runnable jar of local DynamoDB)
8+
- Docker, Docker Compose (if using docker of local DynamoDB)
9+
10+
## Configuration
11+
12+
Configuration for the application is at `config/default.js` and `config/production.js`.
13+
The following parameters can be set in config files or in env variables:
14+
15+
- LOG_LEVEL: the log level
16+
- PORT: the server port
17+
- API_VERSION: the API version
18+
- AUTH_SECRET: TC Authentication secret
19+
- VALID_ISSUERS: valid issuers for TC authentication
20+
- AUTH0_URL: AUTH0 URL, used to get M2M token
21+
- AUTH0_PROXY_SERVER_URL: AUTH0 proxy server URL, used to get M2M token
22+
- AUTH0_AUDIENCE: AUTH0 audience, used to get M2M token
23+
- TOKEN_CACHE_TIME: AUTH0 token cache time, used to get M2M token
24+
- AUTH0_CLIENT_ID: AUTH0 client id, used to get M2M token
25+
- AUTH0_CLIENT_SECRET: AUTH0 client secret, used to get M2M token
26+
- AMAZON_AWS_REGION: the Amazon AWS region to access DynamoDB
27+
- AMAZON_AWS_DYNAMODB_READ_CAPACITY_UNITS: AWS DynamoDB read capacity units
28+
- AMAZON_AWS_DYNAMODB_WRITE_CAPACITY_UNITS: AWS DynamoDB write capacity units
29+
- AMAZON_AWS_DYNAMODB_ENDPOINT: DynamoDB endpoint, set it only for local DynamoDB
30+
- AMAZON_AWS_DYNAMODB_PREFERENCE_TABLE: AWS DynamoDB table for user preferences
31+
- MAILCHIMP_API_BASE_URL: Mailchimp API base URL
32+
- MAILCHIMP_API_KEY: Mailchimp API key
33+
- MAILCHIMP_LIST_ID: Mailchimp list/audience id
34+
- SEARCH_USERS_URL: URL to search users
35+
36+
Set the following environment variables so that the app can get TC M2M token (use 'set' insted of 'export' for Windows OS):
37+
38+
- export AUTH0_CLIENT_ID=8QovDh27SrDu1XSs68m21A1NBP8isvOt
39+
- export AUTH0_CLIENT_SECRET=3QVxxu20QnagdH-McWhVz0WfsQzA1F8taDdGDI4XphgpEYZPcMTF4lX3aeOIeCzh
40+
- export AUTH0_URL=https://topcoder-dev.auth0.com/oauth/token
41+
- export AUTH0_AUDIENCE=https://m2m.topcoder-dev.com/
42+
43+
## AWS Setup
44+
45+
1. Download your AWS Credentials from AWS Console. Refer [AWS Documentation](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html)
46+
47+
2. Depending on your Operating System, create AWS credentials file in the path listed below
48+
49+
```bash
50+
Linux, Unix, and macOS users: ~/.aws/credentials
51+
52+
Windows users: C:\Users\{USER_NAME}\.aws\credentials
53+
```
54+
55+
3. credentials file should look like below
56+
57+
```bash
58+
[default]
59+
aws_access_key_id=SOME_ACCESS_KEY_ID
60+
aws_secret_access_key=SOME_SECRET_ACCESS_KEY
61+
```
62+
63+
4. Configure AMAZON_AWS_REGION in config file or environment
64+
65+
## Local DynamoDB setup (Optional)
66+
67+
This page `https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html` provides several ways to deploy local DynamoDB.
68+
69+
If you want to use runnable jar of local DynamoDB:
70+
71+
- see `https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html` for details
72+
- download the local DynamoDB of your region
73+
- extract out the downloaded archive
74+
- ensure Java 6+ is installed
75+
- in the extracted folder, run `java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb`
76+
- configure the AMAZON_AWS_DYNAMODB_ENDPOINT config parameter to `http://localhost:8000` in config file or via environment variable
77+
- follow above section to configure AWS credential, when using local DynamoDB, any fake access key id and secret access key may be used
78+
79+
If you want to use docker of local DynamoDB:
80+
81+
- see `https://hub.docker.com/r/amazon/dynamodb-local` for details
82+
- you may go to `docker` folder, and run `docker-compose up` to start local DynamoDB
83+
- configure the AMAZON_AWS_DYNAMODB_ENDPOINT config parameter to `http://localhost:8000` in config file or via environment variable
84+
- follow above section to configure AWS credential, when using local DynamoDB, any fake access key id and secret access key may be used
85+
86+
## AWS DynamoDB setup
87+
88+
- setup AWS credential and region properly
89+
- do not configure the AMAZON_AWS_DYNAMODB_ENDPOINT config param, then AWS DynamoDB will be used by default
90+
91+
## Mailchimp setup
92+
93+
- register a new account at `https://mailchimp.com`
94+
- after login, see the URL of your admin page, it is like `https://us20.admin.mailchimp.com/`,
95+
here the `us20` is data center, it may be different for different users,
96+
the MAILCHIMP_API_BASE_URL config param is like `https://{data-center}.api.mailchimp.com/3.0`,
97+
so if data center is `us20`, then the MAILCHIMP_API_BASE_URL param should be `https://us20.api.mailchimp.com/3.0`
98+
- click right top profile -> Account -> Extras -> API keys, here you can generate API key, configure it to
99+
MAILCHIMP_API_KEY config param
100+
- click header Audience -> right side Manage Audience -> Settings, scroll down and you can see the audience id,
101+
configure it to MAILCHIMP_LIST_ID config param
102+
- click header Audience -> right side Manage Audience -> Settings -> Manage contacts -> Tags, here you can manage tags,
103+
create 3 tags: `Data Science Newsletter`, `Design Newsletter`, `Dev Newsletter`
104+
105+
- during review, you may simply use the provided Mailchimp config params, and then you don't need to do above setup
106+
107+
## Local Deployment
108+
109+
- Install dependencies `npm install`
110+
- Run lint `npm run lint`
111+
- Run lint fix `npm run lint:fix`
112+
- To delete DynamoDB table if needed `npm run delete-table`
113+
- To create DynamoDB table if needed `npm run create-table`
114+
- Start app `npm start`
115+
- App is running at `http://localhost:3000`
116+
117+
## Verification
118+
119+
- import Postman collection and environment in the docs folder to Postman
120+
- then run the Postman tests
121+
122+
- you may use command `npm run view-table 23124329` to view DynamoDB preferences table record of given user id,
123+
console will show info like below:
124+
125+
```bash
126+
info: Data of id 23124329: {
127+
"id": "23124329",
128+
"email": {
129+
"firstName": "first name",
130+
"lastName": "last name",
131+
"subscriptions": {
132+
"Dev Newsletter": false,
133+
"Data Science Newsletter": true,
134+
"Design Newsletter": true
135+
},
136+
"updatedBy": "user2",
137+
"createdBy": "user1"
138+
},
139+
"objectId": "23124329",
140+
"updatedAt": "2019-04-19T18:24:15.128Z"
141+
}
142+
info: Done!
143+
```
144+
145+
- if you are using AWS DynamoDB, you may also view the table content in AWS web console
146+
147+
## Notes
148+
149+
- swagger is at `docs/swagger.yaml`, you may check it using `http://editor.swagger.io/`
150+
- all JWT tokens provided in Postman environment file and tests are created in `https://jwt.io` with secret `mysecret`

app-bootstrap.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* App bootstrap
3+
*/
4+
global.Promise = require('bluebird')

app-constants.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* App constants
3+
*/
4+
const UserRoles = {
5+
Admin: 'Administrator',
6+
Copilot: 'Copilot',
7+
User: 'Topcoder User'
8+
}
9+
10+
const Scopes = {
11+
ReadPreference: 'read:preferences',
12+
UpdatePreference: 'update:preferences',
13+
AllPreference: 'all:preferences'
14+
}
15+
16+
const PreferenceSubscriptions = ['Dev Newsletter', 'Design Newsletter', 'Data Science Newsletter']
17+
18+
const MailchimpMemberStatuses = {
19+
Subscribed: 'subscribed'
20+
}
21+
22+
const MailchimpTagStatuses = {
23+
Active: 'active',
24+
Inactive: 'inactive'
25+
}
26+
27+
module.exports = {
28+
UserRoles,
29+
Scopes,
30+
PreferenceSubscriptions,
31+
MailchimpMemberStatuses,
32+
MailchimpTagStatuses
33+
}

app-routes.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Configure all routes for express app
3+
*/
4+
const _ = require('lodash')
5+
const config = require('config')
6+
const HttpStatus = require('http-status-codes')
7+
const helper = require('./src/common/helper')
8+
const errors = require('./src/common/errors')
9+
const routes = require('./src/routes')
10+
const authenticator = require('tc-core-library-js').middleware.jwtAuthenticator
11+
12+
/**
13+
* Checks if the source matches the term.
14+
*
15+
* @param {Array} source the array in which to search for the term
16+
* @param {Array | String} term the term to search
17+
*/
18+
function checkIfExists (source, term) {
19+
let terms
20+
21+
if (!_.isArray(source)) {
22+
throw new Error('Source argument should be an array')
23+
}
24+
25+
source = source.map(s => s.toLowerCase())
26+
27+
if (_.isString(term)) {
28+
terms = term.split(' ')
29+
} else if (_.isArray(term)) {
30+
terms = term.map(t => t.toLowerCase())
31+
} else {
32+
throw new Error('Term argument should be either a string or an array')
33+
}
34+
35+
for (let i = 0; i < terms.length; i++) {
36+
if (source.includes(terms[i])) {
37+
return true
38+
}
39+
}
40+
41+
return false
42+
}
43+
44+
/**
45+
* Configure all routes for express app
46+
* @param app the express app
47+
*/
48+
module.exports = (app) => {
49+
// Load all routes
50+
_.each(routes, (verbs, path) => {
51+
_.each(verbs, (def, verb) => {
52+
const controllerPath = `./src/controllers/${def.controller}`
53+
const method = require(controllerPath)[def.method]; // eslint-disable-line
54+
if (!method) {
55+
throw new Error(`${def.method} is undefined`)
56+
}
57+
58+
const actions = []
59+
actions.push((req, res, next) => {
60+
req.signature = `${def.controller}#${def.method}`
61+
next()
62+
})
63+
64+
// Authentication and Authorization
65+
if (def.auth === 'jwt') {
66+
actions.push((req, res, next) => {
67+
authenticator(_.pick(config, ['AUTH_SECRET', 'VALID_ISSUERS']))(req, res, next)
68+
})
69+
70+
actions.push((req, res, next) => {
71+
if (!req.authUser) {
72+
return next(new errors.UnauthorizedError('Action is not allowed for invalid token'))
73+
}
74+
75+
if (req.authUser.roles) {
76+
if (def.access && !checkIfExists(def.access, req.authUser.roles)) {
77+
next(new errors.ForbiddenError('You are not allowed to perform this action!'))
78+
} else {
79+
next()
80+
}
81+
} else if (req.authUser.scopes) {
82+
if (def.scopes && !checkIfExists(def.scopes, req.authUser.scopes)) {
83+
next(new errors.ForbiddenError('You are not allowed to perform this action!'))
84+
} else {
85+
next()
86+
}
87+
} else if ((_.isArray(def.access) && def.access.length > 0) ||
88+
(_.isArray(def.scopes) && def.scopes.length > 0)) {
89+
next(new errors.UnauthorizedError('You are not authorized to perform this action'))
90+
} else {
91+
next()
92+
}
93+
})
94+
}
95+
96+
actions.push(method)
97+
app[verb](`${config.API_VERSION}${path}`, helper.autoWrapExpress(actions))
98+
})
99+
})
100+
101+
// Check if the route is not found or HTTP method is not supported
102+
app.use('*', (req, res) => {
103+
const route = routes[req.baseUrl]
104+
let status
105+
let message
106+
if (route) {
107+
status = HttpStatus.METHOD_NOT_ALLOWED
108+
message = 'The requested HTTP method is not supported.'
109+
} else {
110+
status = HttpStatus.NOT_FOUND
111+
message = 'The requested resource cannot be found.'
112+
}
113+
res.status(status).json({ message })
114+
})
115+
}

app.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* The application entry point
3+
*/
4+
5+
require('./app-bootstrap')
6+
7+
const config = require('config')
8+
const express = require('express')
9+
const bodyParser = require('body-parser')
10+
const _ = require('lodash')
11+
const cors = require('cors')
12+
const logger = require('./src/common/logger')
13+
const HttpStatus = require('http-status-codes')
14+
15+
// setup express app
16+
const app = express()
17+
18+
app.use(cors())
19+
app.use(bodyParser.json())
20+
app.use(bodyParser.urlencoded({ extended: true }))
21+
app.set('port', config.PORT)
22+
23+
// Register routes
24+
require('./app-routes')(app)
25+
26+
// The error handler
27+
// eslint-disable-next-line no-unused-vars
28+
app.use((err, req, res, next) => {
29+
logger.logFullError(err, req.signature || `${req.method} ${req.url}`)
30+
const errorResponse = {}
31+
const status = err.isJoi ? HttpStatus.BAD_REQUEST : (err.httpStatus || HttpStatus.INTERNAL_SERVER_ERROR)
32+
33+
if (_.isArray(err.details)) {
34+
if (err.isJoi) {
35+
_.map(err.details, (e) => {
36+
if (e.message) {
37+
if (_.isUndefined(errorResponse.message)) {
38+
errorResponse.message = e.message
39+
} else {
40+
errorResponse.message += `, ${e.message}`
41+
}
42+
}
43+
})
44+
}
45+
}
46+
if (_.isUndefined(errorResponse.message)) {
47+
if (err.message && status !== HttpStatus.INTERNAL_SERVER_ERROR) {
48+
errorResponse.message = err.message
49+
} else {
50+
errorResponse.message = 'Internal server error'
51+
}
52+
}
53+
54+
res.status(status).json(errorResponse)
55+
})
56+
57+
app.listen(app.get('port'), () => {
58+
logger.info(`Express server listening on port ${app.get('port')}`)
59+
})
60+
61+
module.exports = app

0 commit comments

Comments
 (0)