Support for multiple games and keys per game, refactor, tweaks
* Update editorconfig for Dockerfile formatting * Update Dockerfile to strip debug symbols * Support pushing multiple highscores and use of Options to support traditional API mode * Support sending and retrieving different highscore lists for a game based on a key * Support retrieving partial highscore lists for client side pagination * Use of vweb_global attribute for better performance (No mutex/shared variable) and immutability * Support for a verification key that is used for checksumming score data with a private key to raise the difficulty for potential cheaters * Add Logger for nicer console output and severity distinction * Use of transactions in database for higher reliability * Additional error checking for improved error recovery and reliability
This commit is contained in:
parent
674aa90c49
commit
dc8c7bd2b6
9 changed files with 342 additions and 174 deletions
|
@ -10,6 +10,10 @@ trim_trailing_whitespace = true
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[{Dockerfile,Dockerfile.*}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
[*.{yml,yaml,hcl,toml}]
|
[*.{yml,yaml,hcl,toml}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
FROM git.snrd.eu/sunred/vlang:alpine AS build
|
FROM thevlang/vlang:alpine AS build
|
||||||
|
|
||||||
WORKDIR /tmp/app
|
WORKDIR /tmp/app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN v -prod -o highscore-server .
|
RUN v -prod -ldflags "-s -w" -o highscore-server .
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
|
@ -10,6 +10,6 @@ RUN apk --no-cache add sqlite-libs
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /tmp/app/highscore-server .
|
COPY --from=build /tmp/app/highscore-server .
|
||||||
COPY --from=build /tmp/app/config.example.toml ./config.toml
|
COPY ./config.example.toml ./config.toml
|
||||||
|
|
||||||
ENTRYPOINT [ "./highscore-server" ]
|
ENTRYPOINT [ "./highscore-server" ]
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
host = ""
|
host = ""
|
||||||
port = 8080
|
port = 8080
|
||||||
token = "asd"
|
token = "asd"
|
||||||
|
valkey = "asd"
|
||||||
redirect = false
|
redirect = false
|
||||||
redirect_url = ""
|
redirect_url = ""
|
||||||
db_path = "./db/app.db"
|
db_path = "./db/app.db"
|
||||||
|
loglevel = "warn"
|
||||||
origins = [
|
origins = [
|
||||||
"*"
|
"*"
|
||||||
]
|
]
|
||||||
|
games = [
|
||||||
|
"mygame"
|
||||||
|
]
|
||||||
|
|
||||||
|
[valid_keys]
|
||||||
|
mygame = ["level1", "level2", "level3"]
|
||||||
|
|
184
src/api.v
Normal file
184
src/api.v
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
module main
|
||||||
|
|
||||||
|
// import profanity
|
||||||
|
import vweb
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import crypto.sha256
|
||||||
|
|
||||||
|
[get; head]
|
||||||
|
pub fn (mut app App) index() vweb.Result {
|
||||||
|
if app.config.redirect {
|
||||||
|
return app.redirect(app.config.redirect_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.not_found()
|
||||||
|
}
|
||||||
|
|
||||||
|
['/api/v2/score/list/:game'; get; head]
|
||||||
|
pub fn (mut app App) score_list(game string) vweb.Result {
|
||||||
|
app.add_cors_headers()
|
||||||
|
|
||||||
|
if !app.auth() {
|
||||||
|
return app.status(Status{401, 'Access token is wrong or missing'})
|
||||||
|
}
|
||||||
|
|
||||||
|
key := app.query['key'] or { default_score_key }
|
||||||
|
offset := (app.query['offset'] or { '0' }).int()
|
||||||
|
limit := (app.query['limit'] or { '-1' }).int()
|
||||||
|
|
||||||
|
scores := app.get_scores_limit(game, key, offset, limit) or {
|
||||||
|
app.logger.error('A database error occurred: ${err.str()}')
|
||||||
|
return app.status(Status{500, 'An error occurred while retrieving score list'})
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.json(scores)
|
||||||
|
}
|
||||||
|
|
||||||
|
['/api/v2/score/submit/:game'; post]
|
||||||
|
pub fn (mut app App) score_submit(game string) vweb.Result {
|
||||||
|
app.add_cors_headers()
|
||||||
|
|
||||||
|
if !app.auth() {
|
||||||
|
return app.status(Status{401, 'Access token is wrong or missing'})
|
||||||
|
}
|
||||||
|
|
||||||
|
body := json.decode(Score, app.req.data) or {
|
||||||
|
app.logger.error('Bad JSON object was retrieved: ${err.str()}')
|
||||||
|
return app.status(Status{400, 'Bad JSON object'})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.score != none && body.scores != none) || (body.score == none && body.scores == none) {
|
||||||
|
return app.status(Status{400, 'Either score field or scores field should be used'})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.valid_key(game, body) {
|
||||||
|
app.logger.error('${body.player} tried to use invalid verification key for game ${game}')
|
||||||
|
return app.status(Status{401, 'Verification key is wrong or missing'})
|
||||||
|
}
|
||||||
|
|
||||||
|
if game !in app.config.games {
|
||||||
|
app.logger.error('${body.player} tried to submit score(s) for invalid game ${game}')
|
||||||
|
return app.status(Status{400, 'Unknown game'})
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.player.trim_space().len == 0 {
|
||||||
|
app.logger.error('Somebody (${body.player}) tried to submit an empty name for game ${game}')
|
||||||
|
return app.status(Status{400, 'Name cannot be empty'})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Bad word filter
|
||||||
|
|
||||||
|
mut scores := []ScoreTable{}
|
||||||
|
|
||||||
|
if score_num := body.score {
|
||||||
|
if score_num <= 0 {
|
||||||
|
app.logger.error('${body.player} tried to submit an illegal score for game ${game}')
|
||||||
|
return app.status(Status{400, 'Illegal score'})
|
||||||
|
}
|
||||||
|
scores << ScoreTable{
|
||||||
|
player: body.player
|
||||||
|
game: game
|
||||||
|
key: default_score_key
|
||||||
|
score: score_num
|
||||||
|
time: time.now().unix_time()
|
||||||
|
}
|
||||||
|
} else if scores_option := body.scores {
|
||||||
|
score_list := scores_option.clone()
|
||||||
|
for key, score in score_list {
|
||||||
|
valid_key := app.config.valid_keys[game] or { [key] }
|
||||||
|
if key !in valid_key {
|
||||||
|
app.logger.error('${body.player} tried to use an invalid key for game ${game}')
|
||||||
|
return app.status(Status{400, 'Unknown key'})
|
||||||
|
}
|
||||||
|
if score <= 0 {
|
||||||
|
app.logger.error('${body.player} tried to submit illegal score for game ${game}')
|
||||||
|
return app.status(Status{400, 'Illegal score for "${key}"'})
|
||||||
|
}
|
||||||
|
scores << ScoreTable{
|
||||||
|
player: body.player
|
||||||
|
game: game
|
||||||
|
key: key
|
||||||
|
score: score
|
||||||
|
time: time.now().unix_time()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.logger.error('Bad JSON object was retrieved (Neither score and scores is an Option)')
|
||||||
|
return app.status(Status{400, 'Bad JSON object'})
|
||||||
|
}
|
||||||
|
|
||||||
|
score_res := app.insert_score_res(scores) or {
|
||||||
|
app.logger.error('A database error occurred: ${err.str()}')
|
||||||
|
return app.status(Status{500, 'An error occurred while inserting score'})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.logger.info('New score(s) submitted by ${body.player} for game ${game}')
|
||||||
|
|
||||||
|
return app.json(score_res)
|
||||||
|
}
|
||||||
|
|
||||||
|
['/api/v2/score/list'; options]
|
||||||
|
pub fn (mut app App) handle_score_list_options() vweb.Result {
|
||||||
|
return app.handle_options()
|
||||||
|
}
|
||||||
|
|
||||||
|
['/api/v2/score/submit'; options]
|
||||||
|
pub fn (mut app App) handle_score_submit_options() vweb.Result {
|
||||||
|
return app.handle_options()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) handle_options() vweb.Result {
|
||||||
|
app.set_status(204, '')
|
||||||
|
app.add_cors_headers()
|
||||||
|
return app.ok('')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) add_cors_headers() {
|
||||||
|
origin := app.get_header('origin')
|
||||||
|
|
||||||
|
// Only return headers if actual cross-origin request
|
||||||
|
if origin.len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
origins := app.config.origins
|
||||||
|
|
||||||
|
default_origin := origins[0] or { '*' }
|
||||||
|
allowed_origin := if origins.any(it == origin) { origin } else { default_origin }
|
||||||
|
|
||||||
|
app.add_header('Access-Control-Allow-Origin', allowed_origin)
|
||||||
|
app.add_header('Access-Control-Allow-Methods', 'OPTIONS, HEAD, GET, POST')
|
||||||
|
app.add_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
|
||||||
|
app.add_header('Access-Control-Max-Age', '86400')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) auth() bool {
|
||||||
|
auth_header := app.get_header('Authorization')
|
||||||
|
token := auth_header.after('Bearer ')
|
||||||
|
|
||||||
|
return token == app.config.token
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) valid_key(game string, score &Score) bool {
|
||||||
|
mut scorestr := ''
|
||||||
|
|
||||||
|
if score_num := score.score {
|
||||||
|
scorestr += score_num.str()
|
||||||
|
} else if scores_option := score.scores {
|
||||||
|
score_list := scores_option.clone()
|
||||||
|
for k, v in score_list {
|
||||||
|
scorestr += k + v.str()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_key := sha256.hexhash(game + score.player + scorestr + app.config.valkey)
|
||||||
|
return score.valkey == valid_key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) status(status &Status) vweb.Result {
|
||||||
|
app.set_status(status.status, '')
|
||||||
|
return app.json(status)
|
||||||
|
}
|
36
src/config.v
36
src/config.v
|
@ -11,32 +11,50 @@ struct Config {
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
token string
|
token string
|
||||||
|
valkey string
|
||||||
redirect bool
|
redirect bool
|
||||||
redirect_url string
|
redirect_url string
|
||||||
db_path string
|
db_path string
|
||||||
|
loglevel string
|
||||||
origins []string
|
origins []string
|
||||||
|
games []string
|
||||||
|
valid_keys map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (config Config) copy() Config {
|
fn load_config() !&Config {
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config() !Config {
|
|
||||||
if !os.is_file(path) {
|
if !os.is_file(path) {
|
||||||
return error('Config file does not exist or is not a file')
|
return error('Config file does not exist or is not a file')
|
||||||
}
|
}
|
||||||
config := toml.parse_file(path) or {
|
config := toml.parse_file(path) or { return error('Parsing error:\n' + err.str()) }
|
||||||
return error('Error while parsing config file:\n' + err.msg())
|
|
||||||
|
token := config.value('token').string()
|
||||||
|
if token.len == 0 {
|
||||||
|
return error('Token cannot be empty')
|
||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
valkey := config.value('valkey').string()
|
||||||
|
if valkey.len == 0 {
|
||||||
|
return error('Verification key cannot be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_keys_toml := config.value('valid_keys').as_map()
|
||||||
|
mut valid_keys := map[string][]string{}
|
||||||
|
for key, arr in valid_keys_toml {
|
||||||
|
valid_keys[key] = arr.array().as_strings()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
host: config.value('host').string()
|
host: config.value('host').string()
|
||||||
port: config.value('port').int()
|
port: config.value('port').int()
|
||||||
token: config.value('token').string()
|
token: token
|
||||||
|
valkey: valkey
|
||||||
redirect: config.value('redirect').bool()
|
redirect: config.value('redirect').bool()
|
||||||
redirect_url: config.value('redirect_url').string()
|
redirect_url: config.value('redirect_url').string()
|
||||||
db_path: config.value('db_path').string()
|
db_path: config.value('db_path').string()
|
||||||
|
loglevel: config.value('loglevel').string()
|
||||||
origins: config.value('origins').array().as_strings()
|
origins: config.value('origins').array().as_strings()
|
||||||
|
games: config.value('games').array().as_strings()
|
||||||
|
valid_keys: valid_keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,66 @@
|
||||||
module main
|
module main
|
||||||
|
|
||||||
[table: 'Score']
|
[table: 'Score']
|
||||||
struct ScoreRes {
|
struct ScoreTable {
|
||||||
mut:
|
mut:
|
||||||
id i64 [primary; sql: serial]
|
id int [primary; sql: serial]
|
||||||
player string [nonull]
|
player string [nonull]
|
||||||
|
game string [nonull]
|
||||||
|
key string [nonull]
|
||||||
score int [nonull]
|
score int [nonull]
|
||||||
time i64 [nonull]
|
time i64 [nonull]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut app App) create_tables() ! {
|
fn (mut app App) create_tables() ! {
|
||||||
sql app.db {
|
sql app.db {
|
||||||
create table ScoreRes
|
create table ScoreTable
|
||||||
}!
|
}!
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut app App) insert_score(score ScoreRes) !ScoreRes {
|
fn (mut app App) insert_score_res(scores []ScoreTable) ![]ScoreRes {
|
||||||
|
mut score_res := []ScoreRes{}
|
||||||
|
app.db.exec('BEGIN')!
|
||||||
|
for _, score in scores {
|
||||||
sql app.db {
|
sql app.db {
|
||||||
insert score into ScoreRes
|
insert score into ScoreTable
|
||||||
}!
|
} or {
|
||||||
|
app.db.exec('ROLLBACK')!
|
||||||
|
return err
|
||||||
|
}
|
||||||
last_id := app.db.last_id() as int
|
last_id := app.db.last_id() as int
|
||||||
return sql app.db {
|
score_tables := sql app.db {
|
||||||
select from ScoreRes where id == last_id
|
select from ScoreTable where id == last_id
|
||||||
}![0] or { ScoreRes{} }
|
} or {
|
||||||
|
app.db.exec('ROLLBACK')!
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
score_res << (score_tables[0] or {
|
||||||
|
app.db.exec('ROLLBACK')!
|
||||||
|
return error('Result returned zero')
|
||||||
|
}).convert_to_res()
|
||||||
|
}
|
||||||
|
app.db.exec('COMMIT')!
|
||||||
|
return score_res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut app App) get_scores() ![]ScoreRes {
|
fn (mut app App) get_scores(game string, key string) ![]ScoreRes {
|
||||||
return sql app.db {
|
score_table := sql app.db {
|
||||||
select from ScoreRes order by score desc limit 1000
|
select from ScoreTable where game == game && key == key order by score desc
|
||||||
}!
|
}!
|
||||||
|
|
||||||
|
return score_table.convert_to_res()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) get_scores_limit(game string, key string, offset int, rows int) ![]ScoreRes {
|
||||||
|
score_table := sql app.db {
|
||||||
|
select from ScoreTable where game == game && key == key order by score desc limit rows offset offset
|
||||||
|
}!
|
||||||
|
|
||||||
|
return score_table.convert_to_res()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut app App) delete_score(score_id int) ! {
|
fn (mut app App) delete_score(score_id int) ! {
|
||||||
sql app.db {
|
sql app.db {
|
||||||
delete from ScoreRes where id == score_id
|
delete from ScoreTable where id == score_id
|
||||||
}!
|
}!
|
||||||
}
|
}
|
||||||
|
|
57
src/main.v
57
src/main.v
|
@ -4,61 +4,64 @@ import vweb
|
||||||
import db.sqlite
|
import db.sqlite
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import log
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
vweb.Context
|
vweb.Context
|
||||||
|
config Config [vweb_global]
|
||||||
mut:
|
mut:
|
||||||
config shared Config
|
logger log.Logger [vweb_global]
|
||||||
is_admin bool
|
is_admin bool
|
||||||
pub mut:
|
pub mut:
|
||||||
db sqlite.DB
|
db sqlite.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
mut app := &App{}
|
config := load_config() or { panic('Could not load config file: ${err.str()}') }
|
||||||
mut app_config := Config{}
|
|
||||||
|
|
||||||
lock app.config {
|
mut logger := &log.Log{
|
||||||
app.config = load_config() or { panic('Could not load config file: ' + err.msg()) }
|
level: log.level_from_tag(config.loglevel.to_upper()) or { log.Level.info }
|
||||||
app_config = app.config.copy()
|
output_target: log.LogTarget.console
|
||||||
}
|
}
|
||||||
|
|
||||||
if !os.exists(app_config.db_path) {
|
log.set_logger(logger)
|
||||||
println('Creating database file at ' + app_config.db_path)
|
|
||||||
mut file := os.create(app_config.db_path) or { panic(err) }
|
if !os.exists(config.db_path) {
|
||||||
|
logger.info('Creating database file at ' + config.db_path)
|
||||||
|
mut file := os.create(config.db_path) or { logger.fatal(err.str()) }
|
||||||
file.close()
|
file.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
app.db = sqlite.connect(app_config.db_path) or {
|
db := sqlite.connect(config.db_path) or { logger.fatal('Database error! ${err.str()}') }
|
||||||
println('Database error!')
|
|
||||||
panic(err)
|
mut app := &App{
|
||||||
|
config: config
|
||||||
|
logger: logger
|
||||||
|
db: db
|
||||||
}
|
}
|
||||||
|
|
||||||
app.create_tables() or {
|
app.create_tables() or { logger.fatal('Could not create database tables! ${err.str()}') }
|
||||||
println('Could not create database tables!')
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mut host := '::'
|
mut host := '0.0.0.0'
|
||||||
if app_config.host.len != 0 {
|
if config.host.len != 0 {
|
||||||
host = app_config.host
|
host = config.host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle graceful shutdown for Docker
|
// Handle graceful shutdown for Docker
|
||||||
os.signal_opt(os.Signal.int, app.shutdown) or { panic(err) }
|
os.signal_opt(os.Signal.int, app.shutdown) or { logger.fatal(err.str()) }
|
||||||
os.signal_opt(os.Signal.term, app.shutdown) or { panic(err) }
|
os.signal_opt(os.Signal.term, app.shutdown) or { logger.fatal(err.str()) }
|
||||||
|
|
||||||
vweb.run_at(app, vweb.RunParams{
|
vweb.run_at(app, vweb.RunParams{
|
||||||
host: host
|
host: host
|
||||||
port: app_config.port
|
port: config.port
|
||||||
family: .ip6
|
family: .ip
|
||||||
}) or { panic(err) }
|
}) or { logger.fatal(err.str()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut app App) shutdown(sig os.Signal) {
|
fn (mut app App) shutdown(sig os.Signal) {
|
||||||
app.db.close() or { panic(err) }
|
app.db.close() or { app.logger.fatal(err.str()) }
|
||||||
println('Shut down database gracefully')
|
app.logger.info('Shut down database gracefully')
|
||||||
println('Exiting...')
|
app.logger.info('Exiting...')
|
||||||
time.sleep(1e+9) // Sleep one second
|
time.sleep(1e+9) // Sleep one second
|
||||||
exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
42
src/types.v
Normal file
42
src/types.v
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
module main
|
||||||
|
|
||||||
|
pub const default_score_key = '_default'
|
||||||
|
|
||||||
|
struct Score {
|
||||||
|
player string
|
||||||
|
score ?int
|
||||||
|
scores ?map[string]int
|
||||||
|
valkey string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScoreRes {
|
||||||
|
id int
|
||||||
|
player string
|
||||||
|
score int
|
||||||
|
time i64
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Status {
|
||||||
|
status int
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (score ScoreTable) convert_to_res() &ScoreRes {
|
||||||
|
return &ScoreRes{
|
||||||
|
id: score.id
|
||||||
|
player: score.player
|
||||||
|
score: score.score
|
||||||
|
time: score.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// More efficient than array.map(it.convert_to_res())?
|
||||||
|
fn (scores []ScoreTable) convert_to_res() []ScoreRes {
|
||||||
|
mut score_res := []ScoreRes{}
|
||||||
|
|
||||||
|
for _, score in scores {
|
||||||
|
score_res << score.convert_to_res()
|
||||||
|
}
|
||||||
|
|
||||||
|
return score_res
|
||||||
|
}
|
121
src/web.v
121
src/web.v
|
@ -1,122 +1,3 @@
|
||||||
module main
|
module main
|
||||||
|
|
||||||
import vweb
|
// TODO: Webinterface using oauth2 with htmx
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
struct Status {
|
|
||||||
status int
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
[get; head]
|
|
||||||
pub fn (mut app App) index() vweb.Result {
|
|
||||||
rlock app.config {
|
|
||||||
if app.config.redirect {
|
|
||||||
return app.redirect(app.config.redirect_url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.not_found()
|
|
||||||
}
|
|
||||||
|
|
||||||
['/api/v1/score/list'; get; head]
|
|
||||||
pub fn (mut app App) score_list() vweb.Result {
|
|
||||||
app.add_cors_headers()
|
|
||||||
|
|
||||||
if !app.auth() {
|
|
||||||
app.set_status(401, '')
|
|
||||||
return app.json(Status{401, 'Access token is wrong or missing'})
|
|
||||||
}
|
|
||||||
|
|
||||||
scores := app.get_scores() or {
|
|
||||||
return app.json(Status{500, 'An error occurred while retrieving score list'})
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.json(scores)
|
|
||||||
}
|
|
||||||
|
|
||||||
['/api/v1/score/submit'; post]
|
|
||||||
pub fn (mut app App) score_submit() vweb.Result {
|
|
||||||
app.add_cors_headers()
|
|
||||||
|
|
||||||
if !app.auth() {
|
|
||||||
app.set_status(401, '')
|
|
||||||
return app.json(Status{401, 'Access token is wrong or missing'})
|
|
||||||
}
|
|
||||||
|
|
||||||
body := json.decode(Score, app.req.data) or {
|
|
||||||
app.set_status(400, '')
|
|
||||||
return app.json(Status{400, 'Bad JSON object'})
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.player.trim_space().len == 0 {
|
|
||||||
app.set_status(400, '')
|
|
||||||
return app.json(Status{400, 'Name cannot be empty'})
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.score <= 0 {
|
|
||||||
app.set_status(400, '')
|
|
||||||
return app.json(Status{400, 'Illegal score'})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Bad word filter
|
|
||||||
|
|
||||||
score := app.insert_score(ScoreRes{
|
|
||||||
player: body.player
|
|
||||||
score: body.score
|
|
||||||
time: time.now().unix_time()
|
|
||||||
}) or { return app.json(Status{500, 'A database error occurred while inserting score'}) }
|
|
||||||
|
|
||||||
return app.json(score)
|
|
||||||
}
|
|
||||||
|
|
||||||
['/api/v1/score/list'; options]
|
|
||||||
pub fn (mut app App) handle_score_list_options() vweb.Result {
|
|
||||||
return app.handle_options()
|
|
||||||
}
|
|
||||||
|
|
||||||
['/api/v1/score/submit'; options]
|
|
||||||
pub fn (mut app App) handle_score_submit_options() vweb.Result {
|
|
||||||
return app.handle_options()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn (mut app App) handle_options() vweb.Result {
|
|
||||||
app.set_status(204, '')
|
|
||||||
app.add_cors_headers()
|
|
||||||
return app.ok('')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn (mut app App) add_cors_headers() {
|
|
||||||
origin := app.get_header('origin')
|
|
||||||
|
|
||||||
// Only return headers if actual cross-origin request
|
|
||||||
if origin.len == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rlock app.config {
|
|
||||||
origins := app.config.origins
|
|
||||||
|
|
||||||
default_origin := origins[0] or { '*' }
|
|
||||||
allowed_origin := if origins.any(it == origin) { origin } else { default_origin }
|
|
||||||
|
|
||||||
app.add_header('Access-Control-Allow-Origin', allowed_origin)
|
|
||||||
}
|
|
||||||
app.add_header('Access-Control-Allow-Methods', 'OPTIONS, HEAD, GET, POST')
|
|
||||||
app.add_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
|
|
||||||
app.add_header('Access-Control-Max-Age', '86400')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn (mut app App) auth() bool {
|
|
||||||
auth_header := app.get_header('Authorization')
|
|
||||||
token := auth_header.after('Bearer ')
|
|
||||||
|
|
||||||
mut config_token := ''
|
|
||||||
|
|
||||||
rlock app.config {
|
|
||||||
config_token = app.config.token
|
|
||||||
}
|
|
||||||
|
|
||||||
return config_token.len != 0 && token == config_token
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue