Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.

Commit ceb6835

Browse files
committed
Initial commit
Added prototype leaderboard component, util/scores, and scores migration
1 parent 53e2bc6 commit ceb6835

File tree

10 files changed

+247
-4
lines changed

10 files changed

+247
-4
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ COPY . .
1313

1414
USER node
1515

16-
CMD ["node", "index.js"]
16+
CMD ["node", "index.js"]

migrations/1581292018019_add-users.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
exports.up = function (pgm) {
2-
pgm.createExtension('uuid-ossp')
32
pgm.createTable('users', {
43
id: { type: 'uuid', primaryKey: true },
54
name: { type: 'string', unique: true, notNull: true },
@@ -11,5 +10,4 @@ exports.up = function (pgm) {
1110

1211
exports.down = function (pgm) {
1312
pgm.dropTable('users')
14-
pgm.dropExtension('uuid-ossp')
1513
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
exports.up = function (pgm) {
2+
pgm.createTable('solves', {
3+
id: { type: 'uuid', primaryKey: true },
4+
challengeid: { type: 'string', unique: true, notNull: true },
5+
userid: { type: 'string', unique: true, notNull: true },
6+
})
7+
}
8+
9+
exports.down = function (pgm) {
10+
pgm.dropTable('solves')
11+
}

server/api/auth-login.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { responses } = require('../responses')
2+
3+
module.exports = {
4+
method: 'post',
5+
path: '/auth/login',
6+
requireAuth: true,
7+
schema: {
8+
type: 'object',
9+
properties: {
10+
email: {
11+
type: 'string'
12+
},
13+
password: {
14+
type: 'string'
15+
}
16+
},
17+
required: ['email', 'password']
18+
},
19+
handler: ({ req, uuid }) => {
20+
return [responses.goodLogin, {
21+
uuid
22+
}]
23+
}
24+
}

server/api/index.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,66 @@
11
const express = require('express')
2+
const Ajv = require('ajv')
3+
const { responses, responseList } = require('../responses')
4+
const auth = require('../auth')
5+
26
const router = express.Router()
37

4-
module.exports = router
8+
const routes = [
9+
require('./auth-login'),
10+
require('./leaderboard')
11+
]
12+
13+
const routeValidators = routes.map((route) => new Ajv().compile(route.schema))
14+
15+
routes.forEach((route, i) => {
16+
router[route.method](route.path, async (req, res) => {
17+
const sendResponse = (responseKind, data = null) => {
18+
const response = responseList[responseKind]
19+
if (response === undefined) {
20+
throw new Error(`unknown response ${responseKind}`)
21+
}
22+
res.set('content-type', 'application/json')
23+
res.status(response.status)
24+
res.send(JSON.stringify({
25+
kind: responseKind,
26+
message: response.message,
27+
data
28+
}))
29+
}
30+
31+
let uuid
32+
if (route.requireAuth) {
33+
const authHeader = req.get('authorization')
34+
if (authHeader === undefined || !authHeader.startsWith('Bearer ')) {
35+
sendResponse(responses.badToken)
36+
return
37+
}
38+
uuid = await auth.token.getUserId(authHeader.slice('Bearer '.length))
39+
}
40+
41+
const validator = routeValidators[i]
42+
if (validator !== undefined && !validator(req.body)) {
43+
sendResponse(responses.badBody)
44+
return
45+
}
46+
47+
let response
48+
try {
49+
response = await route.handler({
50+
req,
51+
uuid
52+
})
53+
} catch (e) {
54+
sendResponse(responses.errorInternal)
55+
console.error(e.stack)
56+
}
57+
58+
if (response instanceof Array) {
59+
sendResponse(...response)
60+
} else {
61+
sendResponse(response)
62+
}
63+
})
64+
})
65+
66+
module.exports = router

server/api/leaderboard.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const db = require('../database')
2+
3+
const { responses } = require('../responses')
4+
const challenges = require('../challenges')
5+
const { getScore } = require('../util/scores')
6+
7+
module.exports = {
8+
method: 'get',
9+
path: '/leaderboard',
10+
requireAuth: false,
11+
schema: {},
12+
handler: ({ req, uuid }) => {
13+
14+
let solveAmount = {}
15+
let challengeValues = {}
16+
let userSolves = {}
17+
let userScores = []
18+
const solves = db.leaderboard.getSolves()
19+
const users = db.leaderboard.getUsers()
20+
21+
for(let i = 0; i < solves.length; i++){
22+
// Accumulate in solveAmount
23+
if(solveAmount[!solves[i].challengeid]){
24+
solveAmount[solves[i].challengeid] = 1
25+
}else{
26+
solveAmount[solves[i].challengeid]++
27+
}
28+
// Store which challenges each user solved for later
29+
if(!userSolves[solves[i].userid]){
30+
userSolves[solves[i].userid] = [solves[i].challengeid]
31+
}else{
32+
userSolves[solves[i].userid].push(solves[i].challengeid)
33+
}
34+
}
35+
36+
for(let i = 0; i < challenges.getAllChallenges().length; i++){
37+
const challenge = challenges.getAllChallenges()[i]
38+
if(!solveAmount[challenge.id]){
39+
// There are currently no solves
40+
challengeValues[challenge.id] = getScore('dynamic', challlenge.points.min, challenge.points.max, 0)
41+
}else{
42+
challengeValues[challenge.id] = getScore('dynamic', challlenge.points.min, challenge.points.max, solveAmount[challenge.id])
43+
}
44+
}
45+
46+
for(let i = 0; i < users.length; i++){
47+
if(!userSovles[users[i].userid]){
48+
// The user has not solved anything
49+
userScores.push([users[i].username, 0])
50+
}else{
51+
let currScore = 0
52+
for(let j = 0; j < userSolves[users[i].userid]; j++){
53+
// Add the score for the specific solve loaded fr om the challengeValues array using ids
54+
currScore += challengeValues[userSolves[users[i].userid][j]]
55+
}
56+
userScores.push([users[i].username, currScore])
57+
}
58+
}
59+
60+
const sortedUsers = userScores.sort((a, b) => b[1] - a[1])
61+
62+
return [responses.goodLeaderboard, {
63+
sortedUsers
64+
}]
65+
}
66+
}

server/auth/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
token: require('./token')
3+
}

server/database/leaderboard.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const db = require('./db')
2+
3+
const ret = {
4+
getSolves: () => {
5+
return db.query('SELECT * FROM solves')
6+
.then(res => res.rows)
7+
},
8+
getUsers: () => {
9+
return db.query('SELECT id AS userid, username FROM users')
10+
.then(res => res.rows)
11+
}
12+
}
13+
14+
module.exports = ret

server/responses/index.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const responseList = {
2+
goodLogin: {
3+
status: 200,
4+
message: 'The login was successful.'
5+
},
6+
goodRegister: {
7+
status: 200,
8+
message: 'The account was created.'
9+
},
10+
badEmail: {
11+
status: 400,
12+
message: 'The email address is malformed.'
13+
},
14+
badUnknownEmail: {
15+
status: 404,
16+
message: 'The account does not exist.'
17+
},
18+
badKnownEmail: {
19+
status: 409,
20+
message: 'An account with this email already exists.'
21+
},
22+
badPassword: {
23+
status: 401,
24+
message: 'The password is incorrect.'
25+
},
26+
goodLeaderboard: {
27+
status: 200,
28+
message: 'The retrieval of the leaderbard was successful.'
29+
},
30+
badBody: {
31+
status: 400,
32+
message: 'The request body does not meet requirements.'
33+
},
34+
badToken: {
35+
status: 401,
36+
message: 'The token provided is invalid.'
37+
},
38+
errorInternal: {
39+
status: 500,
40+
message: 'An internal error occurred.'
41+
}
42+
}
43+
44+
const responses = {}
45+
Object.keys(responseList).forEach((kind) => {
46+
responses[kind] = kind
47+
})
48+
49+
module.exports = {
50+
responseList,
51+
responses
52+
}
53+

server/util/scores.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const threshold = 100;
2+
3+
module.exports = {
4+
getScore: (type, minVal, maxVal, solves) => {
5+
if(type === 'static'){
6+
return minVal
7+
}else{
8+
// Assumes dynamic if type is not static
9+
return Math.ceil((minVal - maxVal) / Math.pow(threshold, 2) * Math.pow(solves, 2) + maxVal)
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)