Compare commits
No commits in common. "develop" and "master" have entirely different histories.
102 changed files with 839 additions and 16675 deletions
19
.dockerignore
Normal file
19
.dockerignore
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# 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,3 +1 @@
|
||||||
**/node_modules
|
**/*.bundle.js
|
||||||
**/fftcg.db
|
|
||||||
**/org.vue.*.json
|
|
||||||
|
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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
40
Makefile
|
@ -1,40 +0,0 @@
|
||||||
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
|
|
|
@ -1,7 +0,0 @@
|
||||||
# node stuff
|
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
|
|
||||||
# Docker stuff
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
|
@ -1,8 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"extends": ["plugin:@fellow/coffee/recommended"],
|
|
||||||
"plugins": ["@fellow/coffee"],
|
|
||||||
"rules": {
|
|
||||||
"@fellow/coffee/indentation": ["error", { "value": 2 }],
|
|
||||||
"@fellow/coffee/colon-assignment-spacing": "off"
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
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" ]
|
|
|
@ -1,271 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,30 +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": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,26 +0,0 @@
|
||||||
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
|
|
|
@ -1,12 +0,0 @@
|
||||||
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'
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,24 +0,0 @@
|
||||||
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
|
|
|
@ -1,41 +0,0 @@
|
||||||
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
|
|
|
@ -1,24 +0,0 @@
|
||||||
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
|
|
|
@ -1,11 +0,0 @@
|
||||||
module.exports =
|
|
||||||
body:
|
|
||||||
session: type: 'string'
|
|
||||||
response:
|
|
||||||
200:
|
|
||||||
type: 'object'
|
|
||||||
required: ['success']
|
|
||||||
properties:
|
|
||||||
success:
|
|
||||||
type: 'boolean'
|
|
||||||
const: true
|
|
|
@ -1,22 +0,0 @@
|
||||||
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
|
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
|
@ -1,65 +0,0 @@
|
||||||
# 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()
|
|
|
@ -1,171 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,42 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,38 +0,0 @@
|
||||||
<!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
3169
backend/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -1,24 +0,0 @@
|
||||||
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"
|
|
|
@ -1,16 +0,0 @@
|
||||||
version: "2.3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
redis:
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
target: prod
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
target: prod
|
|
||||||
restart: unless-stopped
|
|
|
@ -1,16 +1,20 @@
|
||||||
version: "2.3"
|
version: "2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
fftcg:
|
||||||
|
build: .
|
||||||
|
command: "yarn debug"
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- "${PWD}/src:/app/src"
|
||||||
|
- "${PWD}/views:/app/views:ro"
|
||||||
|
- "${PWD}/public_html:/app/public_html"
|
||||||
|
- "${PWD}/inc:/app/inc:ro"
|
||||||
|
- "${PWD}/server.coffee:/app/server.coffee:ro"
|
||||||
|
# - "${PWD}/fftcg.db:/app/fftcg.db"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
restart: "no"
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
ports:
|
|
||||||
- "3001:3001"
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
|
|
BIN
fftcg.db
Normal file
BIN
fftcg.db
Normal file
Binary file not shown.
|
@ -1,3 +0,0 @@
|
||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not ie <= 8
|
|
|
@ -1,14 +0,0 @@
|
||||||
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
21
frontend/.gitignore
vendored
|
@ -1,21 +0,0 @@
|
||||||
.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*
|
|
|
@ -1,5 +0,0 @@
|
||||||
# .prettierrc
|
|
||||||
# trailingComma: "es5"
|
|
||||||
tabWidth: 2
|
|
||||||
semi: false
|
|
||||||
singleQuote: true
|
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
# 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/).
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: ['@vue/app']
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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/'
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,17 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-app>
|
|
||||||
<router-view />
|
|
||||||
</v-app>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'App'
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<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>
|
|
|
@ -1 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 539 B |
|
@ -1,212 +0,0 @@
|
||||||
'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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,71 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,155 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,85 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,116 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,134 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,57 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{ user.login }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'UserInfo',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
session: String,
|
|
||||||
user: Object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,118 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,99 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,75 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,92 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,49 +0,0 @@
|
||||||
# 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,14 +0,0 @@
|
||||||
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')
|
|
|
@ -1,5 +0,0 @@
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
axios.defaults.baseURL =
|
|
||||||
window.location.protocol + '//' + window.location.hostname + ':3001'
|
|
||||||
export default axios
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import AsyncComputed from 'vue-async-computed'
|
|
||||||
|
|
||||||
Vue.use(AsyncComputed)
|
|
|
@ -1,22 +0,0 @@
|
||||||
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'
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,43 +0,0 @@
|
||||||
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')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
|
@ -1,29 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,32 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,53 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
jest: true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
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
10434
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
11
inc/console.coffee
Normal file
11
inc/console.coffee
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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
Normal file
222
inc/db.coffee
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
# 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
|
82
inc/router.coffee
Normal file
82
inc/router.coffee
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# 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
|
22
inc/session.coffee
Normal file
22
inc/session.coffee
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# 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
|
38
inc/socket.coffee
Normal file
38
inc/socket.coffee
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# 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
|
4
nodemon.json
Normal file
4
nodemon.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"verbose": true,
|
||||||
|
"watch": ["server.coffee", "inc/*"]
|
||||||
|
}
|
57
package.json
Normal file
57
package.json
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 610 KiB After Width: | Height: | Size: 610 KiB |
Before Width: | Height: | Size: 615 KiB After Width: | Height: | Size: 615 KiB |
40
server.coffee
Normal file
40
server.coffee
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# 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()
|
71
src/game.coffee
Normal file
71
src/game.coffee
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# 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
|
|
@ -1,4 +1,3 @@
|
||||||
CONF = require '../config.coffee'
|
|
||||||
# intermediate config
|
# intermediate config
|
||||||
bcConf = Crafty.clone CONF.bigcard
|
bcConf = Crafty.clone CONF.bigcard
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
CONF = require '../config.coffee'
|
|
||||||
require './BigCard.coffee'
|
require './BigCard.coffee'
|
||||||
|
|
||||||
################
|
################
|
|
@ -1,5 +1,3 @@
|
||||||
CONF = require '../config.coffee'
|
|
||||||
|
|
||||||
################
|
################
|
||||||
# Playmat
|
# Playmat
|
||||||
################
|
################
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports =
|
window.CONF =
|
||||||
|
|
||||||
playmat:
|
playmat:
|
||||||
w: 2000
|
w: 2000
|
|
@ -1,4 +1,3 @@
|
||||||
CONF = require '../config.coffee'
|
|
||||||
require '../components/Playmat.coffee'
|
require '../components/Playmat.coffee'
|
||||||
|
|
||||||
Crafty.defineScene "Battle", ->
|
Crafty.defineScene "Battle", ->
|
||||||
|
@ -33,7 +32,7 @@ Crafty.defineScene "Battle", ->
|
||||||
Crafty.trigger 'ViewportResize'
|
Crafty.trigger 'ViewportResize'
|
||||||
|
|
||||||
# Example playmats at https://imgur.com/a/VSosu#cwGQdAS
|
# Example playmats at https://imgur.com/a/VSosu#cwGQdAS
|
||||||
Crafty.sprite 2000, 1000, require('@/assets/ff7.jpg'),
|
Crafty.sprite 2000, 1000, 'assets/ff7.jpg',
|
||||||
playmat: [
|
playmat: [
|
||||||
0
|
0
|
||||||
0
|
0
|
105
src/index.coffee
Normal file
105
src/index.coffee
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
# 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?'
|
19
src/style/custom.scss
Normal file
19
src/style/custom.scss
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// 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;
|
||||||
|
}
|
11
src/usercp.coffee
Normal file
11
src/usercp.coffee
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# libs
|
||||||
|
window.$ = require('jquery')
|
||||||
|
|
||||||
|
# import bootstrap
|
||||||
|
require './style/custom.scss'
|
||||||
|
require 'bootstrap/js/dist/alert'
|
||||||
|
require 'bootstrap/js/dist/collapse'
|
||||||
|
|
||||||
|
# on load
|
||||||
|
$ ->
|
||||||
|
return
|
6
views/game.pug
Normal file
6
views/game.pug
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
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