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 (
|
||||
login text NOT NULL COLLATE NOCASE,
|
||||
pwdhash text NOT NULL,
|
||||
session text,
|
||||
UNIQUE(login)
|
||||
);
|
||||
"""
|
||||
|
||||
console.log "[FFTCGDB] Connected to '#{@filename}'"
|
||||
return
|
||||
|
||||
|
@ -35,46 +35,74 @@ FFTCGDB::register = (login, password) ->
|
|||
that = @
|
||||
|
||||
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
|
||||
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.code == 'SQLITE_CONSTRAINT'
|
||||
reject 'existence'
|
||||
else
|
||||
reject 'db'
|
||||
console.log "[FFTCGDB] 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
|
||||
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
|
||||
console.log "[FFTCGDB] reg: OK '#{login}'"
|
||||
stmt.finalize()
|
||||
# registration successful
|
||||
resolve @lastID
|
||||
resolve
|
||||
uid: @lastID
|
||||
login: login
|
||||
|
||||
FFTCGDB::login = (login, password) ->
|
||||
that = @
|
||||
|
||||
new Promise (resolve, reject) ->
|
||||
# validate username
|
||||
|
||||
# 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
|
||||
console.log "[FFTCGDB] login: DB fail '#{err.code}' for name '#{login}'"
|
||||
stmt.finalize()
|
||||
reject 'db'
|
||||
|
||||
else if rows.length == 0
|
||||
# hashing the password for timing attack reasons
|
||||
else if not row
|
||||
# hash the password for timing attack reasons
|
||||
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
|
||||
row = rows[0]
|
||||
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
|
||||
resolve row.rowid
|
||||
console.log "[FFTCGDB] login: OK '#{row.login}'"
|
||||
stmt.finalize()
|
||||
# login successful
|
||||
resolve
|
||||
uid: row.rowid
|
||||
login: row.login
|
||||
|
||||
else
|
||||
console.log "[FFTCGDB] login: wrong password for '#{login}'"
|
||||
stmt.finalize()
|
||||
# login failed
|
||||
reject 'login'
|
||||
|
||||
|
||||
module.exports = FFTCGDB
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# node libraries
|
||||
FFTCGROUTER = (require 'express').Router()
|
||||
express = (require 'express')
|
||||
path = (require 'path')
|
||||
|
||||
# my libraries
|
||||
|
@ -8,17 +8,22 @@ FFTCGDB = (require './fftcgdb')
|
|||
# open 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
|
||||
FFTCGROUTER.post '/register', (req, res) ->
|
||||
fftcgdb.register req.body.login, req.body.password
|
||||
.then (userid) ->
|
||||
console.log "registered '#{req.body.login}'"
|
||||
.then (user) ->
|
||||
# registration successful, return JSON status
|
||||
res.json
|
||||
status: 'ok'
|
||||
uid: userid
|
||||
text: req.body.login
|
||||
uid: user.id
|
||||
login: user.login
|
||||
|
||||
.catch (err) ->
|
||||
console.log "failed to register '#{req.body.login}'"
|
||||
# registration failed, return JSON status
|
||||
res.json
|
||||
status: 'fail'
|
||||
text: err
|
||||
|
@ -26,17 +31,19 @@ FFTCGROUTER.post '/register', (req, res) ->
|
|||
# log in user
|
||||
FFTCGROUTER.post '/login', (req, res) ->
|
||||
fftcgdb.login req.body.login, req.body.password
|
||||
.then (userid) ->
|
||||
req.session.userID = userid
|
||||
req.session.userLogin = req.body.login
|
||||
.then (user) ->
|
||||
# login successful, save stuff in session
|
||||
req.session.user = user
|
||||
req.session.save()
|
||||
console.log "logged in '#{req.body.login}'"
|
||||
|
||||
# return JSON status
|
||||
res.json
|
||||
status: 'ok'
|
||||
uid: userid
|
||||
text: req.body.login
|
||||
uid: user.uid
|
||||
login: user.login
|
||||
|
||||
.catch (err) ->
|
||||
console.log "failed to login '#{req.body.login}'"
|
||||
# login failed, return JSON status
|
||||
res.json
|
||||
status: 'fail'
|
||||
text: err
|
||||
|
|
|
@ -21,17 +21,14 @@ app.use bodyParser.urlencoded
|
|||
sessionMiddleware = FFTCGSESSION(app)
|
||||
app.use sessionMiddleware
|
||||
|
||||
# REST routes
|
||||
# routes
|
||||
app.use FFTCGROUTER
|
||||
|
||||
# Static content
|
||||
app.use express.static path.resolve(__dirname, 'public_html')
|
||||
|
||||
# Templates
|
||||
app.set 'view engine', 'pug'
|
||||
app.get '/:template.html', (req, res) ->
|
||||
if req.session
|
||||
console.log "logged in as '#{req.session.userLogin}'"
|
||||
if req.session.user
|
||||
console.log "[FFTCG] user is '#{req.session.user.login}'"
|
||||
res.render (req.params.template + '.pug')
|
||||
|
||||
# socket.io
|
||||
|
|
|
@ -1,23 +1,39 @@
|
|||
# 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
|
||||
$ ->
|
||||
# style sheet
|
||||
require './style/custom.scss'
|
||||
|
||||
# reset forms
|
||||
$('form').each ->
|
||||
that = @
|
||||
|
||||
@fullReset = ->
|
||||
$('input', that).each ->
|
||||
$(that).removeClass 'is-invalid'
|
||||
$(that).removeClass 'is-valid'
|
||||
that.reset()
|
||||
$('input', @).each ->
|
||||
$(@).removeClass 'is-invalid'
|
||||
$(@).removeClass 'is-valid'
|
||||
@reset()
|
||||
|
||||
# login form
|
||||
$('form[name="login"]').submit (event) ->
|
||||
that = @
|
||||
# inhibit normal form submission
|
||||
event.preventDefault()
|
||||
|
||||
|
@ -30,13 +46,24 @@ $ ->
|
|||
login: login.val()
|
||||
password: password.val()
|
||||
.done (data) ->
|
||||
alert "#{data.status}, #{data.uid}, #{data.text}"
|
||||
if data.status == 'ok'
|
||||
that.fullReset()
|
||||
showAlert 'success', "successfully logged in '#{data.login}'"
|
||||
|
||||
# reset form
|
||||
@fullReset()
|
||||
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()
|
||||
|
||||
|
@ -46,19 +73,32 @@ $ ->
|
|||
confirm = $('input[name="confirm"]', @)
|
||||
|
||||
# check form data
|
||||
if password.val() == confirm.val()
|
||||
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) ->
|
||||
alert "#{data.status}, #{data.uid}, #{data.text}"
|
||||
|
||||
# reset form
|
||||
@fullReset()
|
||||
if data.status == 'ok'
|
||||
that.fullReset()
|
||||
showAlert 'success', "successfully registered '#{data.login}'"
|
||||
|
||||
else
|
||||
confirm.val ''
|
||||
confirm.addClass 'is-invalid'
|
||||
confirm.focus()
|
||||
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?'
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Bootstrap and its default variables
|
||||
//
|
||||
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
|
|
|
@ -10,9 +10,10 @@ html
|
|||
header.jumbotron.jumbotron-fluid.py-4.bg-primary.text-light.text-center
|
||||
div.container
|
||||
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#alert-area
|
||||
div.row
|
||||
|
||||
div.col-md-6
|
||||
|
@ -39,12 +40,14 @@ html
|
|||
div.form-group
|
||||
label(for="login") User name:
|
||||
input.form-control(name="login" required)
|
||||
div.invalid-feedback User name not available.
|
||||
div.invalid-feedback User name invalid or taken.
|
||||
|
||||
div.form-group
|
||||
label(for="password") Password:
|
||||
input.form-control(name="password" type="password" required)
|
||||
|
||||
label(for="confirm") Confirm password:
|
||||
input.form-control(name="confirm" type="password")
|
||||
div.invalid-feedback Passwords do not match.
|
||||
|
||||
button.btn.btn-primary.w-100(type="submit") Register
|
||||
|
|
Reference in a new issue