usable authentication and session management
This commit is contained in:
parent
4f143edc23
commit
1f08303947
6 changed files with 136 additions and 61 deletions
|
@ -16,10 +16,10 @@ FFTCGDB = (filename) ->
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
login text NOT NULL COLLATE NOCASE,
|
login text NOT NULL COLLATE NOCASE,
|
||||||
pwdhash text NOT NULL,
|
pwdhash text NOT NULL,
|
||||||
session text,
|
|
||||||
UNIQUE(login)
|
UNIQUE(login)
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
console.log "[FFTCGDB] Connected to '#{@filename}'"
|
console.log "[FFTCGDB] Connected to '#{@filename}'"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -35,46 +35,74 @@ FFTCGDB::register = (login, password) ->
|
||||||
that = @
|
that = @
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
new Promise (resolve, reject) ->
|
||||||
# validate username
|
# validate user input
|
||||||
|
if login == '' or password == ''
|
||||||
|
# no user name or password given
|
||||||
|
console.log "[FFTCGDB] reg: user name '#{login}' or password empty"
|
||||||
|
reject 'invalid'
|
||||||
|
|
||||||
# hash password
|
# hash password
|
||||||
bcrypt.hash password, saltRounds, (err, hash) ->
|
bcrypt.hash password, saltRounds, (err, hash) ->
|
||||||
reject 'hash' if err
|
|
||||||
# try creating row in users table
|
|
||||||
that.db.run "INSERT INTO users (login, pwdhash) VALUES ('#{login}', '#{hash}');", (err) ->
|
|
||||||
if err
|
if err
|
||||||
if err.code == 'SQLITE_CONSTRAINT'
|
console.log "[FFTCGDB] reg: hash fail for name '#{login}'"
|
||||||
reject 'existence'
|
reject 'hash'
|
||||||
else
|
|
||||||
reject 'db'
|
# try creating row in users table
|
||||||
|
stmt = that.db.prepare 'INSERT INTO users (login, pwdhash) VALUES (?, ?)'
|
||||||
|
stmt.run [login, hash], (err) ->
|
||||||
|
if err
|
||||||
|
console.log "[FFTCGDB] reg: DB fail '#{err.code}' for name '#{login}'"
|
||||||
|
stmt.finalize()
|
||||||
|
# reduce attack surface, don't disclose user names
|
||||||
|
reject 'db' # user already exists
|
||||||
|
|
||||||
else
|
else
|
||||||
|
console.log "[FFTCGDB] reg: OK '#{login}'"
|
||||||
|
stmt.finalize()
|
||||||
# registration successful
|
# registration successful
|
||||||
resolve @lastID
|
resolve
|
||||||
|
uid: @lastID
|
||||||
|
login: login
|
||||||
|
|
||||||
FFTCGDB::login = (login, password) ->
|
FFTCGDB::login = (login, password) ->
|
||||||
that = @
|
that = @
|
||||||
|
|
||||||
new Promise (resolve, reject) ->
|
new Promise (resolve, reject) ->
|
||||||
# validate username
|
|
||||||
|
|
||||||
# get users table row
|
# get users table row
|
||||||
that.db.all "SELECT rowid, pwdhash FROM users WHERE login = '#{login}';", (err, rows) ->
|
stmt = that.db.prepare 'SELECT rowid, login, pwdhash FROM users WHERE login = ?'
|
||||||
|
stmt.get [login], (err, row) ->
|
||||||
if err
|
if err
|
||||||
|
console.log "[FFTCGDB] login: DB fail '#{err.code}' for name '#{login}'"
|
||||||
|
stmt.finalize()
|
||||||
reject 'db'
|
reject 'db'
|
||||||
|
|
||||||
else if rows.length == 0
|
else if not row
|
||||||
# hashing the password for timing attack reasons
|
# hash the password for timing attack reasons
|
||||||
bcrypt.hash password, saltRounds, (err, hash) ->
|
bcrypt.hash password, saltRounds, (err, hash) ->
|
||||||
reject 'existence'
|
console.log "[FFTCGDB] login: nonexistent '#{login}'"
|
||||||
|
stmt.finalize()
|
||||||
|
# reduce attack surface, don't disclose user names
|
||||||
|
reject 'login' # user doesnt exist
|
||||||
|
|
||||||
else
|
else
|
||||||
row = rows[0]
|
|
||||||
bcrypt.compare password, row.pwdhash, (err, res) ->
|
bcrypt.compare password, row.pwdhash, (err, res) ->
|
||||||
reject 'hash' if err
|
if err
|
||||||
|
console.log "[FFTCGDB] login: hash fail for name '#{login}'"
|
||||||
|
reject 'hash'
|
||||||
|
|
||||||
if res == true
|
if res == true
|
||||||
resolve row.rowid
|
console.log "[FFTCGDB] login: OK '#{row.login}'"
|
||||||
|
stmt.finalize()
|
||||||
|
# login successful
|
||||||
|
resolve
|
||||||
|
uid: row.rowid
|
||||||
|
login: row.login
|
||||||
|
|
||||||
else
|
else
|
||||||
|
console.log "[FFTCGDB] login: wrong password for '#{login}'"
|
||||||
|
stmt.finalize()
|
||||||
|
# login failed
|
||||||
reject 'login'
|
reject 'login'
|
||||||
|
|
||||||
|
|
||||||
module.exports = FFTCGDB
|
module.exports = FFTCGDB
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# node libraries
|
# node libraries
|
||||||
FFTCGROUTER = (require 'express').Router()
|
express = (require 'express')
|
||||||
path = (require 'path')
|
path = (require 'path')
|
||||||
|
|
||||||
# my libraries
|
# my libraries
|
||||||
|
@ -8,17 +8,22 @@ FFTCGDB = (require './fftcgdb')
|
||||||
# open fftcg db
|
# open fftcg db
|
||||||
fftcgdb = new FFTCGDB path.resolve(__dirname, '../fftcg.db')
|
fftcgdb = new FFTCGDB path.resolve(__dirname, '../fftcg.db')
|
||||||
|
|
||||||
|
# create router
|
||||||
|
FFTCGROUTER = express.Router()
|
||||||
|
FFTCGROUTER.use express.static path.resolve(__dirname, '../public_html')
|
||||||
|
|
||||||
# register user
|
# register user
|
||||||
FFTCGROUTER.post '/register', (req, res) ->
|
FFTCGROUTER.post '/register', (req, res) ->
|
||||||
fftcgdb.register req.body.login, req.body.password
|
fftcgdb.register req.body.login, req.body.password
|
||||||
.then (userid) ->
|
.then (user) ->
|
||||||
console.log "registered '#{req.body.login}'"
|
# registration successful, return JSON status
|
||||||
res.json
|
res.json
|
||||||
status: 'ok'
|
status: 'ok'
|
||||||
uid: userid
|
uid: user.id
|
||||||
text: req.body.login
|
login: user.login
|
||||||
|
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.log "failed to register '#{req.body.login}'"
|
# registration failed, return JSON status
|
||||||
res.json
|
res.json
|
||||||
status: 'fail'
|
status: 'fail'
|
||||||
text: err
|
text: err
|
||||||
|
@ -26,17 +31,19 @@ FFTCGROUTER.post '/register', (req, res) ->
|
||||||
# log in user
|
# log in user
|
||||||
FFTCGROUTER.post '/login', (req, res) ->
|
FFTCGROUTER.post '/login', (req, res) ->
|
||||||
fftcgdb.login req.body.login, req.body.password
|
fftcgdb.login req.body.login, req.body.password
|
||||||
.then (userid) ->
|
.then (user) ->
|
||||||
req.session.userID = userid
|
# login successful, save stuff in session
|
||||||
req.session.userLogin = req.body.login
|
req.session.user = user
|
||||||
req.session.save()
|
req.session.save()
|
||||||
console.log "logged in '#{req.body.login}'"
|
|
||||||
|
# return JSON status
|
||||||
res.json
|
res.json
|
||||||
status: 'ok'
|
status: 'ok'
|
||||||
uid: userid
|
uid: user.uid
|
||||||
text: req.body.login
|
login: user.login
|
||||||
|
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.log "failed to login '#{req.body.login}'"
|
# login failed, return JSON status
|
||||||
res.json
|
res.json
|
||||||
status: 'fail'
|
status: 'fail'
|
||||||
text: err
|
text: err
|
||||||
|
|
|
@ -21,17 +21,14 @@ app.use bodyParser.urlencoded
|
||||||
sessionMiddleware = FFTCGSESSION(app)
|
sessionMiddleware = FFTCGSESSION(app)
|
||||||
app.use sessionMiddleware
|
app.use sessionMiddleware
|
||||||
|
|
||||||
# REST routes
|
# routes
|
||||||
app.use FFTCGROUTER
|
app.use FFTCGROUTER
|
||||||
|
|
||||||
# Static content
|
|
||||||
app.use express.static path.resolve(__dirname, 'public_html')
|
|
||||||
|
|
||||||
# Templates
|
# Templates
|
||||||
app.set 'view engine', 'pug'
|
app.set 'view engine', 'pug'
|
||||||
app.get '/:template.html', (req, res) ->
|
app.get '/:template.html', (req, res) ->
|
||||||
if req.session
|
if req.session.user
|
||||||
console.log "logged in as '#{req.session.userLogin}'"
|
console.log "[FFTCG] user is '#{req.session.user.login}'"
|
||||||
res.render (req.params.template + '.pug')
|
res.render (req.params.template + '.pug')
|
||||||
|
|
||||||
# socket.io
|
# socket.io
|
||||||
|
|
|
@ -1,23 +1,39 @@
|
||||||
# libs
|
# libs
|
||||||
window.$ = require('jquery')
|
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
|
# on load
|
||||||
$ ->
|
$ ->
|
||||||
# style sheet
|
|
||||||
require './style/custom.scss'
|
|
||||||
|
|
||||||
# reset forms
|
# reset forms
|
||||||
$('form').each ->
|
$('form').each ->
|
||||||
that = @
|
|
||||||
|
|
||||||
@fullReset = ->
|
@fullReset = ->
|
||||||
$('input', that).each ->
|
$('input', @).each ->
|
||||||
$(that).removeClass 'is-invalid'
|
$(@).removeClass 'is-invalid'
|
||||||
$(that).removeClass 'is-valid'
|
$(@).removeClass 'is-valid'
|
||||||
that.reset()
|
@reset()
|
||||||
|
|
||||||
# login form
|
# login form
|
||||||
$('form[name="login"]').submit (event) ->
|
$('form[name="login"]').submit (event) ->
|
||||||
|
that = @
|
||||||
# inhibit normal form submission
|
# inhibit normal form submission
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
@ -30,13 +46,24 @@ $ ->
|
||||||
login: login.val()
|
login: login.val()
|
||||||
password: password.val()
|
password: password.val()
|
||||||
.done (data) ->
|
.done (data) ->
|
||||||
alert "#{data.status}, #{data.uid}, #{data.text}"
|
if data.status == 'ok'
|
||||||
|
that.fullReset()
|
||||||
|
showAlert 'success', "successfully logged in '#{data.login}'"
|
||||||
|
|
||||||
# reset form
|
else
|
||||||
@fullReset()
|
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
|
# registration form
|
||||||
$('form[name="register"]').submit (event) ->
|
$('form[name="register"]').submit (event) ->
|
||||||
|
that = @
|
||||||
# inhibit normal form submission
|
# inhibit normal form submission
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
@ -46,19 +73,32 @@ $ ->
|
||||||
confirm = $('input[name="confirm"]', @)
|
confirm = $('input[name="confirm"]', @)
|
||||||
|
|
||||||
# check form data
|
# check form data
|
||||||
if password.val() == confirm.val()
|
if password.val() != confirm.val()
|
||||||
|
confirm.addClass 'is-invalid'
|
||||||
|
confirm.focus()
|
||||||
|
|
||||||
|
else
|
||||||
# transmit form data
|
# transmit form data
|
||||||
$.post '/register',
|
$.post '/register',
|
||||||
login: login.val()
|
login: login.val()
|
||||||
password: password.val()
|
password: password.val()
|
||||||
.done (data) ->
|
.done (data) ->
|
||||||
alert "#{data.status}, #{data.uid}, #{data.text}"
|
if data.status == 'ok'
|
||||||
|
that.fullReset()
|
||||||
# reset form
|
showAlert 'success', "successfully registered '#{data.login}'"
|
||||||
@fullReset()
|
|
||||||
|
|
||||||
else
|
else
|
||||||
confirm.val ''
|
switch data.text
|
||||||
confirm.addClass 'is-invalid'
|
when 'invalid'
|
||||||
confirm.focus()
|
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?'
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// Bootstrap and its default variables
|
// Bootstrap and its default variables
|
||||||
//
|
//
|
||||||
|
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -10,9 +10,10 @@ html
|
||||||
header.jumbotron.jumbotron-fluid.py-4.bg-primary.text-light.text-center
|
header.jumbotron.jumbotron-fluid.py-4.bg-primary.text-light.text-center
|
||||||
div.container
|
div.container
|
||||||
h1 Hello World!
|
h1 Hello World!
|
||||||
h2 App under development, please don't send valuable data!
|
h2 App under development, please don't submit any valuable data!
|
||||||
|
|
||||||
div.container.bg-light
|
div.container.bg-light
|
||||||
|
div#alert-area
|
||||||
div.row
|
div.row
|
||||||
|
|
||||||
div.col-md-6
|
div.col-md-6
|
||||||
|
@ -39,12 +40,14 @@ html
|
||||||
div.form-group
|
div.form-group
|
||||||
label(for="login") User name:
|
label(for="login") User name:
|
||||||
input.form-control(name="login" required)
|
input.form-control(name="login" required)
|
||||||
div.invalid-feedback User name not available.
|
div.invalid-feedback User name invalid or taken.
|
||||||
|
|
||||||
div.form-group
|
div.form-group
|
||||||
label(for="password") Password:
|
label(for="password") Password:
|
||||||
input.form-control(name="password" type="password" required)
|
input.form-control(name="password" type="password" required)
|
||||||
|
|
||||||
label(for="confirm") Confirm password:
|
label(for="confirm") Confirm password:
|
||||||
input.form-control(name="confirm" type="password")
|
input.form-control(name="confirm" type="password")
|
||||||
|
div.invalid-feedback Passwords do not match.
|
||||||
|
|
||||||
button.btn.btn-primary.w-100(type="submit") Register
|
button.btn.btn-primary.w-100(type="submit") Register
|
||||||
|
|
Reference in a new issue