Big refactor, split client and server. add weapon get route

This commit is contained in:
2023-03-12 21:18:33 +01:00
parent 72385f0c11
commit fee43b1a98
12 changed files with 258 additions and 53 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
POSTGRES_HOST="localhost" POSTGRES_HOST="localhost"
POSTGRES_DATABASE="postgres" POSTGRES_DATABASE="postgres"
POSTGRES_USER="postgres" POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres" POSTGRES_PASSWORD="postgres"
REDIS_USER="default"
REDIS_PASSWORD=""
REDIS_URL="redis://awesome.redis.server:6379"
+25 -20
View File
@@ -1,21 +1,26 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Program", "name": "Launch Server API",
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**" "program": "${workspaceFolder}/src/server.ts",
], "preLaunchTask": "tsc: build - tsconfig.json",
"program": "${workspaceFolder}/src/index.ts", "outFiles": ["${workspaceFolder}/out/**/*.js"]
"preLaunchTask": "tsc: build - tsconfig.json", },
"outFiles": [ {
"${workspaceFolder}/out/**/*.js" "type": "node",
] "request": "launch",
} "name": "Launch Client API",
] "skipFiles": ["<node_internals>/**"],
} "program": "${workspaceFolder}/src/client.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}
]
}
+84 -2
View File
@@ -12,6 +12,7 @@
"express-validator": "^6.15.0", "express-validator": "^6.15.0",
"kysely": "^0.23.4", "kysely": "^0.23.4",
"pg": "^8.9.0", "pg": "^8.9.0",
"redis": "^4.6.5",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
}, },
"devDependencies": { "devDependencies": {
@@ -2553,6 +2554,59 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.6.tgz",
"integrity": "sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA==",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
"integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.25.24", "version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -3705,6 +3759,14 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": { "node_modules/co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -4888,6 +4950,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -7151,6 +7221,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true "dev": true
}, },
"node_modules/redis": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.5.tgz",
"integrity": "sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg==",
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.6",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -8185,8 +8268,7 @@
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"dev": true
}, },
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.1", "version": "17.7.1",
+4 -2
View File
@@ -7,6 +7,7 @@
"express-validator": "^6.15.0", "express-validator": "^6.15.0",
"kysely": "^0.23.4", "kysely": "^0.23.4",
"pg": "^8.9.0", "pg": "^8.9.0",
"redis": "^4.6.5",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
}, },
"devDependencies": { "devDependencies": {
@@ -29,7 +30,8 @@
"main": "out/index.js", "main": "out/index.js",
"scripts": { "scripts": {
"build": "npx tsc", "build": "npx tsc",
"start": "node out/index.js", "startServer": "node out/server.js",
"startClient": "node out/client.js",
"test": "jest" "test": "jest"
} }
} }
+18
View File
@@ -0,0 +1,18 @@
import { createClient } from 'redis'
const client = createClient({
url: process.env.REDIS_URL,
username: process.env.REDIS_USER,
password: process.env.REDIS_PASSWORD
})
client.on('error', (err) => console.log('Redis Client Error', err))
export function cacheReady() {
return client.connect()
}
export default client
//await client.set('key', 'value');
//const value = await client.get('key');
//await client.disconnect();
+24 -18
View File
@@ -1,21 +1,27 @@
import { Router } from 'express' import * as dotenv from 'dotenv'
const router = Router() dotenv.config()
//timeout middleware ? import express from 'express'
router.get('/*', (req, res, next) => { import cors from 'cors'
next() import client from './client/client'
import { dbReady } from './db/db'
import { cacheReady } from './cache/redis'
const app = express()
const port = 3001
app.use(express.json())
app.use(cors())
app.get('/', (req, res) => {
res.send('Hello World!')
}) })
router.get('/weapons/', (req, res, next) => {}) app.use('/v1/client', client)
router.get('/maps/', (req, res, next) => {}) dbReady().then((e) => {
cacheReady().then((e) => {
router.get('/players', (req, res, next) => {}) app.listen(port, '0.0.0.0', () => {
router.get('/players/:playerId', (req, res, next) => {}) console.log(`Example app listening on port ${port}`)
})
router.get('/servers/', (req, res, next) => {}) })
router.get('/servers/:serverId/', (req, res, next) => {}) })
router.get('/servers/:serverId/players', (req, res, next) => {})
router.get('/servers/:serverId/players/:playerId', (req, res, next) => {})
router.get('/servers/:serverId/weapons', (req, res, next) => {})
export default router
+22
View File
@@ -0,0 +1,22 @@
import { Router } from 'express'
import weapons from './weapons'
const router = Router()
//timeout middleware ?
router.get('/*', (req, res, next) => {
next()
})
router.get('/weapons/', (req, res, next) => {})
router.get('/maps/', (req, res, next) => {})
router.get('/players', (req, res, next) => {})
router.get('/players/:playerId', (req, res, next) => {})
router.get('/servers/', (req, res, next) => {})
router.get('/servers/:serverId/', (req, res, next) => {})
router.get('/servers/:serverId/players', (req, res, next) => {})
router.get('/servers/:serverId/players/:playerId', (req, res, next) => {})
router.use('/', weapons)
export default router
+63
View File
@@ -0,0 +1,63 @@
import { Router } from 'express'
import { param, validationResult } from 'express-validator'
import cache from '../cache/redis'
import db from '../db/db'
const router = Router()
const { count, max } = db.fn
router.get(
'/servers/:serverId/weapons',
param('serverId').exists().toInt().isInt(),
async (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
console.error(JSON.stringify(errors))
return res.status(400).json({ errors: errors.array() })
}
if (!req.params) {
res.sendStatus(500)
return
}
const server = Number(req.params.serverId)
await processTopWeapons(server)
const data = await cache.HGETALL(`servers:${server}:weapons`)
delete data.last_entry
res.status(200).send(data)
}
)
async function processTopWeapons(server: number) {
let last_entry =
Number(await cache.HGET(`servers:${server}:weapons`, 'last_entry')) || 0
const newData = await db
.selectFrom(['kill'])
.select([
count<number>('kill.id').as('num_kills'),
'kill.cause_of_death',
max('kill.id').as('last_entry')
])
.where('server', '=', server)
.where('kill.id', '>', last_entry)
.groupBy('cause_of_death')
.execute()
//if no new kills
if (newData.length == 0) {
return
}
last_entry = newData.reduce(
(acc, current) => (current.last_entry > acc ? current.last_entry : acc),
0
)
const promises: Promise<number>[] = []
newData.forEach(({ num_kills, cause_of_death }) => {
promises.push(
cache.HINCRBY(`servers:${server}:weapons`, cause_of_death, num_kills)
)
})
promises.push(
cache.HSET(`servers:${server}:weapons`, 'last_entry', newData[0].last_entry)
)
await Promise.all(promises)
}
export default router
+7 -1
View File
@@ -12,7 +12,13 @@ const db = new Kysely<Database>({
user: process.env.POSTGRES_USER, user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD password: process.env.POSTGRES_PASSWORD
}) })
}) }),
log(event) {
if (event.level === 'query') {
console.log(event.query.sql)
console.log(event.query.parameters)
}
}
}) })
interface RemoveFromKill { interface RemoveFromKill {
+1 -3
View File
@@ -2,7 +2,6 @@ import * as dotenv from 'dotenv'
dotenv.config() dotenv.config()
import express from 'express' import express from 'express'
import cors from 'cors' import cors from 'cors'
import client from './client'
import { dbReady } from './db/db' import { dbReady } from './db/db'
import server from './server/server' import server from './server/server'
@@ -16,8 +15,7 @@ app.get('/', (req, res) => {
res.send('Hello World!') res.send('Hello World!')
}) })
app.use('/v1/', server) app.use('/v1/servers', server)
app.use('/v1/', client)
dbReady().then((e) => { dbReady().then((e) => {
app.listen(port, '0.0.0.0', () => { app.listen(port, '0.0.0.0', () => {
+2 -2
View File
@@ -12,7 +12,7 @@ const hostsCount: { [id: string]: number } = {}
const hostsTimeout: { [id: string]: NodeJS.Timeout } = {} const hostsTimeout: { [id: string]: NodeJS.Timeout } = {}
//Very simple rate limiting. max 2 registers per IP every 5 mins. Maybe 2 is a bit few ? //Very simple rate limiting. max 2 registers per IP every 5 mins. Maybe 2 is a bit few ?
router.post('/servers/register', (req, res, next) => { router.post('/register', (req, res, next) => {
let ip = let ip =
req.header('x-forwarded-for') || req.socket.remoteAddress || 'undefined' req.header('x-forwarded-for') || req.socket.remoteAddress || 'undefined'
if (hostsCount[ip] > 2) { if (hostsCount[ip] > 2) {
@@ -31,7 +31,7 @@ router.post('/servers/register', (req, res, next) => {
}) })
router.post( router.post(
'/servers/register', '/register',
body(['name', 'description']).isString().withMessage('must be strings'), body(['name', 'description']).isString().withMessage('must be strings'),
body('auth_port') body('auth_port')
.toInt() .toInt()
+4 -4
View File
@@ -10,7 +10,7 @@ router.use('/', register)
//auth middleware //auth middleware
router.post( router.post(
'/servers/:serverId/*', '/:serverId/*',
header('authorization') header('authorization')
.exists({ checkFalsy: true }) .exists({ checkFalsy: true })
.withMessage('Missing Authorization Header') .withMessage('Missing Authorization Header')
@@ -37,7 +37,7 @@ router.post(
) )
//Route to check auth //Route to check auth
router.post('/servers/:serverId', (req, res) => { router.post('/:serverId', (req, res) => {
res.sendStatus(200) res.sendStatus(200)
}) })
@@ -45,7 +45,7 @@ const serversCount: { [id: string]: number } = {}
const serversTimeout: { [id: string]: NodeJS.Timeout } = {} const serversTimeout: { [id: string]: NodeJS.Timeout } = {}
//same rate limiting code as register. max 10 kills per server every 1 sec. should be enough. //same rate limiting code as register. max 10 kills per server every 1 sec. should be enough.
router.post('/servers/:serverId/kill', (req, res, next) => { router.post('/:serverId/kill', (req, res, next) => {
let serverId = req.body.serverId || 'undefined' let serverId = req.body.serverId || 'undefined'
if (serversCount[serverId] > 2) { if (serversCount[serverId] > 2) {
return res.status(429).json({ return res.status(429).json({
@@ -62,7 +62,7 @@ router.post('/servers/:serverId/kill', (req, res, next) => {
}) })
router.post( router.post(
'/servers/:serverId/kill', '/:serverId/kill',
body([ body([
'attacker_current_weapon_mods', 'attacker_current_weapon_mods',
'attacker_weapon_1_mods', 'attacker_weapon_1_mods',