Compare commits

...

4 commits

Author SHA1 Message Date
f8b73e38c2 minor backend rework 2019-06-04 18:01:32 +02:00
e963576e46 crude games list 2019-06-04 17:00:09 +02:00
b56aedbca6 (lint) 2019-06-04 16:58:44 +02:00
7182121423 eslint setup 2019-06-04 12:13:19 +02:00
12 changed files with 1016 additions and 95 deletions

8
backend/.eslintrc.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
"extends": ["plugin:@fellow/coffee/recommended"],
"plugins": ["@fellow/coffee"],
"rules": {
"@fellow/coffee/indentation": ["error", { "value": 2 }],
"@fellow/coffee/colon-assignment-spacing": "off"
}
};

View file

@ -17,14 +17,12 @@ messages =
class FFTCGDB class FFTCGDB
constructor: (filename, truncate) -> constructor: (filename, truncate) ->
@filename = filename @db = new sqlite3.Database filename, (err) =>
@db = new sqlite3.Database @filename, (err) =>
if err if err
logger.error err.message logger.error err.message
else else
logger.info "OK open '#{@filename}'" logger.info "OK opened '#{filename}'"
@db.run 'PRAGMA foreign_keys = ON;', (err) => @db.run 'PRAGMA foreign_keys = ON;', (err) =>
logger.error err.message if err logger.error err.message if err
@ -33,49 +31,67 @@ class FFTCGDB
@db.run 'DROP TABLE IF EXISTS users;', (err) => @db.run 'DROP TABLE IF EXISTS users;', (err) =>
logger.error err.message if err logger.error err.message if err
@db.run ''' @db.run '''
CREATE TABLE users ( CREATE TABLE users (
user integer PRIMARY KEY, user integer PRIMARY KEY,
login text NOT NULL COLLATE NOCASE, login text NOT NULL COLLATE NOCASE,
pwdhash text NOT NULL, pwdhash text NOT NULL,
settings text, settings text,
UNIQUE(login) UNIQUE(login)
); );
''', (err) => ''', (err) =>
logger.error err.message if err
@db.run 'DROP TABLE IF EXISTS decks;', (err) =>
logger.error err.message if err logger.error err.message if err
@db.run '''
@db.run 'DROP TABLE IF EXISTS decks;', (err) => CREATE TABLE decks (
deck integer PRIMARY KEY,
user integer NOT NULL,
json text,
FOREIGN KEY (user) REFERENCES users (user)
ON DELETE CASCADE
);
''', (err) =>
logger.error err.message if err logger.error err.message if err
@db.run '''
CREATE TABLE decks (
deck integer PRIMARY KEY,
user integer NOT NULL,
json text,
FOREIGN KEY (user) REFERENCES users (user)
ON DELETE CASCADE
);
''', (err) =>
logger.error err.message if err
@db.run '''INSERT INTO users VALUES(1,'jmm','$2b$13$jgDdHHDWqq1RV6PXxf7aOO6AbxqY6tbxIADyIO0FeXt2BlKQCCMzS',NULL);''' @db.run '''
@db.run '''INSERT INTO decks VALUES(1,1,'{"name":"Antipode Bomb Version 6.0","note":"As Seen In Tournament: The North American Water Cup","cards":[{"count":1,"serial":"1-192"},{"count":2,"serial":"7-132"},{"count":2,"serial":"8-037"},{"count":2,"serial":"8-139"},{"count":1,"serial":"5-036"},{"count":3,"serial":"4-048"},{"count":1,"serial":"2-026"},{"count":3,"serial":"8-043"},{"count":3,"serial":"4-021"},{"count":3,"serial":"3-033"},{"count":1,"serial":"8-014"},{"count":2,"serial":"8-006"},{"count":1,"serial":"8-042"},{"count":1,"serial":"6-027"},{"count":3,"serial":"5-019"},{"count":2,"serial":"2-019"},{"count":2,"serial":"5-032"},{"count":3,"serial":"4-026"},{"count":3,"serial":"1-057"},{"count":1,"serial":"1-048"},{"count":2,"serial":"8-036"},{"count":3,"serial":"8-005"},{"count":3,"serial":"2-005"},{"count":1,"serial":"7-017"},{"count":1,"serial":"8-007"}]}');''' INSERT INTO users VALUES (1,'jmm','$2b$13$jgDdHHDWqq1RV6PXxf7aOO6AbxqY6tbxIADyIO0FeXt2BlKQCCMzS',NULL);
'''
@db.run '''
INSERT INTO decks VALUES (1,1,'{
"name":"Antipode Bomb Version 6.0",
"note":"As Seen In Tournament: The North American Water Cup",
"cards":[
{"count":1,"serial":"1-192"},{"count":2,"serial":"7-132"},{"count":2,"serial":"8-037"},
{"count":2,"serial":"8-139"},{"count":1,"serial":"5-036"},{"count":3,"serial":"4-048"},
{"count":1,"serial":"2-026"},{"count":3,"serial":"8-043"},{"count":3,"serial":"4-021"},
{"count":3,"serial":"3-033"},{"count":1,"serial":"8-014"},{"count":2,"serial":"8-006"},
{"count":1,"serial":"8-042"},{"count":1,"serial":"6-027"},{"count":3,"serial":"5-019"},
{"count":2,"serial":"2-019"},{"count":2,"serial":"5-032"},{"count":3,"serial":"4-026"},
{"count":3,"serial":"1-057"},{"count":1,"serial":"1-048"},{"count":2,"serial":"8-036"},
{"count":3,"serial":"8-005"},{"count":3,"serial":"2-005"},{"count":1,"serial":"7-017"},
{"count":1,"serial":"8-007"}
]
}');
'''
logger.info 'OK clear' logger.info 'OK clear'
close: -> close: ->
logger.info 'shutting down' logger.debug 'shutting down'
new Promise (resolve, reject) => new Promise (resolve, reject) =>
@db.close (err) -> @db.close (err) ->
if err if err
logger.error "FAIL '#{err.message}'" logger.error "FAIL '#{err.message}'"
reject null reject null
else else
logger.warn "OK close '#{@filename}'" logger.info "OK closed"
resolve null resolve null
validate: (login, password) -> validate: (login, password) ->
defined = (value) -> value? and value isnt '' defined = (value) -> value? and value isnt ''
new Promise (resolve, reject) => new Promise (resolve, reject) ->
if (defined login) and (defined password) if (defined login) and (defined password)
# both are defined # both are defined
resolve null resolve null
@ -97,7 +113,10 @@ class FFTCGDB
else else
# try creating row in users table # try creating row in users table
stmt = @db.prepare 'INSERT INTO users (login, pwdhash) VALUES (?, ?)' stmt = @db.prepare '''
INSERT INTO users (login, pwdhash)
VALUES (?, ?)
'''
stmt.run [login, hash], (err) -> stmt.run [login, hash], (err) ->
stmt.finalize() stmt.finalize()
if err if err
@ -119,8 +138,12 @@ class FFTCGDB
@validate login, password @validate login, password
.then => .then =>
# get users table row # get users table row
stmt = @db.prepare 'SELECT * FROM users WHERE login = ?' stmt = @db.prepare '''
stmt.get [login], (err, row) => SELECT *
FROM users
WHERE login = ?
'''
stmt.get [login], (err, row) ->
stmt.finalize() stmt.finalize()
if err if err
logger.warn "login: FAIL db '#{err.code}' for '#{login}'" logger.warn "login: FAIL db '#{err.code}' for '#{login}'"
@ -153,8 +176,12 @@ class FFTCGDB
getUser: (userID) -> getUser: (userID) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# get users table row # get users table row
stmt = @db.prepare 'SELECT * FROM users WHERE user = ?' stmt = @db.prepare '''
stmt.get [userID], (err, row) => SELECT *
FROM users
WHERE user = ?
'''
stmt.get [userID], (err, row) ->
stmt.finalize() stmt.finalize()
if err if err
logger.warn "get: FAIL db '#{err.code}' for '#{userID}'" logger.warn "get: FAIL db '#{err.code}' for '#{userID}'"
@ -173,25 +200,35 @@ class FFTCGDB
addDeck: (userID, deckCards) -> addDeck: (userID, deckCards) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# try creating row in decks table # try creating row in decks table
stmt = @db.prepare 'INSERT INTO decks (user, json) VALUES (?, ?)' stmt = @db.prepare '''
stmt.run [userID, JSON.stringify deckCards], (err) -> INSERT INTO decks (user, json)
VALUES (?, ?)
'''
stmt.run [userID, (JSON.stringify deckCards)], (err) ->
stmt.finalize() stmt.finalize()
if err if err
logger.warn "addDeck: FAIL db '#{err.code}' for '#{userID}'" logger.warn "addDeck: FAIL db '#{err.code}' for '#{userID}'"
reject messages.db reject messages.db
else else
# eslint-disable-next-line @fellow/coffee/missing-fat-arrows
logger.debug "addDeck: OK '#{@lastID}'" logger.debug "addDeck: OK '#{@lastID}'"
resolve @lastID resolve @lastID
modDeck: (userID, deckID, deckCards) -> modDeck: (userID, deckID, deckCards) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
stmt = @db.prepare 'UPDATE decks SET json = ? WHERE deck = ? AND user = ?' stmt = @db.prepare '''
UPDATE decks
SET json = ?
WHERE deck = ? AND user = ?
'''
stmt.run [(JSON.stringify deckCards), deckID, userID], (err) -> stmt.run [(JSON.stringify deckCards), deckID, userID], (err) ->
stmt.finalize() stmt.finalize()
isUnchanged =
if err if err
logger.warn "modDeck: FAIL db '#{err.code}' for '#{deckID}'" logger.warn "modDeck: FAIL db '#{err.code}' for '#{deckID}'"
reject messages.db reject messages.db
# eslint-disable-next-line
else if @changes == 0 else if @changes == 0
logger.warn "no changes for input (#{userID}, #{deckID}, #{JSON.stringify deckCards})!" logger.warn "no changes for input (#{userID}, #{deckID}, #{JSON.stringify deckCards})!"
reject messages.db reject messages.db
@ -200,7 +237,12 @@ class FFTCGDB
getDecks: (userID) -> getDecks: (userID) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
stmt = @db.prepare 'SELECT decks.deck, decks.json FROM decks INNER JOIN users ON decks.user = users.user WHERE users.user = ?' stmt = @db.prepare '''
SELECT decks.deck, decks.json
FROM decks
INNER JOIN users ON decks.user = users.user
WHERE users.user = ?
'''
stmt.all [userID], (err, rows) -> stmt.all [userID], (err, rows) ->
stmt.finalize() stmt.finalize()
if err if err
@ -212,7 +254,10 @@ class FFTCGDB
delDeck: (userID, deckID) -> delDeck: (userID, deckID) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
stmt = @db.prepare 'DELETE FROM decks WHERE deck = ? AND user = ?' stmt = @db.prepare '''
DELETE FROM decks
WHERE deck = ? AND user = ?
'''
stmt.run [deckID, userID], (err) -> stmt.run [deckID, userID], (err) ->
stmt.finalize() stmt.finalize()
if err if err

View file

@ -8,10 +8,13 @@
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"start": "coffee server.coffee", "start": "coffee server.coffee",
"dev": "nodemon", "dev": "nodemon server.coffee --exec 'yarn lint && yarn start'",
"lint": "eslint $(find . -name node_modules -prune -o \\( -type f -iname '*.coffee' -print \\))",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"devDependencies": { "devDependencies": {
"@fellow/eslint-plugin-coffee": "^0.4.13",
"eslint": "^5.16.0",
"nodemon": "^1.19.0" "nodemon": "^1.19.0"
}, },
"dependencies": { "dependencies": {

View file

@ -0,0 +1,26 @@
logger = (require 'logging').default '/games/list'
# session storage (volatile data)
session = (require '../../session')
module.exports =
url: '/games/list'
method: 'POST'
# schema: (require './info.schema')
handler: (request, reply) ->
session.check request.body.session ? ""
.then (userid) ->
# active session found, get associated user
session.getGames()
.then (games) ->
logger.debug "OK '#{userid}' got games"
reply.send
success: true
games: games
.catch ->
# no session found
logger.info "FAIL '#{request.body.session}' session not found"
reply.send
success: false

View file

@ -28,6 +28,8 @@ fastify.route (require "./routes/#{route}") for route in [
'decks/modify' 'decks/modify'
# delete deck # delete deck
'decks/delete' 'decks/delete'
# list games
'games/list'
] ]
# request logging # request logging
@ -53,7 +55,11 @@ fastify.listen 3001, '0.0.0.0'
logger.error err logger.error err
# Handle termination # Handle termination
process.on 'SIGINT', -> process.on 'SIGUSR2', ->
socket.close() (require './db').close()
logger.info 'shutting down after SIGINT' .then ->
process.exit() logger.info 'shutting down normally after SIGINT'
.catch ->
logger.info 'error shutting down after SIGINT'
.finally ->
process.exit()

View file

@ -13,11 +13,11 @@ EXPIRY =
class FFTCGSESSION class FFTCGSESSION
constructor: -> constructor: ->
@db = redis.createClient @redis = redis.createClient
host: 'redis' host: 'redis'
port: 6379 port: 6379
@db.on 'error', (err) => @redis.on 'error', (err) ->
logger.error err.message logger.error err.message
sessionKey: (digest) -> "session.#{digest}" sessionKey: (digest) -> "session.#{digest}"
@ -31,7 +31,7 @@ class FFTCGSESSION
digest = hmac.digest 'base64' digest = hmac.digest 'base64'
# push (hash, userid) into DB for the configured timespan # push (hash, userid) into DB for the configured timespan
@db.setex (@sessionKey digest), EXPIRY.login * 86400, userid, => @redis.setex (@sessionKey digest), EXPIRY.login * 86400, userid, =>
logger.info "OK '#{@sessionKey digest}' created" logger.info "OK '#{@sessionKey digest}' created"
# return cookie data # return cookie data
resolve resolve
@ -42,7 +42,7 @@ class FFTCGSESSION
destroy: (digest) -> destroy: (digest) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# delete hash immediately # delete hash immediately
@db.del (@sessionKey digest), (err, res) => @redis.del (@sessionKey digest), (err, res) =>
if res == 0 if res == 0
reject null reject null
else else
@ -52,12 +52,12 @@ class FFTCGSESSION
check: (digest) -> check: (digest) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# refresh expiry timer on digest # refresh expiry timer on digest
@db.expire (@sessionKey digest), EXPIRY.login * 86400, (err, res) => @redis.expire (@sessionKey digest), EXPIRY.login * 86400, (err, res) =>
if res == 0 if res == 0
reject null reject null
else else
@db.get (@sessionKey digest), (err, res) => @redis.get (@sessionKey digest), (err, res) =>
logger.debug "OK '#{@sessionKey digest}' resumed" logger.debug "OK '#{@sessionKey digest}' resumed"
resolve res resolve res
@ -69,62 +69,79 @@ class FFTCGSESSION
digest = hmac.digest 'base64' digest = hmac.digest 'base64'
# insert game key # insert game key
@db.hsetnx (@gameKey digest), 'owner', userid, (err, res) => @redis.hsetnx (@gameKey digest), 'owner', userid, (err, res) =>
if res == 0 if res == 0
@db.del (@gameKey digest) @redis.del (@gameKey digest)
reject null reject null
else else
@db.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) => @redis.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) =>
if res == 0 if res == 0
@db.del (@gameKey digest) @redis.del (@gameKey digest)
reject null reject null
else else
# add game to active set # add game to active set
@db.sadd (@gameKey 'active'), (@gameKey digest), (err, res) => @redis.sadd (@gameKey 'active'), (@gameKey digest), (err, res) =>
# return game ID # return game ID
logger.info "OK '#{@gameKey digest}' created" logger.info "OK '#{@gameKey digest}' created"
resolve digest resolve digest
getGames: -> getGames: ->
# function to return all active gameKeys new Promise (resolve) =>
activeGameKeys = (set, cursor) => # function to return all active gameKeys
# start iteration activeGameKeys = (set, cursor) =>
set ?= new Set() # start iteration
cursor ?= '0' set ?= new Set()
cursor ?= '0'
return new Promise (resolve, reject) => return new Promise (resolve, reject) =>
# scan "active" gameKey # scan "active" gameKey
@db.sscan (@gameKey 'active'), cursor, 'COUNT', '100', (err, res) => @redis.sscan (@gameKey 'active'), cursor, 'COUNT', '100', (err, res) ->
if err
reject null
# add to results set # add to results set
cursor = res[0] cursor = res[0]
for key in res[1] for key in res[1]
set.add key set.add key
if cursor == '0' if cursor == '0'
# done on cursor = 0 # done on cursor = 0
resolve set
else
# recursive call (resolve one step deeper)
allGames set, cursor
.then (set) =>
resolve set resolve set
else
# recursive call (resolve one step deeper)
allGames set, cursor
.then (set) ->
resolve set
activeGameKeys()
.then (set) =>
activeGames = []
for key in Array.from set
activeGames.push new Promise (resolve) =>
@redis.hget key, 'owner', (err, res) ->
if(err)
resolve null
resolve res
Promise.all activeGames
.then (activeGames) ->
resolve activeGames
activeGameKeys().then (set) =>
logger.info "game count: #{Array.from(set).length}"
joinGame: (digest, userid) -> joinGame: (digest, userid) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# refresh expiry timer on digest # refresh expiry timer on digest
@db.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) => @redis.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) =>
if res == 0 if res == 0
reject null reject null
else else
# insert opponent value # insert opponent value
@db.hsetnx (@gameKey digest), 'opponent', userid, (err, res) => @redis.hsetnx (@gameKey digest), 'opponent', userid, (err, res) =>
if res == 0 if res == 0
reject null reject null
@ -136,13 +153,13 @@ class FFTCGSESSION
updateGame: (digest, state) -> updateGame: (digest, state) ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# refresh expiry timer on digest # refresh expiry timer on digest
@db.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) => @redis.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) =>
if res == 0 if res == 0
reject null reject null
else else
# update state value # update state value
@db.hset (@gameKey digest), 'state', (JSON.stringify state), (err, res) => @redis.hset (@gameKey digest), 'state', (JSON.stringify state), (err, res) =>
if res == 0 if res == 0
reject null reject null

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
{ {
"id": "SdPO5j8y6", "id": "SdPO5j8y6",
"path": "/app", "path": "/app",
"favorite": 0, "favorite": 1,
"type": "vue", "type": "vue",
"name": "frontend", "name": "frontend",
"openDate": 1557166583987, "openDate": 1557166583987,

View file

@ -0,0 +1,36 @@
<template>
<div>
<div v-for="game in games" :key="game">
{{ game }}
</div>
</div>
</template>
<script>
import axios from '@/plugins/axios'
export default {
name: 'GamesList',
props: {
session: String
},
asyncComputed: {
games: {
get() {
return axios
.post('/games/list', {
session: this.session
})
.then(response => {
if (response.data.success) {
return response.data.games
}
})
},
default: []
}
}
}
</script>

View file

@ -4,7 +4,9 @@
<v-icon>view_carousel</v-icon> Decks <v-icon>view_carousel</v-icon> Decks
</v-btn> </v-btn>
<v-btn flat> <v-icon>play_arrow</v-icon> Play </v-btn> <v-btn flat :to="{ name: 'games' }">
<v-icon>play_arrow</v-icon> Play
</v-btn>
<v-btn flat :to="{ name: 'usercp' }"> <v-btn flat :to="{ name: 'usercp' }">
<v-icon>person</v-icon> {{ user.login }} <v-icon>person</v-icon> {{ user.login }}

View file

@ -28,6 +28,12 @@ export default new Router({
component: () => component: () =>
import(/* webpackChunkName: "deckcp" */ './views/DeckCP.vue') import(/* webpackChunkName: "deckcp" */ './views/DeckCP.vue')
}, },
{
path: '/games',
name: 'games',
component: () =>
import(/* webpackChunkName: "games" */ './views/Games.vue')
},
{ {
path: '/game', path: '/game',
name: 'game', name: 'game',

View file

@ -0,0 +1,29 @@
<template>
<v-content>
<HeaderIntern v-model="session" @user="user = $event" />
<v-container v-if="user">
<h2 class="headline">Open Tables</h2>
<GamesList :session="session" />
</v-container>
</v-content>
</template>
<script>
import HeaderIntern from '@/components/HeaderIntern.vue'
import GamesList from '@/components/GamesList.vue'
export default {
name: 'Games',
components: {
HeaderIntern,
GamesList
},
data: () => ({
session: null,
user: null
})
}
</script>