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
@ -56,26 +54,44 @@ class FFTCGDB
''', (err) => ''', (err) =>
logger.error err.message if 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 ->
logger.info 'shutting down normally after SIGINT'
.catch ->
logger.info 'error shutting down after SIGINT'
.finally ->
process.exit() 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,25 +69,26 @@ 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: ->
new Promise (resolve) =>
# function to return all active gameKeys # function to return all active gameKeys
activeGameKeys = (set, cursor) => activeGameKeys = (set, cursor) =>
# start iteration # start iteration
@ -96,7 +97,9 @@ class FFTCGSESSION
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]
@ -109,22 +112,36 @@ class FFTCGSESSION
else else
# recursive call (resolve one step deeper) # recursive call (resolve one step deeper)
allGames set, cursor allGames set, cursor
.then (set) => .then (set) ->
resolve set resolve set
activeGameKeys().then (set) => activeGameKeys()
logger.info "game count: #{Array.from(set).length}" .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
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>