# node libraries bcrypt = (require 'bcrypt') sqlite3 = (require 'sqlite3').verbose() logger = (require 'logging').default 'db' # bruteforce countermeasure saltRounds = 13 FFTCGDB = (filename, truncate) -> that = @ @filename = filename @db = new sqlite3.Database @filename, (err) -> if err logger.error err.message else logger.info "Connected to '#{that.filename}'" that.db.run 'PRAGMA foreign_keys = ON;', (err) -> logger.error err.message if err if truncate == true that.db.run 'DROP TABLE IF EXISTS users;', (err) -> logger.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, settings text, UNIQUE(login) ); ''', (err) -> logger.error err.message if err that.db.run 'DROP TABLE IF EXISTS decks;', (err) -> logger.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) -> logger.error err.message if err logger.info 'recreated sqlite3 db' return FFTCGDB::close = -> logger.info 'shutting down' new Promise (resolve, reject) -> @db.close (err) -> if err logger.error "Error closing: '#{err.message}'" resolve 'ok' else logger.warn "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 logger.info "reg: user name '#{login}' or password empty" reject 'invalid' # hash password bcrypt.hash password, saltRounds, (err, hash) -> if err logger.warn "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 logger.warn "reg: DB fail '#{err.code}' for name '#{login}'" stmt.finalize() # reduce attack surface, don't disclose user names reject 'db' # user already exists else logger.info "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 logger.warn "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) -> logger.debug "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 logger.warn "login: hash fail for name '#{login}'" reject 'hash' if res == true logger.debug "login: OK '#{row.login}'" stmt.finalize() # login successful resolve user: row.user login: row.login else logger.debug "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 logger.warn "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 logger.warn "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 logger.warn "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 logger.debug "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 logger.warn "getDeck: DB fail '#{err.code}' for deck '#{deckID}'" reject 'db' else logger.debug "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 logger.warn "delDeck: DB fail '#{err.code}' for deck '#{deckID}'" reject 'db' else logger.debug "delDeck: OK '#{deckID}'" resolve deckID module.exports = FFTCGDB