Skip to content

Commit 66a12d5

Browse files
Merge pull request #1 from topcoder-platform/develop
Initial commit
2 parents c48b9da + 840c8d2 commit 66a12d5

37 files changed

+10321
-1
lines changed

README.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,63 @@
1-
# challenges-api-v5
1+
# Topcoder Challenge API
2+
3+
## Dependencies
4+
5+
- nodejs https://nodejs.org/en/ (v10)
6+
- DynamoDB
7+
- Docker, Docker Compose
8+
9+
## Configuration
10+
11+
Configuration for the application is at `config/default.js`.
12+
The following parameters can be set in config files or in env variables:
13+
14+
- LOG_LEVEL: the log level, default is 'debug'
15+
- PORT: the server port, default is 3000
16+
- AUTH_SECRET: The authorization secret used during token verification.
17+
- VALID_ISSUERS: The valid issuer of tokens.
18+
- DYNAMODB.AWS_ACCESS_KEY_ID: The Amazon certificate key to use when connecting. Use local dynamodb you can set fake value
19+
- DYNAMODB.AWS_SECRET_ACCESS_KEY: The Amazon certificate access key to use when connecting. Use local dynamodb you can set fake value
20+
- DYNAMODB.AWS_REGION: The Amazon certificate region to use when connecting. Use local dynamodb you can set fake value
21+
- DYNAMODB.IS_LOCAL: Use Amazon DynamoDB Local or server.
22+
- DYNAMODB.URL: The local url if using Amazon DynamoDB Local
23+
24+
## DynamoDB Setup with Docker
25+
We will use DynamoDB setup on Docker.
26+
27+
Just run `docker-compose up` in local folder
28+
29+
If you have already installed aws-cli in your local machine, you can execute `./local/init-dynamodb.sh` to
30+
create the table. If not you can still create table following `Create Table via awscli in Docker`.
31+
32+
## Create Table via awscli in Docker
33+
1. Make sure DynamoDB are running as per instructions above.
34+
35+
2. Run the following commands
36+
```
37+
docker exec -ti dynamodb sh
38+
```
39+
Next
40+
```
41+
./init-dynamodb.sh
42+
```
43+
44+
3. Now the tables have been created, you can use following command to verify
45+
```
46+
aws dynamodb scan --table-name Challenge --endpoint-url http://localhost:7777
47+
aws dynamodb scan --table-name ChallengeType --endpoint-url http://localhost:7777
48+
aws dynamodb scan --table-name ChallengeSetting --endpoint-url http://localhost:7777
49+
aws dynamodb scan --table-name AuditLog --endpoint-url http://localhost:7777
50+
```
51+
52+
## Local Deployment
53+
54+
- Install dependencies `npm install`
55+
- Run lint `npm run lint`
56+
- Run lint fix `npm run lint:fix`
57+
- Start app `npm start`
58+
- App is running at `http://localhost:3000`
59+
- Clear and init db `npm run init-db`
60+
- Insert test data `npm run test-data`
61+
62+
## Verification
63+
Refer to the verification document `Verification.md`

Verification.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# TopCoder Challenge API Verification
2+
3+
## Postman tests
4+
- import Postman collection and environment in the docs folder to Postman
5+
- note that the Postman tests depend on the test data, so you must first run `npm run init-db` and `npm run test-data` to setup test data
6+
- Just run the whole test cases under provided environment.
7+
8+
## DynamoDB Verification
9+
1. Open a new console and run the command `docker exec -ti dynamodb sh` to use `aws-cli`
10+
11+
2. On the console you opened in step 1, run these following commands you can verify the data that inserted into database during the executing of postman tests
12+
```
13+
aws dynamodb scan --table-name Challenge --endpoint-url http://localhost:7777
14+
aws dynamodb scan --table-name ChallengeType --endpoint-url http://localhost:7777
15+
aws dynamodb scan --table-name ChallengeSetting --endpoint-url http://localhost:7777
16+
aws dynamodb scan --table-name AuditLog --endpoint-url http://localhost:7777
17+
```

app-bootstrap.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* App bootstrap
3+
*/
4+
global.Promise = require('bluebird')
5+
const Joi = require('joi')
6+
7+
Joi.optionalId = () => Joi.string()
8+
Joi.id = () => Joi.optionalId().required()
9+
Joi.page = () => Joi.number().integer().min(1).default(1)
10+
Joi.perPage = () => Joi.number().integer().min(1).max(100).default(20)

app-constants.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* App constants
3+
*/
4+
const UserRoles = {
5+
Admin: 'Administrator',
6+
Copilot: 'Copilot'
7+
}
8+
9+
module.exports = {
10+
UserRoles
11+
}

app-routes.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Configure all routes for express app
3+
*/
4+
5+
const _ = require('lodash')
6+
const config = require('config')
7+
const HttpStatus = require('http-status-codes')
8+
const helper = require('./src/common/helper')
9+
const errors = require('./src/common/errors')
10+
const routes = require('./src/routes')
11+
const authenticator = require('tc-core-library-js').middleware.jwtAuthenticator
12+
13+
/**
14+
* Configure all routes for express app
15+
* @param app the express app
16+
*/
17+
module.exports = (app) => {
18+
// Load all routes
19+
_.each(routes, (verbs, path) => {
20+
_.each(verbs, (def, verb) => {
21+
const controllerPath = `./src/controllers/${def.controller}`
22+
const method = require(controllerPath)[def.method]; // eslint-disable-line
23+
if (!method) {
24+
throw new Error(`${def.method} is undefined`)
25+
}
26+
27+
const actions = []
28+
actions.push((req, res, next) => {
29+
req.signature = `${def.controller}#${def.method}`
30+
next()
31+
})
32+
33+
// add Authenticator check if route has auth
34+
if (def.auth) {
35+
actions.push((req, res, next) => {
36+
authenticator(_.pick(config, ['AUTH_SECRET', 'VALID_ISSUERS']))(req, res, next)
37+
})
38+
39+
actions.push((req, res, next) => {
40+
if (req.authUser.isMachine) {
41+
next(new errors.ForbiddenError('M2M is not supported.'))
42+
} else {
43+
req.authUser.userId = String(req.authUser.userId)
44+
// User
45+
if (req.authUser.roles) {
46+
if (!helper.checkIfExists(def.access, req.authUser.roles)) {
47+
next(new errors.ForbiddenError('You are not allowed to perform this action!'))
48+
} else {
49+
next()
50+
}
51+
} else {
52+
next(new errors.ForbiddenError('You are not authorized to perform this action'))
53+
}
54+
}
55+
})
56+
}
57+
58+
actions.push(method)
59+
app[verb](path, helper.autoWrapExpress(actions))
60+
})
61+
})
62+
63+
// Check if the route is not found or HTTP method is not supported
64+
app.use('*', (req, res) => {
65+
if (routes[req.baseUrl]) {
66+
res.status(HttpStatus.METHOD_NOT_ALLOWED).json({
67+
message: 'The requested HTTP method is not supported.'
68+
})
69+
} else {
70+
res.status(HttpStatus.NOT_FOUND).json({
71+
message: 'The requested resource cannot be found.'
72+
})
73+
}
74+
})
75+
}

app.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* The application entry point
3+
*/
4+
5+
require('./app-bootstrap')
6+
7+
const _ = require('lodash')
8+
const config = require('config')
9+
const express = require('express')
10+
const bodyParser = require('body-parser')
11+
const cors = require('cors')
12+
const HttpStatus = require('http-status-codes')
13+
const logger = require('./src/common/logger')
14+
const interceptor = require('express-interceptor')
15+
16+
// setup express app
17+
const app = express()
18+
19+
app.use(cors())
20+
app.use(bodyParser.json())
21+
app.use(bodyParser.urlencoded({ extended: true }))
22+
app.set('port', config.PORT)
23+
24+
// intercept the response body from jwtAuthenticator
25+
app.use(interceptor((req, res) => {
26+
return {
27+
isInterceptable: () => {
28+
return res.statusCode === 403
29+
},
30+
31+
intercept: (body, send) => {
32+
let obj
33+
try {
34+
obj = JSON.parse(body)
35+
} catch (e) {
36+
logger.error('Invalid response body.')
37+
}
38+
if (obj && obj.result && obj.result.content && obj.result.content.message) {
39+
const ret = { message: obj.result.content.message }
40+
send(JSON.stringify(ret))
41+
} else {
42+
send(body)
43+
}
44+
}
45+
}
46+
}))
47+
48+
// Register routes
49+
require('./app-routes')(app)
50+
51+
// The error handler
52+
// eslint-disable-next-line no-unused-vars
53+
app.use((err, req, res, next) => {
54+
logger.logFullError(err, req.signature || `${req.method} ${req.url}`)
55+
const errorResponse = {}
56+
const status = err.isJoi ? HttpStatus.BAD_REQUEST : (err.httpStatus || HttpStatus.INTERNAL_SERVER_ERROR)
57+
58+
if (_.isArray(err.details)) {
59+
if (err.isJoi) {
60+
_.map(err.details, (e) => {
61+
if (e.message) {
62+
if (_.isUndefined(errorResponse.message)) {
63+
errorResponse.message = e.message
64+
} else {
65+
errorResponse.message += `, ${e.message}`
66+
}
67+
}
68+
})
69+
}
70+
}
71+
if (_.isUndefined(errorResponse.message)) {
72+
if (err.message && status !== HttpStatus.INTERNAL_SERVER_ERROR) {
73+
errorResponse.message = err.message
74+
} else {
75+
errorResponse.message = 'Internal server error'
76+
}
77+
}
78+
79+
res.status(status).json(errorResponse)
80+
})
81+
82+
app.listen(app.get('port'), () => {
83+
logger.info(`Express server listening on port ${app.get('port')}`)
84+
})

config/default.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* The configuration file.
3+
*/
4+
5+
module.exports = {
6+
LOG_LEVEL: process.env.LOG_LEVEL || 'debug',
7+
PORT: process.env.PORT || 3000,
8+
AUTH_SECRET: process.env.AUTH_SECRET || 'mysecret',
9+
VALID_ISSUERS: process.env.VALID_ISSUERS || '["https://api.topcoder-dev.com", "https://api.topcoder.com", "https://topcoder-dev.auth0.com/"]',
10+
DYNAMODB: {
11+
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID || 'FAKE_ACCESS_KEY',
12+
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY || 'FAKE_SECRET_ACCESS_KEY',
13+
AWS_REGION: process.env.AWS_REGION || 'eu-central-1',
14+
IS_LOCAL: process.env.IS_LOCAL || true,
15+
URL: process.env.DYNAMODB_URL || 'http://localhost:7777'
16+
}
17+
}

docs/Challenges_ v5.png

51.3 KB
Loading

0 commit comments

Comments
 (0)