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:
|
||||
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:
|
||||
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
|
||||
bcConf = Crafty.clone CONF.bigcard
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
CONF = require '../config.coffee'
|
||||
require './BigCard.coffee'
|
||||
|
||||
################
|
|
@ -1,3 +1,5 @@
|
|||
CONF = require '../config.coffee'
|
||||
|
||||
################
|
||||
# Playmat
|
||||
################
|
|
@ -1,4 +1,4 @@
|
|||
window.CONF =
|
||||
module.exports =
|
||||
|
||||
playmat:
|
||||
w: 2000
|
|
@ -1,3 +1,4 @@
|
|||
CONF = require '../config.coffee'
|
||||
require '../components/Playmat.coffee'
|
||||
|
||||
Crafty.defineScene "Battle", ->
|
||||
|
@ -32,7 +33,7 @@ Crafty.defineScene "Battle", ->
|
|||
Crafty.trigger 'ViewportResize'
|
||||
|
||||
# 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: [
|
||||
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