Compare commits
105 commits
Author | SHA1 | Date | |
---|---|---|---|
f8b73e38c2 | |||
e963576e46 | |||
b56aedbca6 | |||
7182121423 | |||
bfbd1cbf0a | |||
c1f25d008b | |||
dafc8c66fd | |||
7b5bfa4fe1 | |||
e55e659cd0 | |||
63999a9e74 | |||
8d3bddfd5b | |||
063893e3e8 | |||
1bc80b6d89 | |||
3b2ff65054 | |||
348b4b2702 | |||
9b8393c779 | |||
f5425829dd | |||
abf53aea2d | |||
d912b83608 | |||
d9bd46b287 | |||
9616a2fe97 | |||
0475e45275 | |||
38b92829cd | |||
0eeaa08239 | |||
077806411c | |||
f363ead51a | |||
db4677d751 | |||
8aec2a0b6c | |||
3bbb949400 | |||
da69fb8e9d | |||
1354b59cb0 | |||
417c8cdd75 | |||
4f5522ff77 | |||
94a8f69cfd | |||
323d6d2f35 | |||
296ab79bd6 | |||
e028a7c714 | |||
4c32aaf1b5 | |||
310f08dc39 | |||
126f800111 | |||
e372ed1b7c | |||
fc0228bd1f | |||
22dd25b070 | |||
2b304ca407 | |||
48b1a9bbc3 | |||
e52f82477a | |||
0a5df6accc | |||
3f6b91f5ca | |||
c2f077c198 | |||
b60ac2105a | |||
9d1e790a0d | |||
6dcb5a4963 | |||
cbc27f1706 | |||
d4358479f6 | |||
0f7176999b | |||
0d604ef320 | |||
e64f5dbabf | |||
e6bfa62381 | |||
25630bba41 | |||
366339fc9a | |||
0a61d2750b | |||
066073fa54 | |||
eee3ed96ac | |||
4b6b5f339f | |||
86a20f2982 | |||
db53964007 | |||
7c41b94a38 | |||
1505667e1e | |||
f51796902f | |||
ed18dce3ea | |||
84ec601e2a | |||
655f64c193 | |||
3a0e889626 | |||
aac6b5e7b4 | |||
640cfe3b03 | |||
98a774a54d | |||
01049e67fd | |||
e8dbb7b161 | |||
f39f80e64e | |||
939cc8d504 | |||
0afb906c2d | |||
0d96ee66e7 | |||
cf84d4ffec | |||
9fb82dcfaa | |||
a6b9646b87 | |||
4ee00885fe | |||
4dd6236856 | |||
3bd4b71c55 | |||
ac38c6f0c4 | |||
6e70b9a76a | |||
59b5f68b8b | |||
c3fd13e358 | |||
60edcf86b7 | |||
7221dd31af | |||
d0180f3b38 | |||
e78570f298 | |||
d9eb9796b1 | |||
6306a4457d | |||
24484a8ddb | |||
d30f92aa46 | |||
678a30e94d | |||
2ea24ad1da | |||
acd078458f | |||
f278d03519 | |||
ffb7dd8da6 |
102 changed files with 16675 additions and 839 deletions
|
@ -1,19 +0,0 @@
|
||||||
# node stuff
|
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
|
|
||||||
# big files
|
|
||||||
**/*.xcf
|
|
||||||
**/*.bundle.js
|
|
||||||
|
|
||||||
# docker stuff
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
||||||
# container volumes
|
|
||||||
src
|
|
||||||
views
|
|
||||||
public_html
|
|
||||||
inc
|
|
||||||
server.coffee
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
**/*.bundle.js
|
**/node_modules
|
||||||
|
**/fftcg.db
|
||||||
|
**/org.vue.*.json
|
||||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -1,16 +0,0 @@
|
||||||
FROM node:latest
|
|
||||||
|
|
||||||
# some dir for our code
|
|
||||||
WORKDIR /app
|
|
||||||
# container port
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# install dependencies
|
|
||||||
COPY package*.json .
|
|
||||||
RUN yarn
|
|
||||||
|
|
||||||
# copy code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# this is how we start
|
|
||||||
CMD ["yarn", "start"]
|
|
40
Makefile
Normal file
40
Makefile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
COMPOSE:=docker-compose
|
||||||
|
|
||||||
|
ifeq ($(shell which $(COMPOSE) > /dev/null; echo $$?),0)
|
||||||
|
# prefer system's docker-compose
|
||||||
|
else ifeq ($(shell which pipenv > /dev/null; echo $$?),0)
|
||||||
|
# fallback to pipenv
|
||||||
|
COMPOSE:=$(shell pipenv run which $(COMPOSE))
|
||||||
|
$(info found compose as $(COMPOSE))
|
||||||
|
else
|
||||||
|
$(error compose not found, please install)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# need root privileges?
|
||||||
|
PRIVGROUP:=docker
|
||||||
|
ifneq ($(findstring $(PRIVGROUP),$(shell groups)),$(PRIVGROUP))
|
||||||
|
$(info need to run compose as root)
|
||||||
|
COMPOSE:=sudo $(COMPOSE)
|
||||||
|
endif
|
||||||
|
|
||||||
|
CANARY:=node_modules/.yarn-integrity
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: develop
|
||||||
|
|
||||||
|
%/$(CANARY):
|
||||||
|
$(eval image:=$(patsubst %/$(CANARY),%,$@))
|
||||||
|
$(COMPOSE) build --pull $(image)
|
||||||
|
$(COMPOSE) run --rm $(image) yarn install --production=false
|
||||||
|
|
||||||
|
DFILES:=$(wildcard */Dockerfile)
|
||||||
|
IMAGES:=$(patsubst %/Dockerfile,%,$(DFILES))
|
||||||
|
|
||||||
|
.PHONY: develop
|
||||||
|
develop: $(patsubst %,%/$(CANARY),$(IMAGES))
|
||||||
|
$(COMPOSE) up
|
||||||
|
|
||||||
|
.PHONY: production
|
||||||
|
production:
|
||||||
|
$(COMPOSE) -f docker-compose.yml -f docker-compose.prod.yml build
|
||||||
|
$(COMPOSE) -f docker-compose.yml -f docker-compose.prod.yml up -d
|
7
backend/.dockerignore
Normal file
7
backend/.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# node stuff
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# Docker stuff
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
8
backend/.eslintrc.js
Normal file
8
backend/.eslintrc.js
Normal 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"
|
||||||
|
}
|
||||||
|
};
|
22
backend/Dockerfile
Normal file
22
backend/Dockerfile
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
FROM node:lts AS dev
|
||||||
|
ENV NODE_ENV development
|
||||||
|
# some dir for our code
|
||||||
|
WORKDIR /app
|
||||||
|
# mount code
|
||||||
|
VOLUME ["/app"]
|
||||||
|
# this is how we start
|
||||||
|
CMD [ "yarn", "dev" ]
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:lts AS prod
|
||||||
|
ENV NODE_ENV production
|
||||||
|
# some dir for our code
|
||||||
|
WORKDIR /app
|
||||||
|
# install dependencies
|
||||||
|
COPY package*.json yarn*.lock ./
|
||||||
|
RUN yarn --production
|
||||||
|
# copy code
|
||||||
|
COPY . .
|
||||||
|
USER node
|
||||||
|
# this is how we start
|
||||||
|
CMD [ "yarn", "start" ]
|
271
backend/db.coffee
Normal file
271
backend/db.coffee
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
# node libraries
|
||||||
|
bcrypt = (require 'bcrypt')
|
||||||
|
logger = (require 'logging').default 'db'
|
||||||
|
path = (require 'path')
|
||||||
|
sqlite3 = (require 'sqlite3').verbose()
|
||||||
|
|
||||||
|
# bruteforce countermeasure
|
||||||
|
saltRounds = 13
|
||||||
|
|
||||||
|
messages =
|
||||||
|
empty: 'Empty user name or password'
|
||||||
|
hash: 'Failed to process your data, try again later'
|
||||||
|
exists: 'User name is already taken'
|
||||||
|
noexists: 'Wrong user name or password'
|
||||||
|
password: 'Wrong user name or password'
|
||||||
|
db: 'Failed to access the database, try again later'
|
||||||
|
|
||||||
|
class FFTCGDB
|
||||||
|
constructor: (filename, truncate) ->
|
||||||
|
@db = new sqlite3.Database filename, (err) =>
|
||||||
|
if err
|
||||||
|
logger.error err.message
|
||||||
|
|
||||||
|
else
|
||||||
|
logger.info "OK opened '#{filename}'"
|
||||||
|
|
||||||
|
@db.run 'PRAGMA foreign_keys = ON;', (err) =>
|
||||||
|
logger.error err.message if err
|
||||||
|
|
||||||
|
if truncate == true
|
||||||
|
@db.run 'DROP TABLE IF EXISTS users;', (err) =>
|
||||||
|
logger.error err.message if err
|
||||||
|
@db.run '''
|
||||||
|
CREATE TABLE users (
|
||||||
|
user integer PRIMARY KEY,
|
||||||
|
login text NOT NULL COLLATE NOCASE,
|
||||||
|
pwdhash text NOT NULL,
|
||||||
|
settings text,
|
||||||
|
UNIQUE(login)
|
||||||
|
);
|
||||||
|
''', (err) =>
|
||||||
|
logger.error err.message if err
|
||||||
|
|
||||||
|
@db.run 'DROP TABLE IF EXISTS decks;', (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 '''
|
||||||
|
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'
|
||||||
|
|
||||||
|
close: ->
|
||||||
|
logger.debug 'shutting down'
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
@db.close (err) ->
|
||||||
|
if err
|
||||||
|
logger.error "FAIL '#{err.message}'"
|
||||||
|
reject null
|
||||||
|
else
|
||||||
|
logger.info "OK closed"
|
||||||
|
resolve null
|
||||||
|
|
||||||
|
validate: (login, password) ->
|
||||||
|
defined = (value) -> value? and value isnt ''
|
||||||
|
|
||||||
|
new Promise (resolve, reject) ->
|
||||||
|
if (defined login) and (defined password)
|
||||||
|
# both are defined
|
||||||
|
resolve null
|
||||||
|
else
|
||||||
|
# no user name or password given
|
||||||
|
logger.info "validate: FAIL empty '#{login}' or password"
|
||||||
|
reject null
|
||||||
|
|
||||||
|
register: (login, password) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# validate user input
|
||||||
|
@validate login, password
|
||||||
|
.then =>
|
||||||
|
# hash password
|
||||||
|
bcrypt.hash password, saltRounds, (err, hash) =>
|
||||||
|
if err
|
||||||
|
logger.warn "reg: FAIL hash for '#{login}'"
|
||||||
|
reject messages.hash
|
||||||
|
|
||||||
|
else
|
||||||
|
# try creating row in users table
|
||||||
|
stmt = @db.prepare '''
|
||||||
|
INSERT INTO users (login, pwdhash)
|
||||||
|
VALUES (?, ?)
|
||||||
|
'''
|
||||||
|
stmt.run [login, hash], (err) ->
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "reg: FAIL db '#{err.code}' for '#{login}'"
|
||||||
|
# user already exists
|
||||||
|
reject messages.exists
|
||||||
|
|
||||||
|
else
|
||||||
|
logger.info "reg: OK '#{login}'"
|
||||||
|
# registration successful
|
||||||
|
resolve null
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
reject messages.empty
|
||||||
|
|
||||||
|
login: (login, password) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# validate user input
|
||||||
|
@validate login, password
|
||||||
|
.then =>
|
||||||
|
# get users table row
|
||||||
|
stmt = @db.prepare '''
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE login = ?
|
||||||
|
'''
|
||||||
|
stmt.get [login], (err, row) ->
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "login: FAIL db '#{err.code}' for '#{login}'"
|
||||||
|
reject messages.db
|
||||||
|
|
||||||
|
else if not row
|
||||||
|
# hash the password for timing attack reasons
|
||||||
|
bcrypt.hash password, saltRounds, (err, hash) ->
|
||||||
|
logger.debug "login: FAIL nonexistent '#{login}'"
|
||||||
|
reject messages.noexists # user doesnt exist
|
||||||
|
|
||||||
|
else
|
||||||
|
bcrypt.compare password, row.pwdhash, (err, res) ->
|
||||||
|
if err
|
||||||
|
logger.warn "login: FAIL hash for '#{login}'"
|
||||||
|
reject messages.hash
|
||||||
|
|
||||||
|
if res == true
|
||||||
|
logger.debug "login: OK '#{row.login}'"
|
||||||
|
# login successful
|
||||||
|
resolve row.user
|
||||||
|
|
||||||
|
else
|
||||||
|
logger.debug "login: FAIL password for '#{login}'"
|
||||||
|
reject messages.password # login failed
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
reject messages.empty
|
||||||
|
|
||||||
|
getUser: (userID) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# get users table row
|
||||||
|
stmt = @db.prepare '''
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE user = ?
|
||||||
|
'''
|
||||||
|
stmt.get [userID], (err, row) ->
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "get: FAIL db '#{err.code}' for '#{userID}'"
|
||||||
|
reject messages.db
|
||||||
|
|
||||||
|
else if not row
|
||||||
|
logger.debug "get: FAIL nonexistent '#{userID}'"
|
||||||
|
reject messages.noexists # user doesnt exist
|
||||||
|
|
||||||
|
else
|
||||||
|
resolve
|
||||||
|
user: row.user
|
||||||
|
login: row.login
|
||||||
|
settings: row.settings
|
||||||
|
|
||||||
|
addDeck: (userID, deckCards) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# try creating row in decks table
|
||||||
|
stmt = @db.prepare '''
|
||||||
|
INSERT INTO decks (user, json)
|
||||||
|
VALUES (?, ?)
|
||||||
|
'''
|
||||||
|
stmt.run [userID, (JSON.stringify deckCards)], (err) ->
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "addDeck: FAIL db '#{err.code}' for '#{userID}'"
|
||||||
|
reject messages.db
|
||||||
|
|
||||||
|
else
|
||||||
|
# eslint-disable-next-line @fellow/coffee/missing-fat-arrows
|
||||||
|
logger.debug "addDeck: OK '#{@lastID}'"
|
||||||
|
resolve @lastID
|
||||||
|
|
||||||
|
modDeck: (userID, deckID, deckCards) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
stmt = @db.prepare '''
|
||||||
|
UPDATE decks
|
||||||
|
SET json = ?
|
||||||
|
WHERE deck = ? AND user = ?
|
||||||
|
'''
|
||||||
|
stmt.run [(JSON.stringify deckCards), deckID, userID], (err) ->
|
||||||
|
stmt.finalize()
|
||||||
|
isUnchanged =
|
||||||
|
if err
|
||||||
|
logger.warn "modDeck: FAIL db '#{err.code}' for '#{deckID}'"
|
||||||
|
reject messages.db
|
||||||
|
# eslint-disable-next-line
|
||||||
|
else if @changes == 0
|
||||||
|
logger.warn "no changes for input (#{userID}, #{deckID}, #{JSON.stringify deckCards})!"
|
||||||
|
reject messages.db
|
||||||
|
else
|
||||||
|
resolve deckID
|
||||||
|
|
||||||
|
getDecks: (userID) ->
|
||||||
|
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.all [userID], (err, rows) ->
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "getDecks: FAIL db '#{err.code}' for '#{userID}'"
|
||||||
|
reject messages.db
|
||||||
|
else
|
||||||
|
logger.debug "getDecks: OK '#{userID}'"
|
||||||
|
resolve (id: row.deck, content: JSON.parse row.json for row, i in rows)
|
||||||
|
|
||||||
|
delDeck: (userID, deckID) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
stmt = @db.prepare '''
|
||||||
|
DELETE FROM decks
|
||||||
|
WHERE deck = ? AND user = ?
|
||||||
|
'''
|
||||||
|
stmt.run [deckID, userID], (err) ->
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "delDeck: FAIL db '#{err.code}' for '#{deckID}'"
|
||||||
|
reject messages.db
|
||||||
|
else
|
||||||
|
logger.debug "delDeck: OK '#{deckID}'"
|
||||||
|
resolve deckID
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = new FFTCGDB path.resolve(__dirname, 'fftcg.db'), true
|
30
backend/package.json
Normal file
30
backend/package.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "node-fftcg",
|
||||||
|
"version": "0.0.3",
|
||||||
|
"description": "FFTCG online using Socket.IO and CraftyJS on Node.js on Docker",
|
||||||
|
"author": "JMM <jmm@yavook.de>",
|
||||||
|
"main": "server.coffee",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"start": "coffee server.coffee",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fellow/eslint-plugin-coffee": "^0.4.13",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"nodemon": "^1.19.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^3.0.6",
|
||||||
|
"coffeescript": "^2.4.1",
|
||||||
|
"fastify": "^2.3.0",
|
||||||
|
"fastify-cors": "^2.1.3",
|
||||||
|
"fastify-ws": "^1.0.1",
|
||||||
|
"logging": "^3.2.0",
|
||||||
|
"redis": "^2.8.0",
|
||||||
|
"sqlite3": "^4.0.4"
|
||||||
|
}
|
||||||
|
}
|
34
backend/routes/decks/add.coffee
Normal file
34
backend/routes/decks/add.coffee
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
logger = (require 'logging').default '/decks/add'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/decks/add'
|
||||||
|
method: 'POST'
|
||||||
|
# schema: (require './modify.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (userid) ->
|
||||||
|
# active session found, get associated user
|
||||||
|
fftcgdb.addDeck (userid), (request.body.deckCards)
|
||||||
|
.then (deckID) ->
|
||||||
|
logger.info "OK user '#{userid}' added deck '#{deckID}'"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
deck: deckID
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# couldnt get user details
|
||||||
|
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
# no session found
|
||||||
|
logger.info "FAIL '#{request.body.session}' session not found"
|
||||||
|
reply.send
|
||||||
|
success: false
|
34
backend/routes/decks/delete.coffee
Normal file
34
backend/routes/decks/delete.coffee
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
logger = (require 'logging').default '/decks/delete'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/decks/delete'
|
||||||
|
method: 'POST'
|
||||||
|
# schema: (require './modify.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (userid) ->
|
||||||
|
# active session found, get associated user
|
||||||
|
fftcgdb.delDeck (userid), (request.body.deckID)
|
||||||
|
.then (deckID) ->
|
||||||
|
logger.info "OK user '#{userid}' deleted deck '#{deckID}'"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
deck: deckID
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# couldnt get user details
|
||||||
|
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
# no session found
|
||||||
|
logger.info "FAIL '#{request.body.session}' session not found"
|
||||||
|
reply.send
|
||||||
|
success: false
|
34
backend/routes/decks/list.coffee
Normal file
34
backend/routes/decks/list.coffee
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
logger = (require 'logging').default '/decks/list'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/decks/list'
|
||||||
|
method: 'POST'
|
||||||
|
# schema: (require './info.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (userid) ->
|
||||||
|
# active session found, get associated user
|
||||||
|
fftcgdb.getDecks (userid)
|
||||||
|
.then (decks) ->
|
||||||
|
logger.debug "OK '#{userid}' got decks"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
decks: decks
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# couldnt get user details
|
||||||
|
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
# no session found
|
||||||
|
logger.info "FAIL '#{request.body.session}' session not found"
|
||||||
|
reply.send
|
||||||
|
success: false
|
34
backend/routes/decks/modify.coffee
Normal file
34
backend/routes/decks/modify.coffee
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
logger = (require 'logging').default '/decks/modify'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/decks/modify'
|
||||||
|
method: 'POST'
|
||||||
|
# schema: (require './modify.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (userid) ->
|
||||||
|
# active session found, get associated user
|
||||||
|
fftcgdb.modDeck (userid), (request.body.deckID), (request.body.deckCards)
|
||||||
|
.then (deckID) ->
|
||||||
|
logger.info "OK user '#{userid}' modified deck '#{deckID}'"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
deck: deckID
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# couldnt get user details
|
||||||
|
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
# no session found
|
||||||
|
logger.info "FAIL '#{request.body.session}' session not found"
|
||||||
|
reply.send
|
||||||
|
success: false
|
26
backend/routes/games/list.coffee
Normal file
26
backend/routes/games/list.coffee
Normal 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
|
12
backend/routes/test.coffee
Normal file
12
backend/routes/test.coffee
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports =
|
||||||
|
url: '/test'
|
||||||
|
method: 'POST'
|
||||||
|
handler: (request, reply) ->
|
||||||
|
logger.info 'Cookies', request.cookies
|
||||||
|
logger.info 'Body', request.body
|
||||||
|
logger.info 'Query', request.query
|
||||||
|
logger.info 'Params', request.params
|
||||||
|
|
||||||
|
reply.setCookie 'foo', 'foo'
|
||||||
|
reply.send
|
||||||
|
hello: 'world'
|
34
backend/routes/user/info.coffee
Normal file
34
backend/routes/user/info.coffee
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
logger = (require 'logging').default '/user/info'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/user/info'
|
||||||
|
method: 'POST'
|
||||||
|
schema: (require './info.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (userid) ->
|
||||||
|
# active session found, get associated user
|
||||||
|
fftcgdb.getUser (userid)
|
||||||
|
.then (user) ->
|
||||||
|
logger.debug "OK '#{user.login}' got info"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
user: user
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# couldnt get user details
|
||||||
|
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
# no session found
|
||||||
|
logger.info "FAIL '#{request.body.session}' session not found"
|
||||||
|
reply.send
|
||||||
|
success: false
|
24
backend/routes/user/info.schema.coffee
Normal file
24
backend/routes/user/info.schema.coffee
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
module.exports =
|
||||||
|
body:
|
||||||
|
session: type: 'string'
|
||||||
|
response:
|
||||||
|
200:
|
||||||
|
type: 'object'
|
||||||
|
required: ['success']
|
||||||
|
properties:
|
||||||
|
success: type: 'boolean'
|
||||||
|
user:
|
||||||
|
type: 'object'
|
||||||
|
required: ['user', 'login', 'settings']
|
||||||
|
properties:
|
||||||
|
user: type: 'integer'
|
||||||
|
login: type: 'string'
|
||||||
|
settings: type: 'string'
|
||||||
|
# user is required iff success
|
||||||
|
if:
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
const: true
|
||||||
|
then:
|
||||||
|
required: ['user']
|
||||||
|
else: true
|
41
backend/routes/user/login.coffee
Normal file
41
backend/routes/user/login.coffee
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
logger = (require 'logging').default '/user/login'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/user/login'
|
||||||
|
method: 'POST'
|
||||||
|
schema: (require './user.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
new Promise (resolve) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (user) ->
|
||||||
|
# active session found
|
||||||
|
logger.debug "OK '#{user.login}' resumed session '#{request.body.session}'"
|
||||||
|
resolve request.body.session
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
fftcgdb.login request.body.login, request.body.password
|
||||||
|
.then (user) ->
|
||||||
|
# login successful: start new session
|
||||||
|
logger.info "OK '#{request.body.login}'"
|
||||||
|
session.start user
|
||||||
|
.then (cookie_data) ->
|
||||||
|
resolve cookie_data
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# login failed
|
||||||
|
logger.info "FAIL '#{request.body.login}: #{err}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
message: err
|
||||||
|
|
||||||
|
.then (cookie_data) ->
|
||||||
|
# login or resume successful
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
message: JSON.stringify cookie_data
|
24
backend/routes/user/logout.coffee
Normal file
24
backend/routes/user/logout.coffee
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
logger = (require 'logging').default '/user/logout'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/user/logout'
|
||||||
|
method: 'POST'
|
||||||
|
schema: (require './logout.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
new Promise (resolve) ->
|
||||||
|
session.destroy request.body.session ? ""
|
||||||
|
.then ->
|
||||||
|
# active session found
|
||||||
|
logger.debug "OK removed session '#{request.body.session}'"
|
||||||
|
resolve null
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
resolve null
|
||||||
|
|
||||||
|
.then ->
|
||||||
|
reply.send
|
||||||
|
success: true
|
11
backend/routes/user/logout.schema.coffee
Normal file
11
backend/routes/user/logout.schema.coffee
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports =
|
||||||
|
body:
|
||||||
|
session: type: 'string'
|
||||||
|
response:
|
||||||
|
200:
|
||||||
|
type: 'object'
|
||||||
|
required: ['success']
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: 'boolean'
|
||||||
|
const: true
|
22
backend/routes/user/register.coffee
Normal file
22
backend/routes/user/register.coffee
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
logger = (require 'logging').default '/user/register'
|
||||||
|
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/user/register'
|
||||||
|
method: 'POST'
|
||||||
|
schema: (require './user.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
fftcgdb.register(request.body.login, request.body.password)
|
||||||
|
.then ->
|
||||||
|
logger.info "OK '#{request.body.login}'"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
logger.debug "FAIL '#{request.body.login}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
message: err
|
21
backend/routes/user/user.schema.coffee
Normal file
21
backend/routes/user/user.schema.coffee
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports =
|
||||||
|
body:
|
||||||
|
session: type: 'string'
|
||||||
|
login: type: 'string'
|
||||||
|
password: type: 'string'
|
||||||
|
|
||||||
|
response:
|
||||||
|
200:
|
||||||
|
type: 'object'
|
||||||
|
required: ['success']
|
||||||
|
properties:
|
||||||
|
success: type: 'boolean'
|
||||||
|
message: type: 'string'
|
||||||
|
# message is required iff not success
|
||||||
|
if:
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
const: false
|
||||||
|
then:
|
||||||
|
required: ['message']
|
||||||
|
else: true
|
65
backend/server.coffee
Normal file
65
backend/server.coffee
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# node libraries
|
||||||
|
# (require 'debug').enable 'FFTCG'
|
||||||
|
logger = (require 'logging').default 'FFTCG'
|
||||||
|
fastify = (require 'fastify')
|
||||||
|
logger: level: 'warn'
|
||||||
|
|
||||||
|
# fastify and plugin framework
|
||||||
|
fastify.register (require 'fastify-ws'), library: 'uws'
|
||||||
|
fastify.register (require 'fastify-cors')
|
||||||
|
|
||||||
|
# API routes
|
||||||
|
fastify.route (require "./routes/#{route}") for route in [
|
||||||
|
# test route
|
||||||
|
'test'
|
||||||
|
# log in user
|
||||||
|
'user/login'
|
||||||
|
# user info
|
||||||
|
'user/info'
|
||||||
|
# log out user
|
||||||
|
'user/logout'
|
||||||
|
# register user
|
||||||
|
'user/register'
|
||||||
|
# list decks
|
||||||
|
'decks/list'
|
||||||
|
# add deck
|
||||||
|
'decks/add'
|
||||||
|
# modify deck
|
||||||
|
'decks/modify'
|
||||||
|
# delete deck
|
||||||
|
'decks/delete'
|
||||||
|
# list games
|
||||||
|
'games/list'
|
||||||
|
]
|
||||||
|
|
||||||
|
# request logging
|
||||||
|
fastify.addHook 'onRequest', (req, res, next) ->
|
||||||
|
logger.debug 'requested', req.url
|
||||||
|
next()
|
||||||
|
|
||||||
|
# finalize loadup
|
||||||
|
fastify.ready()
|
||||||
|
.then ->
|
||||||
|
# create websocket on successful load
|
||||||
|
socket = (require './socket')
|
||||||
|
fastify.ws.on 'connection', socket
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# abort on load failure
|
||||||
|
logger.error err
|
||||||
|
process.exit 1
|
||||||
|
|
||||||
|
# start server
|
||||||
|
fastify.listen 3001, '0.0.0.0'
|
||||||
|
.catch (err) ->
|
||||||
|
logger.error err
|
||||||
|
|
||||||
|
# Handle termination
|
||||||
|
process.on 'SIGUSR2', ->
|
||||||
|
(require './db').close()
|
||||||
|
.then ->
|
||||||
|
logger.info 'shutting down normally after SIGINT'
|
||||||
|
.catch ->
|
||||||
|
logger.info 'error shutting down after SIGINT'
|
||||||
|
.finally ->
|
||||||
|
process.exit()
|
171
backend/session.coffee
Normal file
171
backend/session.coffee
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
# node libraries
|
||||||
|
redis = (require 'redis')
|
||||||
|
crypto = (require 'crypto')
|
||||||
|
logger = (require 'logging').default 'session'
|
||||||
|
|
||||||
|
# expiry times in days
|
||||||
|
EXPIRY =
|
||||||
|
# games expire 1 week after creation
|
||||||
|
game: 7
|
||||||
|
# logins expire 1 month after last action
|
||||||
|
login: 30
|
||||||
|
|
||||||
|
|
||||||
|
class FFTCGSESSION
|
||||||
|
constructor: ->
|
||||||
|
@redis = redis.createClient
|
||||||
|
host: 'redis'
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
@redis.on 'error', (err) ->
|
||||||
|
logger.error err.message
|
||||||
|
|
||||||
|
sessionKey: (digest) -> "session.#{digest}"
|
||||||
|
gameKey: (digest) -> "game.#{digest}"
|
||||||
|
|
||||||
|
start: (userid) ->
|
||||||
|
new Promise (resolve) =>
|
||||||
|
# hash userid
|
||||||
|
hmac = crypto.createHmac 'sha256', Math.random().toString()
|
||||||
|
hmac.update userid.toString()
|
||||||
|
digest = hmac.digest 'base64'
|
||||||
|
|
||||||
|
# push (hash, userid) into DB for the configured timespan
|
||||||
|
@redis.setex (@sessionKey digest), EXPIRY.login * 86400, userid, =>
|
||||||
|
logger.info "OK '#{@sessionKey digest}' created"
|
||||||
|
# return cookie data
|
||||||
|
resolve
|
||||||
|
value: digest
|
||||||
|
properties:
|
||||||
|
expires: EXPIRY.login
|
||||||
|
|
||||||
|
destroy: (digest) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# delete hash immediately
|
||||||
|
@redis.del (@sessionKey digest), (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
reject null
|
||||||
|
else
|
||||||
|
logger.info "OK '#{@sessionKey digest}' deleted"
|
||||||
|
resolve null
|
||||||
|
|
||||||
|
check: (digest) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# refresh expiry timer on digest
|
||||||
|
@redis.expire (@sessionKey digest), EXPIRY.login * 86400, (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
@redis.get (@sessionKey digest), (err, res) =>
|
||||||
|
logger.debug "OK '#{@sessionKey digest}' resumed"
|
||||||
|
resolve res
|
||||||
|
|
||||||
|
newGame: (userid) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# generate hash
|
||||||
|
hmac = crypto.createHmac 'sha256', Math.random().toString()
|
||||||
|
hmac.update userid.toString()
|
||||||
|
digest = hmac.digest 'base64'
|
||||||
|
|
||||||
|
# insert game key
|
||||||
|
@redis.hsetnx (@gameKey digest), 'owner', userid, (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
@redis.del (@gameKey digest)
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
@redis.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
@redis.del (@gameKey digest)
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
# add game to active set
|
||||||
|
@redis.sadd (@gameKey 'active'), (@gameKey digest), (err, res) =>
|
||||||
|
# return game ID
|
||||||
|
logger.info "OK '#{@gameKey digest}' created"
|
||||||
|
resolve digest
|
||||||
|
|
||||||
|
getGames: ->
|
||||||
|
new Promise (resolve) =>
|
||||||
|
# function to return all active gameKeys
|
||||||
|
activeGameKeys = (set, cursor) =>
|
||||||
|
# start iteration
|
||||||
|
set ?= new Set()
|
||||||
|
cursor ?= '0'
|
||||||
|
|
||||||
|
return new Promise (resolve, reject) =>
|
||||||
|
# scan "active" gameKey
|
||||||
|
@redis.sscan (@gameKey 'active'), cursor, 'COUNT', '100', (err, res) ->
|
||||||
|
if err
|
||||||
|
reject null
|
||||||
|
|
||||||
|
# add to results set
|
||||||
|
cursor = res[0]
|
||||||
|
for key in res[1]
|
||||||
|
set.add key
|
||||||
|
|
||||||
|
if cursor == '0'
|
||||||
|
# done on cursor = 0
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
joinGame: (digest, userid) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# refresh expiry timer on digest
|
||||||
|
@redis.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
# insert opponent value
|
||||||
|
@redis.hsetnx (@gameKey digest), 'opponent', userid, (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
# return game ID
|
||||||
|
logger.info "OK '#{@gameKey digest}' joined"
|
||||||
|
resolve digest
|
||||||
|
|
||||||
|
updateGame: (digest, state) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# refresh expiry timer on digest
|
||||||
|
@redis.expire (@gameKey digest), EXPIRY.game * 86400, (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
# update state value
|
||||||
|
@redis.hset (@gameKey digest), 'state', (JSON.stringify state), (err, res) =>
|
||||||
|
if res == 0
|
||||||
|
reject null
|
||||||
|
|
||||||
|
else
|
||||||
|
# return game ID
|
||||||
|
logger.info "OK '#{@gameKey digest}' updated"
|
||||||
|
resolve digest
|
||||||
|
|
||||||
|
module.exports = new FFTCGSESSION
|
42
backend/socket.coffee
Normal file
42
backend/socket.coffee
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# node libraries
|
||||||
|
path = (require 'path')
|
||||||
|
logger = (require 'logging').default 'socket'
|
||||||
|
|
||||||
|
# my libraries
|
||||||
|
|
||||||
|
module.exports = (socket) ->
|
||||||
|
logger.info 'OK connect'
|
||||||
|
|
||||||
|
socket.on 'message', (msg) ->
|
||||||
|
# echo server
|
||||||
|
logger.info "OK received '#{msg}'"
|
||||||
|
socket.send "Re: #{msg}"
|
||||||
|
|
||||||
|
socket.on 'close', ->
|
||||||
|
logger.info 'OK disconnect'
|
||||||
|
|
||||||
|
# FFTCGSOCKET = (http, session) ->
|
||||||
|
# that = @
|
||||||
|
#
|
||||||
|
# # create server socket
|
||||||
|
# @io = socketio http
|
||||||
|
# @io.use session
|
||||||
|
#
|
||||||
|
# # on new connection
|
||||||
|
# @io.on 'connection', (socket) ->
|
||||||
|
# @session = socket.handshake.session
|
||||||
|
# logger.debug "session '#{@session.id}' connected"
|
||||||
|
# logger.debug "is user '#{@session.userID}'" if @session.userID
|
||||||
|
#
|
||||||
|
# socket.on 'disconnect', ->
|
||||||
|
# logger.debug "session '#{that.session.id}' disconnected"
|
||||||
|
# logger.debug "is user '#{that.session.userID}'" if that.session.userID
|
||||||
|
#
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# FFTCGSOCKET::close = ->
|
||||||
|
# logger.info 'shutting down'
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# module.exports = FFTCGSOCKET
|
38
backend/tmpfront/index.html
Normal file
38
backend/tmpfront/index.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Hello World
|
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
const host = location.origin.replace(/^http/, 'ws')
|
||||||
|
const ws = new WebSocket(host)
|
||||||
|
ws.onmessage = msg => console.log(msg.data)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('Hai')
|
||||||
|
ws.send('Ping') // Send the message 'Ping' to the server
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post('/user/register',{
|
||||||
|
login: 'jmm',
|
||||||
|
password: '123'
|
||||||
|
})
|
||||||
|
.then( (response) => {
|
||||||
|
console.log('register', response)
|
||||||
|
})
|
||||||
|
|
||||||
|
axios.post('/user/login',{
|
||||||
|
login: 'jmm',
|
||||||
|
password: '123'
|
||||||
|
})
|
||||||
|
.then( (response) => {
|
||||||
|
console.log('login', response)
|
||||||
|
console.log('cookie', document.cookie)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
3169
backend/yarn.lock
Normal file
3169
backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
24
docker-compose.override.yml
Normal file
24
docker-compose.override.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
version: "2.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
redis:
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
target: dev
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- "./backend:/app"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
target: dev
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- "./frontend:/app"
|
||||||
|
- "./frontend/.vue-cli-ui:/root/.vue-cli-ui"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "8080:8080"
|
16
docker-compose.prod.yml
Normal file
16
docker-compose.prod.yml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
version: "2.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
redis:
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
target: prod
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
target: prod
|
||||||
|
restart: unless-stopped
|
|
@ -1,20 +1,16 @@
|
||||||
version: "2"
|
version: "2.3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
fftcg:
|
|
||||||
build: .
|
|
||||||
command: "yarn debug"
|
|
||||||
restart: "no"
|
|
||||||
volumes:
|
|
||||||
- "${PWD}/src:/app/src"
|
|
||||||
- "${PWD}/views:/app/views:ro"
|
|
||||||
- "${PWD}/public_html:/app/public_html"
|
|
||||||
- "${PWD}/inc:/app/inc:ro"
|
|
||||||
- "${PWD}/server.coffee:/app/server.coffee:ro"
|
|
||||||
# - "${PWD}/fftcg.db:/app/fftcg.db"
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
restart: "no"
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
|
BIN
fftcg.db
BIN
fftcg.db
Binary file not shown.
3
frontend/.browserslistrc
Normal file
3
frontend/.browserslistrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not ie <= 8
|
14
frontend/.eslintrc.js
Normal file
14
frontend/.eslintrc.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: ['plugin:vue/essential', '@vue/prettier'],
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
}
|
||||||
|
}
|
21
frontend/.gitignore
vendored
Normal file
21
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw*
|
5
frontend/.prettierrc
Normal file
5
frontend/.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# .prettierrc
|
||||||
|
# trailingComma: "es5"
|
||||||
|
tabWidth: 2
|
||||||
|
semi: false
|
||||||
|
singleQuote: true
|
54
frontend/.vue-cli-ui/db.json
Normal file
54
frontend/.vue-cli-ui/db.json
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"id": "SdPO5j8y6",
|
||||||
|
"path": "/app",
|
||||||
|
"favorite": 1,
|
||||||
|
"type": "vue",
|
||||||
|
"name": "frontend",
|
||||||
|
"openDate": 1557166583987,
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "5sGiztidd",
|
||||||
|
"definitionId": "org.vue.widgets.run-task",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 2,
|
||||||
|
"height": 1,
|
||||||
|
"config": {
|
||||||
|
"task": "/app:serve"
|
||||||
|
},
|
||||||
|
"configured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "SyF7UHhrK",
|
||||||
|
"definitionId": "org.vue.widgets.run-task",
|
||||||
|
"x": 0,
|
||||||
|
"y": 1,
|
||||||
|
"width": 2,
|
||||||
|
"height": 1,
|
||||||
|
"config": {
|
||||||
|
"task": "/app:lint"
|
||||||
|
},
|
||||||
|
"configured": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foldersFavorite": [],
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "/app:serve",
|
||||||
|
"answers": {
|
||||||
|
"open": false,
|
||||||
|
"mode": "development",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": "3000",
|
||||||
|
"https": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"lastOpenProject": "SdPO5j8y6"
|
||||||
|
}
|
||||||
|
}
|
21
frontend/Dockerfile
Normal file
21
frontend/Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
FROM node:lts AS dev
|
||||||
|
# some dir for our code
|
||||||
|
WORKDIR /app
|
||||||
|
RUN yarn global add @vue/cli @vue/cli-service-global
|
||||||
|
# mount code
|
||||||
|
VOLUME ["/app"]
|
||||||
|
# this is how we start
|
||||||
|
CMD [ "vue", "ui", "--host", "0.0.0.0", "--port", "8080" ]
|
||||||
|
|
||||||
|
FROM node:lts AS build
|
||||||
|
# some dir for our code
|
||||||
|
WORKDIR /app
|
||||||
|
# install dependencies
|
||||||
|
COPY package*.json yarn*.lock ./
|
||||||
|
RUN yarn --production=false
|
||||||
|
# copy code
|
||||||
|
COPY . .
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine AS prod
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
34
frontend/README.md
Normal file
34
frontend/README.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# frontend
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
yarn run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your tests
|
||||||
|
```
|
||||||
|
yarn run test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
yarn run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your unit tests
|
||||||
|
```
|
||||||
|
yarn run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
3
frontend/babel.config.js
Normal file
3
frontend/babel.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: ['@vue/app']
|
||||||
|
}
|
17
frontend/jest.config.js
Normal file
17
frontend/jest.config.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.vue$': 'vue-jest',
|
||||||
|
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
|
||||||
|
'jest-transform-stub',
|
||||||
|
'^.+\\.jsx?$': 'babel-jest'
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1'
|
||||||
|
},
|
||||||
|
snapshotSerializers: ['jest-serializer-vue'],
|
||||||
|
testMatch: [
|
||||||
|
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
|
||||||
|
],
|
||||||
|
testURL: 'http://localhost/'
|
||||||
|
}
|
43
frontend/package.json
Normal file
43
frontend/package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint",
|
||||||
|
"test:unit": "vue-cli-service test:unit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"material-design-icons-iconfont": "^4.0.5",
|
||||||
|
"roboto-fontface": "*",
|
||||||
|
"vue": "^2.6.6",
|
||||||
|
"vue-router": "^3.0.1",
|
||||||
|
"vuetify": "^1.5.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "^3.4.0",
|
||||||
|
"@vue/cli-plugin-eslint": "^3.4.0",
|
||||||
|
"@vue/cli-plugin-unit-jest": "^3.4.0",
|
||||||
|
"@vue/cli-service": "^3.4.0",
|
||||||
|
"@vue/eslint-config-prettier": "^4.0.1",
|
||||||
|
"@vue/test-utils": "^1.0.0-beta.20",
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"babel-core": "7.0.0-bridge.0",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"babel-jest": "^24.8.0",
|
||||||
|
"coffee-loader": "^0.9.0",
|
||||||
|
"coffeescript": "^2.3.2",
|
||||||
|
"craftyjs": "^0.9.0",
|
||||||
|
"eslint": "^5.8.0",
|
||||||
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
|
"js-cookie": "^2.2.0",
|
||||||
|
"stylus": "^0.54.5",
|
||||||
|
"stylus-loader": "^3.0.1",
|
||||||
|
"vue-async-computed": "^3.6.1",
|
||||||
|
"vue-cli-plugin-coffeescript": "^0.0.3",
|
||||||
|
"vue-cli-plugin-vuetify": "^0.5.0",
|
||||||
|
"vue-template-compiler": "^2.5.21",
|
||||||
|
"vuetify-loader": "^1.0.5"
|
||||||
|
}
|
||||||
|
}
|
5
frontend/postcss.config.js
Normal file
5
frontend/postcss.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
17
frontend/public/index.html
Normal file
17
frontend/public/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
11
frontend/src/App.vue
Normal file
11
frontend/src/App.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<router-view />
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'App'
|
||||||
|
}
|
||||||
|
</script>
|
36
frontend/src/ExampleApp.vue
Normal file
36
frontend/src/ExampleApp.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-toolbar app>
|
||||||
|
<v-toolbar-title class="headline text-uppercase">
|
||||||
|
<span>Vuetify</span>
|
||||||
|
<span class="font-weight-light">MATERIAL DESIGN</span>
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn flat :to="{ path: '/' }">Home</v-btn>
|
||||||
|
<v-btn flat :to="{ path: '/about' }">About</v-btn>
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
href="https://github.com/vuetifyjs/vuetify/releases/latest"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="mr-2">Latest Release</span>
|
||||||
|
<v-icon>open_in_new</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-content>
|
||||||
|
<router-view />
|
||||||
|
</v-content>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ExampleApp',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
Before Width: | Height: | Size: 610 KiB After Width: | Height: | Size: 610 KiB |
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
After Width: | Height: | Size: 539 B |
Before Width: | Height: | Size: 615 KiB After Width: | Height: | Size: 615 KiB |
212
frontend/src/classes/Deck.js
Normal file
212
frontend/src/classes/Deck.js
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
import CardsDB from '@/plugins/ffdecks'
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
constructor(id) {
|
||||||
|
this.id = id
|
||||||
|
this.name = ''
|
||||||
|
this.note = ''
|
||||||
|
this.cards = []
|
||||||
|
}
|
||||||
|
|
||||||
|
populate() {
|
||||||
|
for (let card of this.cards) {
|
||||||
|
card.dbentry = CardsDB[card.serial]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
from_object(obj) {
|
||||||
|
if (obj) {
|
||||||
|
this.name = obj.name
|
||||||
|
this.note = obj.note
|
||||||
|
this.cards = obj.cards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plainObject() {
|
||||||
|
let plainCards = []
|
||||||
|
for (let card of this.cards) {
|
||||||
|
plainCards.push({
|
||||||
|
serial: card.serial,
|
||||||
|
count: card.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
note: this.note,
|
||||||
|
cards: plainCards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
from_deckList(str) {
|
||||||
|
// select all lines containing card serial numbers
|
||||||
|
let cardLinesRE = /^.*\b\d+-0*\d{1,3}[A-Z]?\b.*$/gm
|
||||||
|
let cardLines = str.match(cardLinesRE)
|
||||||
|
let cardCounts = {}
|
||||||
|
|
||||||
|
if (cardLines) {
|
||||||
|
for (let cardLine of cardLines) {
|
||||||
|
// extract serial (guaranteed to be in here!)
|
||||||
|
let serialRE = /\b(\d+)-0*(\d{1,3})[A-Z]?\b/i
|
||||||
|
let serial = serialRE.exec(cardLine)
|
||||||
|
// force format 'x-xxx'
|
||||||
|
serial = `${serial[1]}-${serial[2].padStart(3, '0')}`
|
||||||
|
|
||||||
|
// strip out serial number
|
||||||
|
cardLine = cardLine.replace(serialRE, '')
|
||||||
|
|
||||||
|
let countREs = [
|
||||||
|
// prioritize a count with "times" symbol *, x, ×
|
||||||
|
/\b([0-9]+)(?:[*×]|[x]\b)/,
|
||||||
|
/(?:[*×]|\b[x])([0-9]+)\b/,
|
||||||
|
// next priority: count with whitespace
|
||||||
|
/\s+([0-9]+)\s+/,
|
||||||
|
/\s+([0-9]+)\b/,
|
||||||
|
/\b([0-9]+)\s+/,
|
||||||
|
// least priority: any simple number
|
||||||
|
/\b([0-9]+)\b/
|
||||||
|
]
|
||||||
|
|
||||||
|
// fallback value
|
||||||
|
let count = 1
|
||||||
|
for (let countRE of countREs) {
|
||||||
|
let data = countRE.exec(cardLine)
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
count = Number(data[1])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// count copies
|
||||||
|
if (!cardCounts[serial]) {
|
||||||
|
cardCounts[serial] = 0
|
||||||
|
}
|
||||||
|
cardCounts[serial] += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// push card data into deck
|
||||||
|
this.cards = []
|
||||||
|
for (let serial in cardCounts) {
|
||||||
|
this.cards.push({
|
||||||
|
serial: serial,
|
||||||
|
count: cardCounts[serial]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip out lines with serial numbers
|
||||||
|
str = str.replace(cardLinesRE, '')
|
||||||
|
// then strip out anything after the first empty line
|
||||||
|
str = str.replace(/^[\s]*$[^]*/m, '')
|
||||||
|
|
||||||
|
// select the line containing 'deck name:'
|
||||||
|
// and its successor (ffdecks format)
|
||||||
|
let metaRE = /^Deck Name: (.+)$[\s]*?^(.+)$/m
|
||||||
|
let metaData = metaRE.exec(str)
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
this.name = 'Unnamed Deck'
|
||||||
|
this.note = ''
|
||||||
|
|
||||||
|
if (!metaData) {
|
||||||
|
// use lax format: <anything>:[deck name][newline][note]
|
||||||
|
metaRE = /[^]*?:(.+)$[\s]*?^([^]*)/m
|
||||||
|
metaData = metaRE.exec(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// look again, I am not an else!
|
||||||
|
if (metaData) {
|
||||||
|
// extract matches
|
||||||
|
this.name = metaData[1].trim()
|
||||||
|
this.note = metaData[2].trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts() {
|
||||||
|
let retval = ['Forwards', 'Backups', 'Summons, Monsters & more'].map(
|
||||||
|
item => ({
|
||||||
|
heading: item,
|
||||||
|
cards: [],
|
||||||
|
count: 0
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let card of this.cards) {
|
||||||
|
let target
|
||||||
|
switch (card.dbentry.type) {
|
||||||
|
case 'Forward':
|
||||||
|
target = 0
|
||||||
|
break
|
||||||
|
case 'Backup':
|
||||||
|
target = 1
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
target = 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
retval[target].cards.push(card)
|
||||||
|
retval[target].count += card.count
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let part of retval) {
|
||||||
|
part.cards.sort(
|
||||||
|
(card_l, card_r) => card_l.dbentry.cost - card_r.dbentry.cost
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
deckList() {
|
||||||
|
// empty deck is empty
|
||||||
|
if (this.count() == 0) return ''
|
||||||
|
|
||||||
|
let lines = []
|
||||||
|
|
||||||
|
// begin with deck name and note
|
||||||
|
lines.push('Deck Name: ' + this.name)
|
||||||
|
lines.push(this.note)
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
// list each deck part
|
||||||
|
for (let part of this.parts()) {
|
||||||
|
lines.push(`${part.heading} (${part.count}):`)
|
||||||
|
|
||||||
|
for (let card of part.cards)
|
||||||
|
lines.push(`${card.count}x ${card.serial} "${card.dbentry.name}"`)
|
||||||
|
|
||||||
|
lines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
count() {
|
||||||
|
return this.cards.reduce((total, card) => total + card.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
elements() {
|
||||||
|
let elements = {}
|
||||||
|
|
||||||
|
for (let card of this.cards) {
|
||||||
|
if (!elements[card.dbentry.element]) elements[card.dbentry.element] = 0
|
||||||
|
|
||||||
|
elements[card.dbentry.element] += card.count
|
||||||
|
}
|
||||||
|
|
||||||
|
let retval = []
|
||||||
|
|
||||||
|
for (let element in elements)
|
||||||
|
retval.push({
|
||||||
|
name: element,
|
||||||
|
count: elements[element]
|
||||||
|
})
|
||||||
|
|
||||||
|
retval.sort((element_l, element_r) => element_r.count - element_l.count)
|
||||||
|
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
}
|
63
frontend/src/components/Card.vue
Normal file
63
frontend/src/components/Card.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<v-list-tile avatar>
|
||||||
|
<v-tooltip @input="booted = true" bottom>
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-list-tile-avatar v-on="on" style="cursor: zoom-in">
|
||||||
|
<Crystal :size="35" :element="dbentry.element" :cost="dbentry.cost" />
|
||||||
|
</v-list-tile-avatar>
|
||||||
|
</template>
|
||||||
|
<v-img
|
||||||
|
v-if="booted"
|
||||||
|
:src="ffiurl"
|
||||||
|
:height="300"
|
||||||
|
:width="0.715 * 300"
|
||||||
|
contain
|
||||||
|
/>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title class="body-2">{{ dbentry.name }}</v-list-tile-title>
|
||||||
|
<v-list-tile-sub-title>{{ full_serial }}</v-list-tile-sub-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
|
||||||
|
<v-list-tile-avatar>
|
||||||
|
<span class="subheading">{{ count }}</span>
|
||||||
|
</v-list-tile-avatar>
|
||||||
|
</v-list-tile>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Crystal from './Crystal.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Card',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Crystal
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
count: Number,
|
||||||
|
serial: String,
|
||||||
|
dbentry: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
booted: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
full_serial() {
|
||||||
|
return this.serial + this.dbentry.rarity[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
ffiurl() {
|
||||||
|
return (
|
||||||
|
'https://fftcg.square-enix-games.com/theme/tcg/images/cards/full/' +
|
||||||
|
this.full_serial +
|
||||||
|
'_eg.jpg'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
71
frontend/src/components/Crystal.vue
Normal file
71
frontend/src/components/Crystal.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:height="size + 'px'"
|
||||||
|
:width="(16 / 30) * size + 'px'"
|
||||||
|
viewBox="0 0 16 30"
|
||||||
|
xml:space="preserve"
|
||||||
|
>
|
||||||
|
<polygon points="0,5 8,0 16,5 16,25 8,30 0,25" :style="{ fill: color }" />
|
||||||
|
<polygon points="7,5 7,25 2,25 2,5" style="fill:rgba(255,255,255,0.3);" />
|
||||||
|
<polygon points="0,5 8,0 16,5" style="fill:rgba(255,255,255,0.6);" />
|
||||||
|
<polygon points="10,25 16,25 8,30" style="fill:rgba(255,255,255,0.5);" />
|
||||||
|
<text
|
||||||
|
x="50%"
|
||||||
|
text-anchor="middle"
|
||||||
|
y="17"
|
||||||
|
dominant-baseline="middle"
|
||||||
|
font-size="20"
|
||||||
|
font-family="sans-serif"
|
||||||
|
fill="white"
|
||||||
|
font-weight="bold"
|
||||||
|
transform-origin="center center"
|
||||||
|
:transform="`scale(${this.xscale},1)`"
|
||||||
|
>
|
||||||
|
{{ cost }}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Crystal',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
element: String,
|
||||||
|
size: Number,
|
||||||
|
cost: Number
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
color() {
|
||||||
|
switch (this.element.toLowerCase()) {
|
||||||
|
case 'fire':
|
||||||
|
return '#d41'
|
||||||
|
case 'ice':
|
||||||
|
return '#7ac'
|
||||||
|
case 'wind':
|
||||||
|
return '#596'
|
||||||
|
case 'earth':
|
||||||
|
return '#db1'
|
||||||
|
case 'lightning':
|
||||||
|
return '#859'
|
||||||
|
case 'water':
|
||||||
|
return '#57a'
|
||||||
|
case 'light':
|
||||||
|
return '#888'
|
||||||
|
case 'dark':
|
||||||
|
default:
|
||||||
|
return '#333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
xscale() {
|
||||||
|
if (typeof this.cost !== 'undefined') {
|
||||||
|
let len = this.cost.toString().length
|
||||||
|
return Math.pow(0.7, len - 1)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
155
frontend/src/components/Deck.vue
Normal file
155
frontend/src/components/Deck.vue
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<v-expansion-panel-content>
|
||||||
|
<template v-slot:header>
|
||||||
|
<v-layout align-center row>
|
||||||
|
<v-flex text-xs-center xs4 sm3 md2>
|
||||||
|
<Crystal
|
||||||
|
v-for="element in deck.elements()"
|
||||||
|
:size="40"
|
||||||
|
:cost="element.count"
|
||||||
|
:element="element.name"
|
||||||
|
:key="element.name"
|
||||||
|
/>
|
||||||
|
</v-flex>
|
||||||
|
<v-flex class="subheading">
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-text>
|
||||||
|
{{ deck.name }}
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="!editing">
|
||||||
|
<v-alert :value="deck.note" type="info">
|
||||||
|
{{ deck.note }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-container style="position: relative" grid-list-md fluid>
|
||||||
|
<v-layout row wrap>
|
||||||
|
<v-flex v-for="part in deck.parts()" :key="part.heading" xs12 sm6 md4>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>{{ part.count }} {{ part.heading }}</v-card-title>
|
||||||
|
<v-list dense subheader>
|
||||||
|
<Card
|
||||||
|
v-for="card in part.cards"
|
||||||
|
:key="card.serial"
|
||||||
|
:count="card.count"
|
||||||
|
:serial="card.serial"
|
||||||
|
:dbentry="card.dbentry"
|
||||||
|
></Card>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-btn fab absolute bottom right @click.native="editing = true">
|
||||||
|
<v-icon>edit</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-dialog v-model="deleting">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn fab absolute bottom left v-on="on">
|
||||||
|
<v-icon>delete</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
Really delete this deck?
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
Are you sure you want to delete your deck "{{ deck.name }}"?
|
||||||
|
This cannot be undone.
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn color="error" @click.native="deleting = false">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
@click.native="
|
||||||
|
deleting = false
|
||||||
|
delete_deck()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DeckEditor v-if="editing" :deck="deck" @save="save_deck">
|
||||||
|
<v-btn color="error" @click.native="editing = false">
|
||||||
|
<v-icon>cancel</v-icon>
|
||||||
|
cancel
|
||||||
|
</v-btn>
|
||||||
|
</DeckEditor>
|
||||||
|
</v-expansion-panel-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from '@/plugins/axios'
|
||||||
|
|
||||||
|
import Card from './Card.vue'
|
||||||
|
import Crystal from './Crystal.vue'
|
||||||
|
import DeckEditor from './forms/DeckEditor.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Deck',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
session: String,
|
||||||
|
deck: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Card,
|
||||||
|
Crystal,
|
||||||
|
DeckEditor
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
editing: false,
|
||||||
|
deleting: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
save_deck(new_deck) {
|
||||||
|
axios
|
||||||
|
.post('/decks/modify', {
|
||||||
|
session: this.session,
|
||||||
|
deckID: this.deck.id,
|
||||||
|
deckCards: new_deck.plainObject()
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.editing = false
|
||||||
|
this.$emit('change')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_deck() {
|
||||||
|
axios
|
||||||
|
.post('/decks/delete', {
|
||||||
|
session: this.session,
|
||||||
|
deckID: this.deck.id
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.$emit('change')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
85
frontend/src/components/DeckList.vue
Normal file
85
frontend/src/components/DeckList.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<v-expansion-panel v-model="open">
|
||||||
|
<NewDeck
|
||||||
|
:session="session"
|
||||||
|
@change="
|
||||||
|
open = null
|
||||||
|
refresh_decks()
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Deck
|
||||||
|
v-for="deck in linked"
|
||||||
|
:deck="deck"
|
||||||
|
:session="session"
|
||||||
|
:key="deck.id"
|
||||||
|
@change="refresh_decks"
|
||||||
|
/>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from '@/plugins/axios'
|
||||||
|
import DeckJS from '@/classes/Deck'
|
||||||
|
|
||||||
|
import Deck from './Deck.vue'
|
||||||
|
import NewDeck from './NewDeck.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DeckList',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Deck,
|
||||||
|
NewDeck
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
session: String
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
open: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
asyncComputed: {
|
||||||
|
decks: {
|
||||||
|
get() {
|
||||||
|
return axios
|
||||||
|
.post('/decks/list', {
|
||||||
|
session: this.session
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.decks
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
default: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
linked() {
|
||||||
|
let result = []
|
||||||
|
|
||||||
|
for (let plainDeck of this.decks) {
|
||||||
|
let deck = new DeckJS(plainDeck.id)
|
||||||
|
|
||||||
|
deck.from_object(plainDeck.content)
|
||||||
|
deck.populate()
|
||||||
|
|
||||||
|
result.push(deck)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sort((deck_l, deck_r) => deck_l.name.localeCompare(deck_r.name))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refresh_decks() {
|
||||||
|
this.$asyncComputed.decks.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
36
frontend/src/components/GamesList.vue
Normal file
36
frontend/src/components/GamesList.vue
Normal 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>
|
16
frontend/src/components/Header.vue
Normal file
16
frontend/src/components/Header.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<v-toolbar app>
|
||||||
|
<v-toolbar-title class="headline text-uppercase">
|
||||||
|
<span>Yavook</span>
|
||||||
|
<span class="font-weight-light">!FFTCG</span>
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<slot />
|
||||||
|
</v-toolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Header'
|
||||||
|
}
|
||||||
|
</script>
|
116
frontend/src/components/HeaderIntern.vue
Normal file
116
frontend/src/components/HeaderIntern.vue
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<Header v-if="user">
|
||||||
|
<v-btn flat :to="{ name: 'deckcp' }">
|
||||||
|
<v-icon>view_carousel</v-icon> Decks
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn flat :to="{ name: 'games' }">
|
||||||
|
<v-icon>play_arrow</v-icon> Play
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn flat :to="{ name: 'usercp' }">
|
||||||
|
<v-icon>person</v-icon> {{ user.login }}
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-dialog v-model="logging_out">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn flat v-on="on"> <v-icon>power_off</v-icon> Logout </v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
Log Out?
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
Are you sure you want to log out?
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn color="error" @click.native="logging_out = false">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn color="success" @click.native="logout">
|
||||||
|
Confirm
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</Header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as Cookies from 'js-cookie'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import Header from './Header'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HeaderIntern',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Header
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
logging_out: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: String
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
goHome() {
|
||||||
|
this.$emit('input', '')
|
||||||
|
Cookies.remove('session')
|
||||||
|
this.$router.push({ name: 'home' })
|
||||||
|
},
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
axios
|
||||||
|
.post('/user/logout', {
|
||||||
|
session: this.value
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.goHome()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
session: () => Cookies.get('session')
|
||||||
|
},
|
||||||
|
|
||||||
|
asyncComputed: {
|
||||||
|
user: {
|
||||||
|
get() {
|
||||||
|
return axios
|
||||||
|
.post('/user/info', {
|
||||||
|
session: this.session
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.$emit('input', this.session)
|
||||||
|
this.$emit('user', response.data.user)
|
||||||
|
return response.data.user
|
||||||
|
} else {
|
||||||
|
this.goHome()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
user: 0,
|
||||||
|
login: '',
|
||||||
|
settings: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
134
frontend/src/components/HelloWorld.vue
Normal file
134
frontend/src/components/HelloWorld.vue
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-layout text-xs-center wrap>
|
||||||
|
<v-flex xs12>
|
||||||
|
<v-img
|
||||||
|
:src="require('../assets/logo.svg')"
|
||||||
|
class="my-3"
|
||||||
|
contain
|
||||||
|
height="200"
|
||||||
|
></v-img>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-flex mb-4>
|
||||||
|
<h1 class="display-2 font-weight-bold mb-3">
|
||||||
|
Welcome to Vuetify
|
||||||
|
</h1>
|
||||||
|
<p class="subheading font-weight-regular">
|
||||||
|
For help and collaboration with other Vuetify developers,
|
||||||
|
<br />please join our online
|
||||||
|
<a href="https://community.vuetifyjs.com" target="_blank"
|
||||||
|
>Discord Community</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-flex mb-5 xs12>
|
||||||
|
<h2 class="headline font-weight-bold mb-3">What's next?</h2>
|
||||||
|
|
||||||
|
<v-layout justify-center>
|
||||||
|
<a
|
||||||
|
v-for="(next, i) in whatsNext"
|
||||||
|
:key="i"
|
||||||
|
:href="next.href"
|
||||||
|
class="subheading mx-3"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ next.text }}
|
||||||
|
</a>
|
||||||
|
</v-layout>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-flex xs12 mb-5>
|
||||||
|
<h2 class="headline font-weight-bold mb-3">Important Links</h2>
|
||||||
|
|
||||||
|
<v-layout justify-center>
|
||||||
|
<a
|
||||||
|
v-for="(link, i) in importantLinks"
|
||||||
|
:key="i"
|
||||||
|
:href="link.href"
|
||||||
|
class="subheading mx-3"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ link.text }}
|
||||||
|
</a>
|
||||||
|
</v-layout>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-flex xs12 mb-5>
|
||||||
|
<h2 class="headline font-weight-bold mb-3">Ecosystem</h2>
|
||||||
|
|
||||||
|
<v-layout justify-center>
|
||||||
|
<a
|
||||||
|
v-for="(eco, i) in ecosystem"
|
||||||
|
:key="i"
|
||||||
|
:href="eco.href"
|
||||||
|
class="subheading mx-3"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ eco.text }}
|
||||||
|
</a>
|
||||||
|
</v-layout>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
ecosystem: [
|
||||||
|
{
|
||||||
|
text: 'vuetify-loader',
|
||||||
|
href: 'https://github.com/vuetifyjs/vuetify-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'github',
|
||||||
|
href: 'https://github.com/vuetifyjs/vuetify'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'awesome-vuetify',
|
||||||
|
href: 'https://github.com/vuetifyjs/awesome-vuetify'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
importantLinks: [
|
||||||
|
{
|
||||||
|
text: 'Documentation',
|
||||||
|
href: 'https://vuetifyjs.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Chat',
|
||||||
|
href: 'https://community.vuetifyjs.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Made with Vuetify',
|
||||||
|
href: 'https://madewithvuejs.com/vuetify'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Twitter',
|
||||||
|
href: 'https://twitter.com/vuetifyjs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Articles',
|
||||||
|
href: 'https://medium.com/vuetify'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
whatsNext: [
|
||||||
|
{
|
||||||
|
text: 'Explore components',
|
||||||
|
href: 'https://vuetifyjs.com/components/api-explorer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Select a layout',
|
||||||
|
href: 'https://vuetifyjs.com/layout/pre-defined'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Frequently Asked Questions',
|
||||||
|
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
57
frontend/src/components/NewDeck.vue
Normal file
57
frontend/src/components/NewDeck.vue
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<v-expansion-panel-content>
|
||||||
|
<template v-slot:header>
|
||||||
|
<v-layout align-center row>
|
||||||
|
<v-flex text-xs-center xs4 sm3 md2>
|
||||||
|
<v-icon>view_carousel</v-icon>
|
||||||
|
<v-icon>add</v-icon>
|
||||||
|
</v-flex>
|
||||||
|
<v-flex class="subheading">
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-text>
|
||||||
|
New Deck
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card flat>
|
||||||
|
<DeckEditor ref="editor" @save="save_deck" />
|
||||||
|
</v-card>
|
||||||
|
</v-expansion-panel-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from '@/plugins/axios'
|
||||||
|
|
||||||
|
import DeckEditor from './forms/DeckEditor.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NewDeck',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
DeckEditor
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
session: String
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
save_deck(new_deck) {
|
||||||
|
axios
|
||||||
|
.post('/decks/add', {
|
||||||
|
session: this.session,
|
||||||
|
deckCards: new_deck.plainObject()
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.$emit('change')
|
||||||
|
this.$refs.editor.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
16
frontend/src/components/UserInfo.vue
Normal file
16
frontend/src/components/UserInfo.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
{{ user.login }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UserInfo',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
session: String,
|
||||||
|
user: Object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
118
frontend/src/components/forms/DeckEditor.vue
Normal file
118
frontend/src/components/forms/DeckEditor.vue
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-card flat>
|
||||||
|
<v-alert :value="check.count !== 50" type="warning">
|
||||||
|
{{ check.count }} cards detected! (Decks should have exactly 50 cards)
|
||||||
|
</v-alert>
|
||||||
|
<v-alert :value="check.maximum > 3" type="warning">
|
||||||
|
Card with {{ check.maximum }} copies detected! (Cards should not have
|
||||||
|
more than 3 copies)
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
ref="deckList"
|
||||||
|
:label="
|
||||||
|
`${
|
||||||
|
typeof deck !== 'undefined' && deck !== null ? 'Edit' : 'Paste'
|
||||||
|
} Decklist`
|
||||||
|
"
|
||||||
|
rows="35"
|
||||||
|
:hint="
|
||||||
|
`ffdecks.com format – One card per line – Include quantity and serial (e.g. ${format_sample})`
|
||||||
|
"
|
||||||
|
style="font-family: monospace"
|
||||||
|
:value="new_deck.deckList()"
|
||||||
|
@input="check.checked = false"
|
||||||
|
>
|
||||||
|
</v-textarea>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<slot></slot>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn color="info" @click.native="validate" :disabled="check.checked">
|
||||||
|
<v-icon>check</v-icon>
|
||||||
|
validate
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn color="success" @click.native="save" :disabled="!check.checked">
|
||||||
|
<v-icon>save</v-icon>
|
||||||
|
save
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Deck from '@/classes/Deck'
|
||||||
|
import CardsDB from '@/plugins/ffdecks'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DeckEditor',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
deck: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
check: null,
|
||||||
|
new_deck: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
format_sample() {
|
||||||
|
let serials = []
|
||||||
|
for (let tmp_key in CardsDB) {
|
||||||
|
if (CardsDB.hasOwnProperty(tmp_key)) {
|
||||||
|
serials.push(tmp_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let quantity = Math.floor(Math.random() * 3) + 1
|
||||||
|
let serial = serials[Math.floor(Math.random() * serials.length)]
|
||||||
|
return `${quantity}x ${serial}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
clear() {
|
||||||
|
this.check = {
|
||||||
|
count: 50,
|
||||||
|
maximum: 0,
|
||||||
|
checked: false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.new_deck = new Deck(0)
|
||||||
|
if (this.deck)
|
||||||
|
// this.deck should already be populated!
|
||||||
|
this.new_deck.from_object(this.deck)
|
||||||
|
},
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
this.new_deck.from_deckList(this.$refs.deckList.lazyValue)
|
||||||
|
this.new_deck.populate()
|
||||||
|
|
||||||
|
// count number of cards
|
||||||
|
this.check.count = this.new_deck.count()
|
||||||
|
|
||||||
|
// find most frequent card
|
||||||
|
this.check.maximum = 0
|
||||||
|
for (let card of this.new_deck.cards) {
|
||||||
|
if (card.count > this.check.maximum) this.check.maximum = card.count
|
||||||
|
}
|
||||||
|
|
||||||
|
// deck has now been checked
|
||||||
|
this.check.checked = true
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.$emit('save', this.new_deck)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
99
frontend/src/components/forms/FormDialog.vue
Normal file
99
frontend/src/components/forms/FormDialog.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog">
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn flat v-on="on">
|
||||||
|
<v-icon>{{ icon }}</v-icon> {{ buttonText }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card>
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.visible"
|
||||||
|
:timeout="6000"
|
||||||
|
:color="snackbar.color"
|
||||||
|
absolute
|
||||||
|
top
|
||||||
|
>
|
||||||
|
{{ snackbar.text }}
|
||||||
|
<v-btn @click.native="snackbar.visible = false" fab flat icon>
|
||||||
|
<v-icon>close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
v-model="valid"
|
||||||
|
@submit.prevent="validate"
|
||||||
|
lazy-validation
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn color="success" type="submit" :disabled="!valid">
|
||||||
|
{{ buttonText }}
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn color="error" @click.native="dialog = false">
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FormDialog',
|
||||||
|
data: () => ({
|
||||||
|
dialog: false,
|
||||||
|
valid: true,
|
||||||
|
snackbar: {
|
||||||
|
visible: false,
|
||||||
|
color: '',
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
props: {
|
||||||
|
buttonText: String,
|
||||||
|
icon: String
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
validate() {
|
||||||
|
if (this.$refs.form.validate()) {
|
||||||
|
this.$emit('validated')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showSnackbar(text, color) {
|
||||||
|
if (text == '') return
|
||||||
|
|
||||||
|
this.snackbar.visible = false
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.snackbar.text = text
|
||||||
|
this.snackbar.color = color
|
||||||
|
this.snackbar.visible = true
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
dialog(val) {
|
||||||
|
if (val) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.$parent.$refs.autofocus.focus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$refs.form.resetValidation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
75
frontend/src/components/forms/Login.vue
Normal file
75
frontend/src/components/forms/Login.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<FormDialog ref="main" buttonText="Login" icon="power" @validated="doLogin">
|
||||||
|
<v-card-title class="headline">
|
||||||
|
Log In
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
ref="autofocus"
|
||||||
|
v-model="login"
|
||||||
|
:rules="loginRules"
|
||||||
|
label="User name"
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
:rules="passwordRules"
|
||||||
|
:append-icon="showPassword ? 'visibility' : 'visibility_off'"
|
||||||
|
@click:append="showPassword = !showPassword"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
label="Password"
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
</v-card-text>
|
||||||
|
</FormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FormDialog from './FormDialog.vue'
|
||||||
|
import * as Cookies from 'js-cookie'
|
||||||
|
import axios from '@/plugins/axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoginForm',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
FormDialog
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
session: String
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
login: '',
|
||||||
|
loginRules: [v => !!v || 'Please enter user name'],
|
||||||
|
|
||||||
|
password: '',
|
||||||
|
showPassword: false,
|
||||||
|
passwordRules: [v => !!v || 'Please enter password']
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
doLogin() {
|
||||||
|
axios
|
||||||
|
.post('/user/login', {
|
||||||
|
session: this.session,
|
||||||
|
login: this.login,
|
||||||
|
password: this.password
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
let cookie_data = JSON.parse(response.data.message)
|
||||||
|
Cookies.set('session', cookie_data.value, cookie_data.properties)
|
||||||
|
this.$refs.main.showSnackbar('Login successful!', 'success')
|
||||||
|
this.$router.push('deckcp')
|
||||||
|
} else {
|
||||||
|
this.$refs.main.showSnackbar(response.data.message, 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
92
frontend/src/components/forms/Register.vue
Normal file
92
frontend/src/components/forms/Register.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<FormDialog
|
||||||
|
ref="main"
|
||||||
|
buttonText="Register"
|
||||||
|
icon="book"
|
||||||
|
@validated="doRegister"
|
||||||
|
>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
Register
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
ref="autofocus"
|
||||||
|
v-model="login"
|
||||||
|
:rules="loginRules"
|
||||||
|
label="User name"
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
:append-icon="showPassword ? 'visibility' : 'visibility_off'"
|
||||||
|
@click:append="showPassword = !showPassword"
|
||||||
|
:rules="passwordRules"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
label="Password"
|
||||||
|
required
|
||||||
|
counter
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-if="!showPassword"
|
||||||
|
v-model="passwordConfirm"
|
||||||
|
:rules="passwordConfirmRules"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
label="Confirm Password"
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
</v-card-text>
|
||||||
|
</FormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FormDialog from './FormDialog.vue'
|
||||||
|
import axios from '@/plugins/axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RegisterForm',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
FormDialog
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
login: '',
|
||||||
|
loginRules: [v => !!v || 'User name is required'],
|
||||||
|
|
||||||
|
password: '',
|
||||||
|
showPassword: false,
|
||||||
|
passwordRules: [v => !!v || 'Password is required'],
|
||||||
|
|
||||||
|
passwordConfirm: ''
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
passwordConfirmRules() {
|
||||||
|
return [
|
||||||
|
() => this.password === this.passwordConfirm || 'Passwords must match'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
doRegister() {
|
||||||
|
axios
|
||||||
|
.post('/user/register', {
|
||||||
|
session: null,
|
||||||
|
login: this.login,
|
||||||
|
password: this.password
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.$refs.main.showSnackbar('Registration successful!', 'success')
|
||||||
|
} else {
|
||||||
|
this.$refs.main.showSnackbar(response.data.message, 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
49
frontend/src/crafty/Game.coffee
Normal file
49
frontend/src/crafty/Game.coffee
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# base scene
|
||||||
|
require '../crafty/scenes/Battle.coffee'
|
||||||
|
Crafty.scene "Battle"
|
||||||
|
|
||||||
|
# annoying shantotto
|
||||||
|
Crafty.sprite 480, 670, '//www.fftcgmognet.com/images/cards/hd/1/1/107.jpg',
|
||||||
|
shantotto: [
|
||||||
|
0
|
||||||
|
0
|
||||||
|
]
|
||||||
|
|
||||||
|
# create cards
|
||||||
|
require '../crafty/components/Card.coffee'
|
||||||
|
|
||||||
|
Crafty.e 'shantotto, AllyCard'
|
||||||
|
.attr {
|
||||||
|
card:
|
||||||
|
type: 'backup'
|
||||||
|
}
|
||||||
|
|
||||||
|
Crafty.e 'shantotto, AllyCard'
|
||||||
|
.attr {
|
||||||
|
card:
|
||||||
|
type: 'backup'
|
||||||
|
}
|
||||||
|
|
||||||
|
Crafty.e 'shantotto, AllyCard'
|
||||||
|
.attr {
|
||||||
|
card:
|
||||||
|
type: 'backup'
|
||||||
|
}
|
||||||
|
|
||||||
|
# place cards
|
||||||
|
CONF = require '../crafty/config.coffee'
|
||||||
|
|
||||||
|
Crafty 'AllyCard'
|
||||||
|
.each (index) ->
|
||||||
|
switch @card.type
|
||||||
|
when 'backup'
|
||||||
|
@trigger 'Place',
|
||||||
|
x: CONF.coord.x.main + index * CONF.coord.x.step
|
||||||
|
y: CONF.coord.y.bkup
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
Crafty.e 'shantotto, EnemyCard'
|
||||||
|
.trigger 'Place',
|
||||||
|
x: 900
|
||||||
|
y: 0
|
|
@ -1,3 +1,4 @@
|
||||||
|
CONF = require '../config.coffee'
|
||||||
# intermediate config
|
# intermediate config
|
||||||
bcConf = Crafty.clone CONF.bigcard
|
bcConf = Crafty.clone CONF.bigcard
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
CONF = require '../config.coffee'
|
||||||
require './BigCard.coffee'
|
require './BigCard.coffee'
|
||||||
|
|
||||||
################
|
################
|
|
@ -1,3 +1,5 @@
|
||||||
|
CONF = require '../config.coffee'
|
||||||
|
|
||||||
################
|
################
|
||||||
# Playmat
|
# Playmat
|
||||||
################
|
################
|
|
@ -1,4 +1,4 @@
|
||||||
window.CONF =
|
module.exports =
|
||||||
|
|
||||||
playmat:
|
playmat:
|
||||||
w: 2000
|
w: 2000
|
|
@ -1,3 +1,4 @@
|
||||||
|
CONF = require '../config.coffee'
|
||||||
require '../components/Playmat.coffee'
|
require '../components/Playmat.coffee'
|
||||||
|
|
||||||
Crafty.defineScene "Battle", ->
|
Crafty.defineScene "Battle", ->
|
||||||
|
@ -32,7 +33,7 @@ Crafty.defineScene "Battle", ->
|
||||||
Crafty.trigger 'ViewportResize'
|
Crafty.trigger 'ViewportResize'
|
||||||
|
|
||||||
# Example playmats at https://imgur.com/a/VSosu#cwGQdAS
|
# Example playmats at https://imgur.com/a/VSosu#cwGQdAS
|
||||||
Crafty.sprite 2000, 1000, 'assets/ff7.jpg',
|
Crafty.sprite 2000, 1000, require('@/assets/ff7.jpg'),
|
||||||
playmat: [
|
playmat: [
|
||||||
0
|
0
|
||||||
0
|
0
|
14
frontend/src/main.js
Normal file
14
frontend/src/main.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './plugins/vuetify'
|
||||||
|
import './plugins/vue-async-computed'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import 'roboto-fontface/css/roboto/roboto-fontface.css'
|
||||||
|
import 'material-design-icons-iconfont/dist/material-design-icons.css'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
5
frontend/src/plugins/axios.js
Normal file
5
frontend/src/plugins/axios.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
axios.defaults.baseURL =
|
||||||
|
window.location.protocol + '//' + window.location.hostname + ':3001'
|
||||||
|
export default axios
|
9
frontend/src/plugins/ffdecks.js
Normal file
9
frontend/src/plugins/ffdecks.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import oldCards from './ffdecks.json'
|
||||||
|
|
||||||
|
let CardsDB = {}
|
||||||
|
|
||||||
|
for (var i = 0; i < oldCards.cards.length; i++) {
|
||||||
|
CardsDB[oldCards.cards[i].serial_number] = oldCards.cards[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardsDB
|
1
frontend/src/plugins/ffdecks.json
Normal file
1
frontend/src/plugins/ffdecks.json
Normal file
File diff suppressed because one or more lines are too long
4
frontend/src/plugins/vue-async-computed.js
Normal file
4
frontend/src/plugins/vue-async-computed.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import AsyncComputed from 'vue-async-computed'
|
||||||
|
|
||||||
|
Vue.use(AsyncComputed)
|
22
frontend/src/plugins/vuetify.js
Normal file
22
frontend/src/plugins/vuetify.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuetify from 'vuetify/lib'
|
||||||
|
import 'vuetify/src/stylus/app.styl'
|
||||||
|
import en from 'vuetify/es5/locale/en'
|
||||||
|
|
||||||
|
Vue.use(Vuetify, {
|
||||||
|
theme: {
|
||||||
|
primary: '#ee44aa',
|
||||||
|
secondary: '#424242',
|
||||||
|
accent: '#82B1FF',
|
||||||
|
error: '#FF5252',
|
||||||
|
info: '#2196F3',
|
||||||
|
success: '#4CAF50',
|
||||||
|
warning: '#FFC107'
|
||||||
|
},
|
||||||
|
// customProperties: true,
|
||||||
|
iconfont: 'md',
|
||||||
|
lang: {
|
||||||
|
locales: { en },
|
||||||
|
current: 'en'
|
||||||
|
}
|
||||||
|
})
|
43
frontend/src/router.js
Normal file
43
frontend/src/router.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Router from 'vue-router'
|
||||||
|
import Home from './views/Home.vue'
|
||||||
|
|
||||||
|
Vue.use(Router)
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/usercp',
|
||||||
|
name: 'usercp',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (usercp.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "usercp" */ './views/UserCP.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/deckcp',
|
||||||
|
name: 'deckcp',
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "deckcp" */ './views/DeckCP.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/games',
|
||||||
|
name: 'games',
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "games" */ './views/Games.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/game',
|
||||||
|
name: 'game',
|
||||||
|
component: () => import(/* webpackChunkName: "game" */ './views/Game.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
29
frontend/src/views/DeckCP.vue
Normal file
29
frontend/src/views/DeckCP.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<v-content>
|
||||||
|
<HeaderIntern v-model="session" @user="user = $event" />
|
||||||
|
|
||||||
|
<v-container v-if="user">
|
||||||
|
<h2 class="headline">Your Decks</h2>
|
||||||
|
<DeckList :session="session" />
|
||||||
|
</v-container>
|
||||||
|
</v-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderIntern from '@/components/HeaderIntern.vue'
|
||||||
|
import DeckList from '@/components/DeckList.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DeckCP',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
HeaderIntern,
|
||||||
|
DeckList
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
32
frontend/src/views/Game.vue
Normal file
32
frontend/src/views/Game.vue
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<template lang="html">
|
||||||
|
<div ref="game" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Game',
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(function() {
|
||||||
|
try {
|
||||||
|
// framework
|
||||||
|
require('craftyjs/dist/crafty')
|
||||||
|
|
||||||
|
// initialize on ref="game"
|
||||||
|
window.Crafty.init(this.$refs.game)
|
||||||
|
window.Crafty.stage.fullscreen = true
|
||||||
|
|
||||||
|
// load fftcg
|
||||||
|
require('@/crafty/Game.coffee')
|
||||||
|
} catch (e) {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.Crafty.stop(true)
|
||||||
|
delete window.Crafty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
29
frontend/src/views/Games.vue
Normal file
29
frontend/src/views/Games.vue
Normal 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>
|
53
frontend/src/views/Home.vue
Normal file
53
frontend/src/views/Home.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<v-content>
|
||||||
|
<v-container>
|
||||||
|
<Header>
|
||||||
|
<LoginForm :session="session" />
|
||||||
|
<RegisterForm />
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<p class="subheading font-weight-regular">
|
||||||
|
App under development, please don't submit any valuable data!
|
||||||
|
</p>
|
||||||
|
</v-container>
|
||||||
|
</v-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as Cookies from 'js-cookie'
|
||||||
|
import axios from '@/plugins/axios'
|
||||||
|
|
||||||
|
import Header from '@/components/Header.vue'
|
||||||
|
import LoginForm from '@/components/forms/Login.vue'
|
||||||
|
import RegisterForm from '@/components/forms/Register.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Home',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Header,
|
||||||
|
LoginForm,
|
||||||
|
RegisterForm
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
session: () => Cookies.get('session')
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.session) {
|
||||||
|
axios
|
||||||
|
.post('/user/login', {
|
||||||
|
session: this.session
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.$router.push({ name: 'deckcp' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
29
frontend/src/views/UserCP.vue
Normal file
29
frontend/src/views/UserCP.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<v-content>
|
||||||
|
<HeaderIntern v-model="session" @user="user = $event" />
|
||||||
|
|
||||||
|
<v-container v-if="user">
|
||||||
|
<h2 class="headline">User Info</h2>
|
||||||
|
<UserInfo :session="session" :user="user" />
|
||||||
|
</v-container>
|
||||||
|
</v-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderIntern from '@/components/HeaderIntern.vue'
|
||||||
|
import UserInfo from '@/components/UserInfo.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserCP',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
HeaderIntern,
|
||||||
|
UserInfo
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
session: null,
|
||||||
|
user: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
5
frontend/tests/unit/.eslintrc.js
Normal file
5
frontend/tests/unit/.eslintrc.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
jest: true
|
||||||
|
}
|
||||||
|
}
|
12
frontend/tests/unit/example.spec.js
Normal file
12
frontend/tests/unit/example.spec.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils'
|
||||||
|
import HelloWorld from '@/components/HelloWorld.vue'
|
||||||
|
|
||||||
|
describe('HelloWorld.vue', () => {
|
||||||
|
it('renders props.msg when passed', () => {
|
||||||
|
const msg = 'new message'
|
||||||
|
const wrapper = shallowMount(HelloWorld, {
|
||||||
|
propsData: { msg }
|
||||||
|
})
|
||||||
|
expect(wrapper.text()).toMatch(msg)
|
||||||
|
})
|
||||||
|
})
|
10434
frontend/yarn.lock
Normal file
10434
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,11 +0,0 @@
|
||||||
FFTCGLOG = (unit) ->
|
|
||||||
@unit = unit
|
|
||||||
return
|
|
||||||
|
|
||||||
FFTCGLOG::log = (msg) ->
|
|
||||||
console.log "[#{@unit}] #{msg}"
|
|
||||||
|
|
||||||
FFTCGLOG::error = (msg) ->
|
|
||||||
console.error "[#{@unit}] #{msg}"
|
|
||||||
|
|
||||||
module.exports = FFTCGLOG
|
|
222
inc/db.coffee
222
inc/db.coffee
|
@ -1,222 +0,0 @@
|
||||||
# libraries
|
|
||||||
bcrypt = (require 'bcrypt')
|
|
||||||
sqlite3 = (require 'sqlite3').verbose()
|
|
||||||
FFTCGLOG = new (require './console')('FFTCGDB')
|
|
||||||
|
|
||||||
# bruteforce countermeasure
|
|
||||||
saltRounds = 13
|
|
||||||
|
|
||||||
FFTCGDB = (filename, truncate) ->
|
|
||||||
that = @
|
|
||||||
@filename = filename
|
|
||||||
|
|
||||||
@db = new sqlite3.Database @filename, (err) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.error err.message
|
|
||||||
|
|
||||||
else
|
|
||||||
FFTCGLOG.log "Connected to '#{that.filename}'"
|
|
||||||
|
|
||||||
that.db.run 'PRAGMA foreign_keys = ON;', (err) ->
|
|
||||||
FFTCGLOG.error err.message if err
|
|
||||||
|
|
||||||
if truncate == true
|
|
||||||
that.db.run 'DROP TABLE IF EXISTS users;', (err) ->
|
|
||||||
FFTCGLOG.error err.message if err
|
|
||||||
that.db.run '''
|
|
||||||
CREATE TABLE users (
|
|
||||||
user integer PRIMARY KEY,
|
|
||||||
login text NOT NULL COLLATE NOCASE,
|
|
||||||
pwdhash text NOT NULL,
|
|
||||||
UNIQUE(login)
|
|
||||||
);
|
|
||||||
''', (err) ->
|
|
||||||
FFTCGLOG.error err.message if err
|
|
||||||
|
|
||||||
that.db.run 'DROP TABLE IF EXISTS decks;', (err) ->
|
|
||||||
FFTCGLOG.error err.message if err
|
|
||||||
that.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) ->
|
|
||||||
FFTCGLOG.error err.message if err
|
|
||||||
|
|
||||||
FFTCGLOG.log 'recreated DB'
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
FFTCGDB::close = ->
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
@db.close (err) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "Error closing: '#{err.message}'"
|
|
||||||
resolve 'ok'
|
|
||||||
else
|
|
||||||
FFTCGLOG.error "Closed '#{@filename}'"
|
|
||||||
reject 'db'
|
|
||||||
|
|
||||||
FFTCGDB::register = (login, password) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
# validate user input
|
|
||||||
if login == '' or password == ''
|
|
||||||
# no user name or password given
|
|
||||||
FFTCGLOG.log "reg: user name '#{login}' or password empty"
|
|
||||||
reject 'invalid'
|
|
||||||
|
|
||||||
# hash password
|
|
||||||
bcrypt.hash password, saltRounds, (err, hash) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "reg: hash fail for name '#{login}'"
|
|
||||||
reject 'hash'
|
|
||||||
|
|
||||||
# try creating row in users table
|
|
||||||
stmt = that.db.prepare 'INSERT INTO users (login, pwdhash) VALUES (?, ?)'
|
|
||||||
stmt.run [login, hash], (err) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "reg: DB fail '#{err.code}' for name '#{login}'"
|
|
||||||
stmt.finalize()
|
|
||||||
# reduce attack surface, don't disclose user names
|
|
||||||
reject 'db' # user already exists
|
|
||||||
|
|
||||||
else
|
|
||||||
FFTCGLOG.log "reg: OK '#{login}'"
|
|
||||||
stmt.finalize()
|
|
||||||
# registration successful
|
|
||||||
resolve
|
|
||||||
user: @lastID
|
|
||||||
login: login
|
|
||||||
|
|
||||||
FFTCGDB::login = (login, password) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
# get users table row
|
|
||||||
stmt = that.db.prepare 'SELECT user, login, pwdhash FROM users WHERE login = ?'
|
|
||||||
stmt.get [login], (err, row) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "login: DB fail '#{err.code}' for name '#{login}'"
|
|
||||||
stmt.finalize()
|
|
||||||
reject 'db'
|
|
||||||
|
|
||||||
else if not row
|
|
||||||
# hash the password for timing attack reasons
|
|
||||||
bcrypt.hash password, saltRounds, (err, hash) ->
|
|
||||||
FFTCGLOG.log "login: nonexistent '#{login}'"
|
|
||||||
stmt.finalize()
|
|
||||||
# reduce attack surface, don't disclose user names
|
|
||||||
reject 'login' # user doesnt exist
|
|
||||||
|
|
||||||
else
|
|
||||||
bcrypt.compare password, row.pwdhash, (err, res) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "login: hash fail for name '#{login}'"
|
|
||||||
reject 'hash'
|
|
||||||
|
|
||||||
if res == true
|
|
||||||
FFTCGLOG.log "login: OK '#{row.login}'"
|
|
||||||
stmt.finalize()
|
|
||||||
# login successful
|
|
||||||
resolve
|
|
||||||
user: row.user
|
|
||||||
login: row.login
|
|
||||||
|
|
||||||
else
|
|
||||||
FFTCGLOG.log "login: wrong password for '#{login}'"
|
|
||||||
stmt.finalize()
|
|
||||||
# login failed
|
|
||||||
reject 'login'
|
|
||||||
|
|
||||||
FFTCGDB::addDeck = (user, deckCards) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
# try creating row in decks table
|
|
||||||
stmt = that.db.prepare 'INSERT INTO decks (user, json) VALUES (?, ?)'
|
|
||||||
stmt.run [user, JSON.stringify deckCards], (err) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "addDeck: DB fail '#{err.code}' for id '#{user}'"
|
|
||||||
stmt.finalize()
|
|
||||||
reject 'db'
|
|
||||||
|
|
||||||
else
|
|
||||||
stmt.finalize()
|
|
||||||
# deck added successfully, now add cards
|
|
||||||
that.modDeck(@lastID, deckCards)
|
|
||||||
.then (deckID) ->
|
|
||||||
resolve deckID
|
|
||||||
.catch (error) ->
|
|
||||||
reject error
|
|
||||||
|
|
||||||
FFTCGDB::modDeck = (deckID, deckCards) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
# delete old deck cards
|
|
||||||
stmt = that.db.prepare 'DELETE FROM decks_cards WHERE deck = ?'
|
|
||||||
stmt.run [deckID], (err) ->
|
|
||||||
stmt.finalize()
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "modDeck: DB fail '#{err.code}' for deck '#{deckID}'"
|
|
||||||
reject 'db'
|
|
||||||
else
|
|
||||||
stmt = that.db.prepare 'INSERT INTO decks_cards (deck, card, quant) VALUES (?, ?, ?)'
|
|
||||||
# add new cards
|
|
||||||
that.db.parallelize ->
|
|
||||||
# needs to be done in several queries
|
|
||||||
promiseCount = deckCards.length
|
|
||||||
deckCards.forEach (card) ->
|
|
||||||
stmt.run [deckID, card.id, card.quant], (err) ->
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "modDeck: DB fail '#{err.code}' for card '#{deckID}', '#{card.id}', '#{card.quant}'"
|
|
||||||
stmt.finalize()
|
|
||||||
reject 'db'
|
|
||||||
else
|
|
||||||
# check if all queries are done
|
|
||||||
promiseCount -= 1
|
|
||||||
if promiseCount == 0
|
|
||||||
FFTCGLOG.log "modDeck: OK '#{deckID}'"
|
|
||||||
stmt.finalize()
|
|
||||||
resolve deckID
|
|
||||||
|
|
||||||
FFTCGDB::getDecks = (user) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
# try deleting correct row in decks table
|
|
||||||
decks = {}
|
|
||||||
stmt = that.db.prepare 'SELECT decks.deck, decks.json FROM decks INNER JOIN users ON decks.user = users.user WHERE users.user = ?'
|
|
||||||
stmt.all [user], (err, rows) ->
|
|
||||||
stmt.finalize()
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "getDeck: DB fail '#{err.code}' for deck '#{deckID}'"
|
|
||||||
reject 'db'
|
|
||||||
else
|
|
||||||
FFTCGLOG.log "getDeck: OK '#{deckID}'"
|
|
||||||
for row in rows
|
|
||||||
decks[row.deck] = JSON.parse row.json
|
|
||||||
resolve decks
|
|
||||||
|
|
||||||
FFTCGDB::delDeck = (deckID) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
# try deleting correct row in decks table
|
|
||||||
stmt = that.db.prepare 'DELETE FROM decks WHERE deck = ?'
|
|
||||||
stmt.run [deckID], (err) ->
|
|
||||||
stmt.finalize()
|
|
||||||
if err
|
|
||||||
FFTCGLOG.log "delDeck: DB fail '#{err.code}' for deck '#{deckID}'"
|
|
||||||
reject 'db'
|
|
||||||
else
|
|
||||||
FFTCGLOG.log "delDeck: OK '#{deckID}'"
|
|
||||||
resolve deckID
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = FFTCGDB
|
|
|
@ -1,82 +0,0 @@
|
||||||
# node libraries
|
|
||||||
express = (require 'express')
|
|
||||||
path = (require 'path')
|
|
||||||
|
|
||||||
# my libraries
|
|
||||||
FFTCGDB = (require './db')
|
|
||||||
FFTCGLOG = new (require './console')('FFTCGROUTER')
|
|
||||||
|
|
||||||
# open fftcg db
|
|
||||||
fftcgdb = new FFTCGDB path.resolve(__dirname, '../fftcg.db')
|
|
||||||
|
|
||||||
# create router
|
|
||||||
FFTCGROUTER = express.Router()
|
|
||||||
|
|
||||||
# request logging
|
|
||||||
FFTCGROUTER.use (req, res, next) ->
|
|
||||||
if req.session.user
|
|
||||||
FFTCGLOG.log "user '#{req.session.user.login}' requested '#{req.url}'"
|
|
||||||
else
|
|
||||||
FFTCGLOG.log "requested '#{req.url}'"
|
|
||||||
|
|
||||||
next()
|
|
||||||
|
|
||||||
# static content
|
|
||||||
FFTCGROUTER.use express.static path.resolve(__dirname, '../public_html')
|
|
||||||
|
|
||||||
# register user
|
|
||||||
FFTCGROUTER.post '/register', (req, res) ->
|
|
||||||
fftcgdb.register req.body.login, req.body.password
|
|
||||||
.then (user) ->
|
|
||||||
# registration successful, return JSON status
|
|
||||||
res.json
|
|
||||||
status: 'ok'
|
|
||||||
user: user.user
|
|
||||||
login: user.login
|
|
||||||
|
|
||||||
.catch (err) ->
|
|
||||||
# registration failed, return JSON status
|
|
||||||
res.json
|
|
||||||
status: 'fail'
|
|
||||||
text: err
|
|
||||||
|
|
||||||
# log in user
|
|
||||||
FFTCGROUTER.post '/login', (req, res) ->
|
|
||||||
fftcgdb.login req.body.login, req.body.password
|
|
||||||
.then (user) ->
|
|
||||||
# login successful, save stuff in session
|
|
||||||
req.session.user = user
|
|
||||||
req.session.save()
|
|
||||||
|
|
||||||
# return JSON status
|
|
||||||
res.json
|
|
||||||
status: 'ok'
|
|
||||||
user: user.user
|
|
||||||
login: user.login
|
|
||||||
|
|
||||||
.catch (err) ->
|
|
||||||
# login failed, return JSON status
|
|
||||||
res.json
|
|
||||||
status: 'fail'
|
|
||||||
text: err
|
|
||||||
|
|
||||||
# Templates
|
|
||||||
FFTCGROUTER.get '/:template.html', (req, res) ->
|
|
||||||
# redirect logged-in users to user cp
|
|
||||||
if req.session.user and req.params.template == 'index'
|
|
||||||
return res.redirect '/usercp.html'
|
|
||||||
|
|
||||||
# render requested template
|
|
||||||
res.render (req.params.template + '.pug'), (err, html) ->
|
|
||||||
# redirect invalid requests to index
|
|
||||||
if err
|
|
||||||
return res.redirect '/index.html'
|
|
||||||
|
|
||||||
# actual response
|
|
||||||
res.send html
|
|
||||||
|
|
||||||
# default route
|
|
||||||
FFTCGROUTER.use (req, res) ->
|
|
||||||
return res.redirect '/index.html'
|
|
||||||
|
|
||||||
module.exports = FFTCGROUTER
|
|
|
@ -1,22 +0,0 @@
|
||||||
# node libraries
|
|
||||||
expressSession = (require 'express-session')
|
|
||||||
RedisStore = (require 'connect-redis')(expressSession)
|
|
||||||
|
|
||||||
module.exports = (app) ->
|
|
||||||
session =
|
|
||||||
secret: 'keyboard cat'
|
|
||||||
store: new RedisStore
|
|
||||||
host: 'redis'
|
|
||||||
port: 6379
|
|
||||||
cookie:
|
|
||||||
httpOnly: true
|
|
||||||
sameSite: 'strict'
|
|
||||||
proxy: true
|
|
||||||
resave: true
|
|
||||||
saveUninitialized: true
|
|
||||||
|
|
||||||
if app.get 'env' == 'production'
|
|
||||||
app.set 'trust proxy', 1
|
|
||||||
session.cookie.secure = true
|
|
||||||
|
|
||||||
expressSession session
|
|
|
@ -1,38 +0,0 @@
|
||||||
# node libraries
|
|
||||||
socketio = (require 'socket.io')
|
|
||||||
path = (require 'path')
|
|
||||||
FFTCGLOG = new (require './console')('FFTCGSOCKET')
|
|
||||||
|
|
||||||
# my libraries
|
|
||||||
|
|
||||||
FFTCGSOCKET = (http, session) ->
|
|
||||||
that = @
|
|
||||||
|
|
||||||
# create server socket
|
|
||||||
@io = socketio http
|
|
||||||
@io.use session
|
|
||||||
|
|
||||||
# on new connection
|
|
||||||
@io.on 'connection', (socket) ->
|
|
||||||
@session = socket.handshake.session
|
|
||||||
FFTCGLOG.log "session '#{@session.id}' connected"
|
|
||||||
FFTCGLOG.log "is user '#{@session.userID}'" if @session.userID
|
|
||||||
|
|
||||||
socket.on 'disconnect', ->
|
|
||||||
FFTCGLOG.log "session '#{that.session.id}' disconnected"
|
|
||||||
FFTCGLOG.log "is user '#{that.session.userID}'" if that.session.userID
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
FFTCGSOCKET::close = ->
|
|
||||||
FFTCGLOG.log 'shutting down'
|
|
||||||
if @db
|
|
||||||
@db.close()
|
|
||||||
.then (msg) ->
|
|
||||||
console.log msg
|
|
||||||
.catch (err) ->
|
|
||||||
console.error err
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = FFTCGSOCKET
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"verbose": true,
|
|
||||||
"watch": ["server.coffee", "inc/*"]
|
|
||||||
}
|
|
57
package.json
57
package.json
|
@ -1,57 +0,0 @@
|
||||||
{
|
|
||||||
"name": "node-fftcg",
|
|
||||||
"version": "0.0.3",
|
|
||||||
|
|
||||||
"description": "FFTCG online using Socket.IO and CraftyJS on Node.js on Docker",
|
|
||||||
"author": "JMM <jmm@yavook.de>",
|
|
||||||
|
|
||||||
"main": "server.coffee",
|
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack",
|
|
||||||
"watch": "webpack --watch",
|
|
||||||
"start": "webpack && coffee server.coffee",
|
|
||||||
"debug": "webpack --watch & nodemon server.coffee",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.1.6",
|
|
||||||
"@babel/preset-env": "^7.1.6",
|
|
||||||
"coffee-loader": "^0.9.0",
|
|
||||||
|
|
||||||
"autoprefixer": "^9.3.1",
|
|
||||||
"css-loader": "^1.0.1",
|
|
||||||
"postcss-loader": "^3.0.0",
|
|
||||||
"node-sass": "^4.10.0",
|
|
||||||
"precss": "^3.1.2",
|
|
||||||
"sass-loader": "^7.1.0",
|
|
||||||
"style-loader": "^0.23.1",
|
|
||||||
|
|
||||||
"nodemon": "^1.18.9",
|
|
||||||
"webpack": "^4.25.1",
|
|
||||||
"webpack-cli": "^3.1.2",
|
|
||||||
|
|
||||||
"bootstrap": "^4.1.3",
|
|
||||||
"craftyjs": "^0.9.0",
|
|
||||||
"jquery": "^3.3.1",
|
|
||||||
"popper.js": "^1.14.5"
|
|
||||||
},
|
|
||||||
|
|
||||||
"dependencies": {
|
|
||||||
"bcrypt": "^3.0.2",
|
|
||||||
"body-parser": "^1.18.3",
|
|
||||||
"coffeescript": "^2.3.2",
|
|
||||||
"connect-redis": "^3.4.0",
|
|
||||||
"express": "^4.16.4",
|
|
||||||
"express-session": "^1.15.6",
|
|
||||||
"express-socket.io-session": "^1.3.5",
|
|
||||||
"helmet": "^3.15.0",
|
|
||||||
"pug": "^2.0.3",
|
|
||||||
"socket.io": "^2.2.0",
|
|
||||||
"socket.io-client": "^2.2.0",
|
|
||||||
"sqlite3": "^4.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
# node libraries
|
|
||||||
bodyParser = (require 'body-parser')
|
|
||||||
express = (require 'express')
|
|
||||||
sharedSession = (require 'express-socket.io-session')
|
|
||||||
helmet = (require 'helmet')
|
|
||||||
http = (require 'http')
|
|
||||||
path = (require 'path')
|
|
||||||
|
|
||||||
# my libraries
|
|
||||||
FFTCGSOCKET = (require './inc/socket')
|
|
||||||
FFTCGSESSION = (require './inc/session')
|
|
||||||
FFTCGROUTER = (require './inc/router')
|
|
||||||
FFTCGLOG = new (require './inc/console')('FFTCG')
|
|
||||||
|
|
||||||
# express framework
|
|
||||||
app = express()
|
|
||||||
app.use helmet()
|
|
||||||
app.use bodyParser.urlencoded
|
|
||||||
extended: true
|
|
||||||
|
|
||||||
# sessions
|
|
||||||
sessionMiddleware = FFTCGSESSION(app)
|
|
||||||
app.use sessionMiddleware
|
|
||||||
|
|
||||||
# routes
|
|
||||||
app.use FFTCGROUTER
|
|
||||||
|
|
||||||
# socket.io
|
|
||||||
web = http.Server app
|
|
||||||
socket = new FFTCGSOCKET web, sharedSession sessionMiddleware
|
|
||||||
|
|
||||||
# Create server
|
|
||||||
web.listen 3000, ->
|
|
||||||
FFTCGLOG.log 'Listening on port 3000 ...'
|
|
||||||
|
|
||||||
# Handle termination
|
|
||||||
process.on 'SIGINT', ->
|
|
||||||
socket.close()
|
|
||||||
FFTCGLOG.log 'shutting down after SIGINT'
|
|
||||||
process.exit()
|
|
|
@ -1,71 +0,0 @@
|
||||||
# libs
|
|
||||||
window.$ = require('jquery')
|
|
||||||
|
|
||||||
# on load
|
|
||||||
$ ->
|
|
||||||
# libs requiring full DOM
|
|
||||||
require 'craftyjs/dist/crafty'
|
|
||||||
io = require 'socket.io-client'
|
|
||||||
|
|
||||||
# style sheet
|
|
||||||
require './style/custom.scss'
|
|
||||||
|
|
||||||
# fftcg libs
|
|
||||||
require './game/config.coffee'
|
|
||||||
require './game/components/Card.coffee'
|
|
||||||
require './game/scenes/Battle.coffee'
|
|
||||||
|
|
||||||
# init Socket.IO
|
|
||||||
socket = io()
|
|
||||||
|
|
||||||
# init CraftyJS framework
|
|
||||||
Crafty.init()
|
|
||||||
|
|
||||||
# Load base scene
|
|
||||||
Crafty.scene "Battle"
|
|
||||||
|
|
||||||
# Testing some entities
|
|
||||||
Crafty.sprite 480, 670, '//www.fftcgmognet.com/images/cards/hd/1/1/107.jpg',
|
|
||||||
shantotto: [
|
|
||||||
0
|
|
||||||
0
|
|
||||||
]
|
|
||||||
|
|
||||||
backups = [
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, AllyCard'
|
|
||||||
.attr {
|
|
||||||
card:
|
|
||||||
type: 'backup'
|
|
||||||
}
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, AllyCard'
|
|
||||||
.attr {
|
|
||||||
card:
|
|
||||||
type: 'backup'
|
|
||||||
}
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, AllyCard'
|
|
||||||
.attr {
|
|
||||||
card:
|
|
||||||
type: 'backup'
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
Crafty 'AllyCard'
|
|
||||||
.each (index) ->
|
|
||||||
switch @card.type
|
|
||||||
when 'backup'
|
|
||||||
@trigger 'Place',
|
|
||||||
x: CONF.coord.x.main + index * CONF.coord.x.step
|
|
||||||
y: CONF.coord.y.bkup
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, EnemyCard'
|
|
||||||
.trigger 'Place',
|
|
||||||
x: 900
|
|
||||||
y: 0
|
|
||||||
|
|
||||||
return
|
|
105
src/index.coffee
105
src/index.coffee
|
@ -1,105 +0,0 @@
|
||||||
# libs
|
|
||||||
window.$ = require('jquery')
|
|
||||||
|
|
||||||
# import bootstrap
|
|
||||||
require './style/custom.scss'
|
|
||||||
require 'bootstrap/js/dist/alert'
|
|
||||||
|
|
||||||
window.showAlert = (level, content) ->
|
|
||||||
($ '.alert').alert 'close'
|
|
||||||
|
|
||||||
($ '#alert-area').append ($ '<div>',
|
|
||||||
class: "alert alert-#{level} alert-dismissible fade show"
|
|
||||||
role: 'alert'
|
|
||||||
.append content, ($ '<button>',
|
|
||||||
type: 'button'
|
|
||||||
class: 'close'
|
|
||||||
'data-dismiss': 'alert',
|
|
||||||
'aria-label': 'Close'
|
|
||||||
.append ($ '<span>',
|
|
||||||
'aria-hidden': 'true'
|
|
||||||
.append '×'
|
|
||||||
)))
|
|
||||||
|
|
||||||
# on load
|
|
||||||
$ ->
|
|
||||||
# reset forms
|
|
||||||
$('form').each ->
|
|
||||||
@fullReset = ->
|
|
||||||
$('input', @).each ->
|
|
||||||
$(@).removeClass 'is-invalid'
|
|
||||||
$(@).removeClass 'is-valid'
|
|
||||||
@reset()
|
|
||||||
|
|
||||||
# login form
|
|
||||||
$('form[name="login"]').submit (event) ->
|
|
||||||
that = @
|
|
||||||
# inhibit normal form submission
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
# gather form data
|
|
||||||
login = $('input[name="login"]', @)
|
|
||||||
password = $('input[name="password"]', @)
|
|
||||||
|
|
||||||
# transmit form data
|
|
||||||
$.post '/login',
|
|
||||||
login: login.val()
|
|
||||||
password: password.val()
|
|
||||||
.done (data) ->
|
|
||||||
if data.status == 'ok'
|
|
||||||
that.fullReset()
|
|
||||||
showAlert 'success', "successfully logged in '#{data.login}'"
|
|
||||||
location.reload()
|
|
||||||
|
|
||||||
else
|
|
||||||
switch data.text
|
|
||||||
when 'login'
|
|
||||||
showAlert 'warning', 'Invalid username and/or password.'
|
|
||||||
login.addClass 'is-invalid'
|
|
||||||
password.addClass 'is-invalid'
|
|
||||||
when 'db' or 'hash'
|
|
||||||
showAlert 'danger', 'Internal failure, try again later.'
|
|
||||||
else
|
|
||||||
showAlert 'danger', 'Unknown failure. Can you reproduce this?'
|
|
||||||
|
|
||||||
# registration form
|
|
||||||
$('form[name="register"]').submit (event) ->
|
|
||||||
that = @
|
|
||||||
# inhibit normal form submission
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
# gather form data
|
|
||||||
login = $('input[name="login"]', @)
|
|
||||||
password = $('input[name="password"]', @)
|
|
||||||
confirm = $('input[name="confirm"]', @)
|
|
||||||
|
|
||||||
# check form data
|
|
||||||
if password.val() != confirm.val()
|
|
||||||
confirm.addClass 'is-invalid'
|
|
||||||
confirm.focus()
|
|
||||||
|
|
||||||
else
|
|
||||||
# transmit form data
|
|
||||||
$.post '/register',
|
|
||||||
login: login.val()
|
|
||||||
password: password.val()
|
|
||||||
.done (data) ->
|
|
||||||
if data.status == 'ok'
|
|
||||||
that.fullReset()
|
|
||||||
showAlert 'success', "successfully registered '#{data.login}'"
|
|
||||||
|
|
||||||
else
|
|
||||||
switch data.text
|
|
||||||
when 'invalid'
|
|
||||||
showAlert 'warning', 'Invalid user input. Please provide username AND password.'
|
|
||||||
login.addClass 'is-invalid'
|
|
||||||
password.addClass 'is-invalid'
|
|
||||||
login.focus()
|
|
||||||
when 'hash'
|
|
||||||
showAlert 'danger', 'Internal failure, try again later.'
|
|
||||||
when 'db'
|
|
||||||
showAlert 'danger', 'Internal failure or user name already taken.'
|
|
||||||
login.addClass 'is-invalid'
|
|
||||||
login.focus()
|
|
||||||
else
|
|
||||||
showAlert 'danger', 'Unknown failure. Can you reproduce this?'
|
|
|
@ -1,19 +0,0 @@
|
||||||
//
|
|
||||||
// Bootstrap and its default variables
|
|
||||||
//
|
|
||||||
|
|
||||||
@import "~bootstrap/scss/bootstrap";
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background-color: #aaa;
|
|
||||||
// background-image: url(//gameranx.com/wp-content/uploads/2016/02/Final-Fantasy-XV-4K-Wallpaper.jpg);
|
|
||||||
background-position-x: center;
|
|
||||||
background-position-y: center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
# libs
|
|
||||||
window.$ = require('jquery')
|
|
||||||
|
|
||||||
# import bootstrap
|
|
||||||
require './style/custom.scss'
|
|
||||||
require 'bootstrap/js/dist/alert'
|
|
||||||
require 'bootstrap/js/dist/collapse'
|
|
||||||
|
|
||||||
# on load
|
|
||||||
$ ->
|
|
||||||
return
|
|
|
@ -1,6 +0,0 @@
|
||||||
doctype html
|
|
||||||
html
|
|
||||||
head
|
|
||||||
title Crafty Things
|
|
||||||
script(src='/game.bundle.js')
|
|
||||||
body
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue