server: add websocket module and http module

websocket
ALI Hamza 2019-10-25 06:17:07 +07:00
parent 28fbf77529
commit 81623db66a
No known key found for this signature in database
GPG Key ID: BCA8A46C87798C4C
19 changed files with 1371 additions and 2 deletions

@ -0,0 +1,29 @@
package common
import "time"
//Visitor struct represents the customer who comes to the IT
type Visitor struct {
Email string `json:"email"`
Name string `json:"name"`
FirstTicket int `json:"first_ticket"`
}
//Staff struct represents a staff member who operates the queuing system
type Staff struct {
Username string `json:"username"`
Name string `json:"name"`
Email string `json:"email"`
Hash string `json:"password"`
Admin bool `json:"admin"`
}
//Ticket represents the item that visitors and staff interact with for each visit
type Ticket struct {
ID int `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Staff string `json:"staff"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
}

@ -0,0 +1,58 @@
package db
import (
"database/sql"
// Import the SQLite library we're going to use so SQL can use it.
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
var alreadyUsingMemory bool
func init() {
openDatabase("./database.sqlite")
}
func openDatabase(fileLocation string) {
db, _ = sql.Open("sqlite3", fileLocation)
db.SetMaxOpenConns(1)
// Initializes the tables
db.Exec(`CREATE TABLE IF NOT EXISTS tickets (
id INTEGER NOT NULL UNIQUE PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
name TEXT NOT NULL,
staff TEXT NOT NULL,
time_start DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,
time_end DATE)`)
db.Exec(`CREATE TABLE IF NOT EXISTS clients (
email TEXT NOT NULL UNIQUE PRIMARY KEY,
name TEXT NOT NULL,
first_ticket INT NOT NULL)`)
db.Exec(`CREATE TABLE IF NOT EXISTS staff (
username TEXT NOT NULL UNIQUE PRIMARY KEY,
password TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
admin BIT NOT NULL)`)
}
//UseInMemoryDatabase requests DB to be ran in memory instead of a file which can be useful during testing.
//Creates a (seemingly) new DB every time by dropping all tables.
func UseInMemoryDatabase() {
if db != nil {
if alreadyUsingMemory {
dropAllTables()
}
db.Close()
}
openDatabase("file::memory:?cache=shared")
alreadyUsingMemory = true
}
func dropAllTables() {
db.Exec("DROP TABLE tickets; DROP TABLE clients; DROP TABLE staff;")
}

@ -0,0 +1,160 @@
package db
import (
"JISQueueing/common"
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"log"
"golang.org/x/crypto/bcrypt"
)
//LoginFailReason specifies either the reason that a login request had failed or a value to indicate that it had succeeded.
type LoginFailReason uint8
//RegisterFailReason specifies either the reason that a registration request had failed or a value to indicate that it had succeeded.
type RegisterFailReason uint8
//BcryptCost is the cost of the password hash we use. A larger value means that it is harder to break but takes longer to compute on the server too.
const BcryptCost = 10
var ErrTokenNotFound = errors.New("the provided token does not belong to any staff member")
const (
//InvalidUser indicates that the provided username could not be found in the database.
InvalidUser LoginFailReason = iota
//InvalidPassword indicates that the provided password was incorrect.
InvalidPassword
//InvalidInternalPasswordHash indicates that the validation of password hash that was stored in database had failed.
InvalidInternalPasswordHash
//ReloginRequest indicates that the login was successful but the user is already logged in.
ReloginRequest
//LoginSuccessful indicates that the login was successful.
LoginSuccessful
)
const (
//UsernameAlreadyExists indicates that the provided username was already registered.
UsernameAlreadyExists RegisterFailReason = iota
//RegisterSuccessful indicates that the registration was successful.
RegisterSuccessful
)
var (
staffToHash = make(map[common.Staff]string)
hashToStaff = make(map[string]common.Staff)
encoder = base64.StdEncoding
)
//Login attempts to log a user in. It takes in username and password, processes the request and returns LoginFailReason.
func Login(username string, password string) (string, LoginFailReason) {
results, _ := db.Query("SELECT * FROM staff WHERE username=? COLLATE NOCASE", username)
defer results.Close()
//No results?
if !results.Next() {
return "", InvalidUser
}
staff := convertIntoStaff(results)
result := bcrypt.CompareHashAndPassword([]byte(staff.Hash), []byte(password))
if result == bcrypt.ErrMismatchedHashAndPassword {
return "", InvalidPassword
}
if result != nil {
log.Printf("[WARN] An internal password hash is invalid: user=%s, hash=%s", username, staff.Hash)
return "", InvalidInternalPasswordHash
}
token, loggedInAlready := newToken(staff)
if loggedInAlready {
return token, ReloginRequest
}
return token, LoginSuccessful
}
func Register(staff common.Staff, password string) RegisterFailReason {
token, loginFailReason := Login(staff.Username, password) //Attempt to login as the user to make sure it doesn't exist.
if loginFailReason == LoginSuccessful || loginFailReason == ReloginRequest {
if loginFailReason == LoginSuccessful {
Logout(token)
}
return UsernameAlreadyExists
}
if loginFailReason != InvalidUser {
//Oops. The user already exists!
return UsernameAlreadyExists
}
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
staff.Hash = string(hashedPassword)
db.Exec("INSERT INTO staff (username, password, name, email, admin) VALUES (?, ?, ?, ?, ?)",
staff.Username, staff.Hash, staff.Name, staff.Email, staff.Admin)
return RegisterSuccessful
}
//Logout logs out the user with the token.
func Logout(token string) bool {
if hashToStaff[token] == (common.Staff{}) {
return false
}
delete(staffToHash, hashToStaff[token])
delete(hashToStaff, token)
return true
}
//GetTokenOwner returns the owner of a given token.
func GetTokenOwner(token string) (common.Staff, error) {
if _, ok := hashToStaff[token]; ok {
return hashToStaff[token], nil
}
return common.Staff{}, ErrTokenNotFound
}
//VerifyToken checks if a given token is valid.
func VerifyToken(token string) bool {
_, ok := hashToStaff[token]
return ok
}
//newToken creates or returns a previously cached token for the given staff member. The second return value is a bool that is true if the member had already been logged in previously.
func newToken(staff common.Staff) (string, bool) {
if staffToHash[staff] != "" {
return staffToHash[staff], true
}
tokenString := ""
ok := true
//Keep looping if the token already exists. Shouldn't happen too often.
for ok {
token := make([]byte, 18) //18 bytes is 24 characters of base64
_, err := rand.Read(token)
if err != nil {
panic(err) //Having no longer random bits is worse than the service not working.
}
tokenString = encoder.EncodeToString(token)
_, ok = hashToStaff[tokenString]
}
staffToHash[staff] = tokenString
hashToStaff[tokenString] = staff
return tokenString, false
}
func convertIntoStaff(rows *sql.Rows) common.Staff {
var username string
var hash string
var name string
var email string
var admin bool
rows.Scan(&username, &hash, &name, &email, &admin)
return common.Staff{
Username: username,
Name: name,
Hash: hash,
Admin: admin,
}
}

@ -0,0 +1,155 @@
package db_test
import (
"JISQueueing/common"
"JISQueueing/db"
"testing"
)
func TestRegister(t *testing.T) {
db.UseInMemoryDatabase()
staff := exampleStaffUser()
result := db.Register(staff, "test123")
if result != db.RegisterSuccessful {
t.Errorf("Registration should be successful. Got: %d", result)
}
result = db.Register(staff, "test123")
if result != db.UsernameAlreadyExists {
t.Errorf("Registration with the same name should fail. Got: %d", result)
}
result = db.Register(staff, "test456")
if result != db.UsernameAlreadyExists {
t.Errorf("Registration with the same name should fail. Got: %d", result)
}
result = db.Register(staff, "test123")
if result != db.UsernameAlreadyExists {
t.Errorf("Registration with the same name should fail. Got: %d", result)
}
token, _ := db.Login(staff.Username, "test123")
result = db.Register(staff, "test123")
if result != db.UsernameAlreadyExists {
t.Errorf("Registration with same username should fail. Got: %d", result)
}
if !db.VerifyToken(token) {
t.Error("Registering a logged-in user logs the user out!")
}
db.Logout(token)
}
func TestLogin(t *testing.T) {
db.UseInMemoryDatabase()
staff1 := exampleStaffUser()
staff2 := exampleStaffUser2()
staff3 := exampleStaffUser3()
db.Register(staff1, "test123")
db.Register(staff2, "test456")
token1, result := db.Login(staff1.Username, "test123")
if result != db.LoginSuccessful {
t.Errorf("Login should be successful. Got: %d", result)
}
token2, result := db.Login(staff2.Username, "test456")
if result != db.LoginSuccessful {
t.Errorf("Login should be successful. Got: %d", result)
}
_, result = db.Login(staff1.Username, "test456")
if result != db.InvalidPassword {
t.Errorf("Password should be invalid. Got: %d", result)
}
_, result = db.Login(staff3.Username, "test123")
if result != db.InvalidUser {
t.Errorf("User should be invalid. Got: %d", result)
}
db.Register(staff3, "test789")
token3, result := db.Login(staff3.Username, "test789")
if result != db.LoginSuccessful {
t.Errorf("Login should be successful. Got: %d", result)
}
newToken1, result := db.Login(staff1.Username, "test123")
if result != db.ReloginRequest {
t.Errorf("DB should acknowledge login as a relogin. Got %d instead.", result)
}
if newToken1 != token1 {
t.Errorf("Old token doesn't match new token! old=%s, new=%s", token1, newToken1)
}
newToken2, result := db.Login(staff2.Username, "test456")
if result != db.ReloginRequest {
t.Errorf("DB should acknowledge login as a relogin. Got %d instead.", result)
}
if newToken2 != token2 {
t.Errorf("Old token doesn't match new token! old=%s, new=%s", token2, newToken2)
}
newToken3, result := db.Login(staff3.Username, "test789")
if result != db.ReloginRequest {
t.Errorf("DB should acknowledge login as a relogin. Got %d instead.", result)
}
if newToken3 != token3 {
t.Errorf("Old token doesn't match new token! old=%s, new=%s", token3, newToken3)
}
// Logout everyone and make sure they are no longer acknowledged as relogin requests.
if !db.Logout(token1) {
t.Errorf("Failed to log out a valid token!")
}
if !db.Logout(token2) {
t.Errorf("Failed to log out a valid token!")
}
if !db.Logout(token3) {
t.Errorf("Failed to log out a valid token!")
}
token1, result = db.Login(staff1.Username, "test123")
if result != db.LoginSuccessful {
t.Errorf("Login should be successful. Got: %d", result)
}
token2, result = db.Login(staff2.Username, "test456")
if result != db.LoginSuccessful {
t.Errorf("Login should be successful. Got: %d", result)
}
token3, result = db.Login(staff3.Username, "test789")
if result != db.LoginSuccessful {
t.Errorf("Login should be successful. Got: %d", result)
}
// Logout everyone again so we have a consistent token cache.
if !db.Logout(token1) {
t.Errorf("Failed to log out a valid token!")
}
if !db.Logout(token2) {
t.Errorf("Failed to log out a valid token!")
}
if !db.Logout(token3) {
t.Errorf("Failed to log out a valid token!")
}
}
func exampleStaffUser() common.Staff {
return common.Staff{
Username: "test",
Name: "Test Member",
Email: "test@example.com",
Admin: false,
}
}
func exampleStaffUser2() common.Staff {
return common.Staff{
Username: "test2",
Name: "Test Member2",
Email: "test2@example.com",
Admin: false,
}
}
func exampleStaffUser3() common.Staff {
return common.Staff {
Username: "test3",
Name: "Test Member3",
Email: "test3@example.com",
Admin: false,
}
}

@ -0,0 +1,126 @@
package db
import (
"JISQueueing/common"
"database/sql"
"errors"
"time"
)
//ErrTicketNotFound indicates that the provided ticket with the id does not belong to any tickets
var ErrTicketNotFound = errors.New("ticket id does not exist")
func GetAllTickets() []common.Ticket {
rows, _ := db.Query("SELECT * FROM tickets")
defer rows.Close()
return convertIntoTickets(rows)
}
//GetTicketsByEmail queries all tickets that a specific client has made.
func GetTicketsByEmail(email string) []common.Ticket {
rows, _ := db.Query("SELECT * FROM tickets WHERE email=? COLLATE NOCASE", email)
defer rows.Close()
return convertIntoTickets(rows)
}
func GetTicket(id int) (common.Ticket, error) {
rows, _ := db.Query("SELECT * FROM tickets where id=?", id)
defer rows.Close()
tickets := convertIntoTickets(rows)
if len(tickets) == 0 {
return common.Ticket{}, ErrTicketNotFound
}
return tickets[0], nil
}
//NewTicket generates a ticket with the current timestamp and inserts it into the database.
func NewTicket(visitor common.Visitor) common.Ticket {
now := time.Now().UTC()
result, _ := db.Exec("INSERT INTO tickets (email, name, staff, time_start, time_end) VALUES (?, ?, ?, ?, ?)",
visitor.Email, visitor.Name, "{}", now, time.Unix(0, 0).UTC())
lastInsertID, _ := result.LastInsertId()
return common.Ticket{
ID: int(lastInsertID),
Email: visitor.Email,
Name: visitor.Name,
Staff: "{}",
Start: now,
End: time.Unix(0, 0).UTC(),
}
}
//SetClaimer updates the database with the staff user who claims a ticket.
func SetClaimer(id int, username string) bool {
ticket, ok := GetTicket(id)
if ok != nil || ticket.Staff != "{}" {
return false
}
result, _ := db.Exec("UPDATE tickets SET staff=? WHERE id=?", username, id)
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0
}
//FinishTicket marks a ticket as complete in the database.
func FinishTicket(id int) bool {
if !isActive(id) {
return false
}
result, _ := db.Exec("UPDATE tickets SET time_end=? WHERE id=?", time.Now().UTC(), id)
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0
}
//Finish Ticket marks a ticket as cancelled if the user decides to cancel it or does not show up.
func CancelTicket(id int) bool {
if !isActive(id) {
return false
}
result, _ := db.Exec("UPDATE tickets SET time_start=? WHERE id=?", time.Unix(0, 0), id)
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0
}
func isActive(id int) bool {
ticket, ok := GetTicket(id)
if ok != nil {
return false
}
if ticket.Start.Unix() == 0 || ticket.End.Unix() != 0 {
println("start", ticket.Start.Unix(),"end", ticket.End.Unix())
return false
}
return true
}
//convertIntoTickets converts the result set from a sql query of a rows of visitors into a []common.Visitor.
func convertIntoTickets(rows *sql.Rows) []common.Ticket {
tickets := make([]common.Ticket, 0)
for rows.Next() {
var id int
var email string
var name string
var staff string
var start time.Time
var end time.Time
rows.Scan(&id, &email, &name, &staff, &start, &end)
ticket := common.Ticket{
ID: id,
Email: email,
Name: name,
Staff: staff,
Start: start,
End: end,
}
tickets = append(tickets, ticket)
}
return tickets
}

@ -0,0 +1,61 @@
package db_test
import (
"JISQueueing/db"
"testing"
"time"
)
func TestNewTicket(t *testing.T) {
db.UseInMemoryDatabase()
db.NewVisitor(exampleVisitor())
ticket := db.NewTicket(exampleVisitor())
if ticket.Email != exampleVisitor().Email || ticket.Staff != "{}" {
t.Errorf("Expected email and username to mach.")
}
newTicket, _ := db.GetTicket(ticket.ID)
if ticket != newTicket {
t.Errorf("Expected ticket to be present in database. returned %v, instead of %v", newTicket, ticket)
}
}
func TestSetClaimer(t *testing.T) {
db.UseInMemoryDatabase()
db.Register(exampleStaffUser(), "test123")
ticket := db.NewTicket(exampleVisitor())
if !db.SetClaimer(ticket.ID, exampleStaffUser().Username) {
t.Errorf("Expected ticket to update claimer successfully.")
}
if db.SetClaimer(ticket.ID, exampleStaffUser().Username) {
t.Errorf("Expected ticket's claimer to not update as already set")
}
}
func TestFinishTicket(t *testing.T) {
db.UseInMemoryDatabase()
ticket := db.NewTicket(exampleVisitor())
breakpoint := time.Date(2019, 10, 25, 0, 0, 0, 0, time.UTC)
if breakpoint.Before(ticket.End) {
t.Errorf("Expected ticket end time to be Unix 0, got %d instead.", ticket.End.Unix())
}
db.FinishTicket(ticket.ID)
ticket, _ = db.GetTicket(ticket.ID)
if breakpoint.After(ticket.End) {
t.Errorf("Expected ticket to get time updated after completion. got %d instead", ticket.End.Unix())
}
}
func TestCancelTicket(t *testing.T) {
db.UseInMemoryDatabase()
ticket := db.NewTicket(exampleVisitor())
db.CancelTicket(ticket.ID)
ticket, _ = db.GetTicket(ticket.ID)
if ticket.Start.Unix() != 0 {
t.Errorf("Expected ticket to get start time updated after cancel. got %d instead", ticket.Start.Unix())
}
}

@ -0,0 +1,77 @@
package db
import (
"JISQueueing/common"
"database/sql"
"errors"
)
//ErrVisitorNotFound indicates that the email does not belong to an existing visitor (new account).
var ErrVisitorNotFound = errors.New("visitor email does not exist")
//GetAllVisitors returns a list of all visitors in our record.
func GetAllVisitors() []common.Visitor {
rows, _ := db.Query("SELECT * FROM clients")
defer rows.Close()
return convertIntoVisitors(rows)
}
//GetVisitor retrieves the visitor with the specified ID in the form of `common.Visitor`.
func GetVisitor(email string) (common.Visitor, error) {
rows, _ := db.Query("SELECT * FROM clients WHERE email=?", email)
defer rows.Close()
visitors := convertIntoVisitors(rows)
if len(visitors) == 0 {
return common.Visitor{}, ErrVisitorNotFound
}
return visitors[0], nil
}
//NewVisitor saves the provided visitor and returns whether the email is new (first time) or already used (old, and update name).
func NewVisitor(visitor common.Visitor) (bool, common.Visitor) {
exists, editVisitor := EditVisitor(visitor)
if exists {
return false, editVisitor
}
result, _ := db.Exec("INSERT INTO clients (email, name, first_ticket) VALUES (?, ?, ?)", visitor.Email, visitor.Name, -1)
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0, visitor
}
//EditVisitor updates the provided visitor with the new name associated with the email.
func EditVisitor(visitor common.Visitor) (bool, common.Visitor) {
result, _ := db.Exec("UPDATE clients SET name=? WHERE email=?", visitor.Name, visitor.Email)
rowsAffected, _ := result.RowsAffected()
updated, _ := GetVisitor(visitor.Email)
return rowsAffected > 0, updated
}
//SetFirstTicket updates the first ticket column for the provided visitor
func SetFirstTicket(visitor common.Visitor) bool {
result, _ := db.Exec("UPDATE clients SET first_ticket=? WHERE email=?", visitor.FirstTicket, visitor.Email)
rowsAffected, _ := result.RowsAffected()
return rowsAffected > 0
}
//convertIntoVisitors converts the result set from a sql query of a rows of visitors into a []common.Visitor.
func convertIntoVisitors(rows *sql.Rows) []common.Visitor {
visitors := make([]common.Visitor, 0)
for rows.Next() {
var email string
var name string
var firstTicket int
rows.Scan(&email, &name, &firstTicket)
visitor := common.Visitor{
Email: email,
Name: name,
FirstTicket: firstTicket,
}
visitors = append(visitors, visitor)
}
return visitors
}

@ -0,0 +1,102 @@
package db_test
import (
"JISQueueing/common"
"JISQueueing/db"
"testing"
)
func TestNewVisitor(t *testing.T) {
db.UseInMemoryDatabase()
db.NewVisitor(exampleVisitor())
db.NewVisitor(exampleVisitor2())
success, _ := db.NewVisitor(exampleDuplicateVisitor())
if success {
t.Error("Uploading duplicate email visitor caused return to be true, when expected false")
}
}
func TestGetVisitor(t *testing.T) {
db.UseInMemoryDatabase()
db.NewVisitor(exampleVisitor())
db.NewVisitor(exampleVisitor2())
visitor, err := db.GetVisitor(exampleVisitor().Email)
if err != nil {
t.Error("Expected visitor to be fetched correctly")
}
if visitor.Name != exampleVisitor().Name {
t.Errorf("Expected sample visitor to have %s as it's name, got %s instead", exampleVisitor().Name, visitor.Name)
}
visitor, err = db.GetVisitor(exampleVisitor2().Email)
if err != nil {
t.Error("Expected visitor to be fetched correctly")
}
if visitor.Name != exampleVisitor2().Name {
t.Errorf("Expected sample visitor to have %s as it's name, got %s instead", exampleVisitor2().Name, visitor.Name)
}
db.EditVisitor(exampleDuplicateVisitor())
visitor, err = db.GetVisitor(exampleDuplicateVisitor().Email)
if err != nil {
t.Error("Expected visitor to be fetched correctly")
}
if visitor.Name != exampleDuplicateVisitor().Name {
t.Errorf("Expected sample visitor to have %s as it's name, got %s instead", exampleDuplicateVisitor().Name, visitor.Name)
}
}
func TestEditVisitor(t *testing.T) {
db.UseInMemoryDatabase()
visitor := exampleVisitor()
db.NewVisitor(visitor)
visitor.Name = "New name"
success, _ := db.EditVisitor(visitor)
if !success {
t.Error("Expected visitor to be edited correctly")
}
}
func TestSetFirstTicket(t *testing.T) {
db.UseInMemoryDatabase()
visitor := exampleVisitor()
db.NewVisitor(visitor)
visitor.FirstTicket = 2
db.SetFirstTicket(visitor)
updated, err := db.GetVisitor(visitor.Email)
if err != nil {
t.Error("Expected visitor to be fetched correctly")
}
if updated.FirstTicket != 2 {
t.Error("Expected first ticket id to be updated correctly")
}
}
func exampleVisitor() common.Visitor {
return common.Visitor{
Email: "test@example.com",
Name: "Example Name",
}
}
func exampleVisitor2() common.Visitor {
return common.Visitor{
Email: "example@example.com",
Name: "John Doe",
}
}
func exampleDuplicateVisitor() common.Visitor {
return common.Visitor{
Email: "example@example.com",
Name: "Bob Smith",
}
}

@ -1,3 +1,10 @@
module gitea.teamortix.com/Team-Ortix/JISQueueing
module JISQueueing
go 1.12
require (
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1
github.com/mattn/go-sqlite3 v1.11.0
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
)

@ -0,0 +1,13 @@
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

@ -1,5 +1,22 @@
package main
import (
"JISQueueing/common"
"JISQueueing/server"
"flag"
"net/http"
"strconv"
)
var hashToStaff = make(map[string]common.Staff)
func main() {
//TODO: Populate with flags and calls to `http/`
var port int
var debug bool
flag.IntVar(&port, "port", 8080, "Specify port for socket and webserver")
flag.BoolVar(&debug, "debug", false, "Specify the debug mode status")
hostLocation := ":" + strconv.Itoa(port)
mux := server.NewServerMux(debug)
http.ListenAndServe(hostLocation, mux)
}

@ -0,0 +1,46 @@
package server
import (
"JISQueueing/db"
"net/http"
)
//Login handles api calls to /api/login
func Login(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
InvalidUserInput(w, r)
return
}
user := r.Form.Get("user")
pass := r.Form.Get("pass")
if user == "" || pass == "" {
InvalidUserInput(w, r)
return
}
token, result := db.Login(user, pass)
switch result {
case db.InvalidUser:
fallthrough
case db.InvalidPassword:
Unauthorized(w, r)
case db.InvalidInternalPasswordHash:
InternalServerError(w, r)
case db.ReloginRequest:
fallthrough
case db.LoginSuccessful:
writeJSONResponse(w, http.StatusOK, token)
}
}
//Logout handles api calls to /api/logout
func Logout(w http.ResponseWriter, r *http.Request) {
if !handleToken(w, r) {
return
}
db.Logout(r.Form.Get("token"))
writeJSONResponse(w, http.StatusOK, nil)
}

@ -0,0 +1,37 @@
package server
import (
"JISQueueing/server/socket"
"net/http"
"github.com/gorilla/mux"
)
//NewServerMux creates a new http handler object handling API requests
func NewServerMux(debug bool) http.Handler {
router := mux.NewRouter()
router.NotFoundHandler = http.HandlerFunc(NotFound)
postRouter := router.Methods("POST").PathPrefix("/api").Subrouter()
postRouter.HandleFunc("/login", Login)
postRouter.HandleFunc("/logout", Logout)
socketRouter := router.PathPrefix("/ws").Subrouter()
socketRouter.HandleFunc("/display", socket.DisplaySocket)
socketRouter.HandleFunc("/kiosk", socket.KioskSocket)
socketRouter.HandleFunc("/staff", socket.StaffSocket)
router.PathPrefix("/api").Handler(http.HandlerFunc(NotFound))
router.PathPrefix("/assets").Handler(http.FileServer(http.Dir("./static/")))
router.PathPrefix("/css").Handler(http.FileServer(http.Dir("./static/")))
//NotFound : A helper function that responds with a 404 status code and an error
router.PathPrefix("/js").Handler(http.FileServer(http.Dir("./static/")))
router.PathPrefix("/").HandlerFunc(indexHandler)
return router
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/index.html")
}

@ -0,0 +1,24 @@
package socket
import (
"github.com/gorilla/websocket"
)
var displays = make(map[*websocket.Conn]bool)
func onNewDisplay(who *websocket.Conn) {
displays[who] = true
}
func onDisplayMessage(who *websocket.Conn, msg string) {
}
func onDisplayDisconnect(who *websocket.Conn) {
delete(displays, who)
}
func sendDisplayMessage(msg string) {
for display := range displays {
display.WriteMessage(websocket.TextMessage, []byte(msg))
}
}

@ -0,0 +1,79 @@
package socket
import (
"JISQueueing/common"
"JISQueueing/db"
"encoding/json"
"github.com/gorilla/websocket"
"strconv"
)
var tickets = make(map[common.Ticket]bool)
func newTicket(ticket common.Ticket, who *websocket.Conn) {
tickets[ticket] = false
jsonTicket, _ := json.Marshal(ticket)
sendStaffMessage("new " + string(jsonTicket))
who.WriteMessage(websocket.TextMessage, []byte("success new "+string(jsonTicket)))
}
func claimedTicket(ticket common.Ticket, staff common.Staff, table int, who *websocket.Conn) {
if !db.SetClaimer(ticket.ID, staff.Username) {
who.WriteMessage(websocket.TextMessage, []byte("error accept the ticket has already been completed or cancelled"))
return
}
delete(tickets, ticket)
ticket.Staff = staff.Username
tickets[ticket] = true
jsonTicket, _ := json.Marshal(ticket)
sendDisplayMessage("claimed " + staff.Username + " " + strconv.Itoa(table) + " " + string(jsonTicket))
who.WriteMessage(websocket.TextMessage, []byte("success claimed "+string(jsonTicket)))
for conn, s := range connToStaff {
if s.Username != staff.Username {
conn.WriteMessage(websocket.TextMessage, []byte("claimed "+string(jsonTicket)))
}
}
}
func finishedTicket(ticket common.Ticket, id int, table int, who *websocket.Conn) {
delete(tickets, ticket)
if success := db.FinishTicket(id); !success {
who.WriteMessage(websocket.TextMessage, []byte("error complete the ticket has already been completed or cancelled"))
return
}
who.WriteMessage(websocket.TextMessage, []byte("success complete "+strconv.Itoa(id)))
sendDisplayMessage("complete " + strconv.Itoa(table))
}
func cancelTicket(ticket common.Ticket, id int, table int, who *websocket.Conn) {
delete(tickets, ticket)
if success := db.CancelTicket(id); !success {
who.WriteMessage(websocket.TextMessage, []byte("error cancel the ticket has already been completed or cancelled"))
return
}
who.WriteMessage(websocket.TextMessage, []byte("success cancel "+strconv.Itoa(id)))
sendDisplayMessage("cancel " + strconv.Itoa(table))
}
func newStaff(table int, who *websocket.Conn) {
who.WriteMessage(websocket.TextMessage, []byte("success pick "+strconv.Itoa(table)))
for member, num := range connToTable {
if num == -1 {
sendTaken(member)
}
}
sendDisplayMessage("pick " + strconv.Itoa(table))
}
func leaveStaff(table int) {
sendDisplayMessage("unpick " + strconv.Itoa(table))
for member, num := range connToTable {
if num == -1 {
sendTaken(member)
}
}
}

@ -0,0 +1,49 @@
package socket
import (
"JISQueueing/common"
"JISQueueing/db"
"github.com/gorilla/websocket"
"strings"
)
func onKioskMessage(who *websocket.Conn, msg string) {
message := strings.SplitN(msg, " ", 2)
switch message[0] {
case "query":
if len(message) < 2 {
return
}
email := strings.TrimSpace(message[1])
visitor, err := db.GetVisitor(email)
if err == db.ErrVisitorNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error query "+err.Error()))
return
}
who.WriteMessage(websocket.TextMessage, []byte("success query "+visitor.Name))
case "new":
arg := strings.SplitN(msg, " ", 3)
if len(arg) < 3 {
return
}
_, visitor := db.NewVisitor(common.Visitor{
Email: strings.TrimSpace(arg[1]),
Name: strings.TrimSpace(arg[2]),
FirstTicket: -1,
})
newTicket(db.NewTicket(visitor), who)
default:
who.WriteMessage(websocket.TextMessage, []byte("error command not found"))
}
}
func onKioskDisconnect(who *websocket.Conn) {
}
func onNewKiosk(who *websocket.Conn) {
}

@ -0,0 +1,77 @@
package socket
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
type WebsocketInterface struct {
Writer http.ResponseWriter
Request *http.Request
OnConnect func(who *websocket.Conn)
OnMessage func(who *websocket.Conn, msg string)
OnDisconnect func(who *websocket.Conn)
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
//DisplaySocket handles all socket calls from the display machine
func DisplaySocket(w http.ResponseWriter, r *http.Request) {
socketUpgrader(WebsocketInterface{
Writer: w,
Request: r,
OnConnect: onNewDisplay,
OnMessage: onDisplayMessage,
OnDisconnect: onDisplayDisconnect,
})
}
//KioskSocket handles all socket calls from the user-facing machine
func KioskSocket(w http.ResponseWriter, r *http.Request) {
socketUpgrader(WebsocketInterface{
Writer: w,
Request: r,
OnConnect: onNewKiosk,
OnMessage: onKioskMessage,
OnDisconnect: onKioskDisconnect,
})
}
//Staffsocket handles all socket calls from the connToTable machines
func StaffSocket(w http.ResponseWriter, r *http.Request) {
socketUpgrader(WebsocketInterface{
Writer: w,
Request: r,
OnConnect: onNewStaff,
OnMessage: onStaffMessage,
OnDisconnect: onStaffDisconnect,
})
}
func socketUpgrader(conn WebsocketInterface) {
ws, err := upgrader.Upgrade(conn.Writer, conn.Request, nil)
if err != nil {
fmt.Fprint(conn.Writer, "You must use the socket protocol", err)
return
}
defer ws.Close()
conn.OnConnect(ws)
for {
_, p, err := ws.ReadMessage()
if err != nil {
conn.OnDisconnect(ws)
break
}
msg := string(p)
conn.OnMessage(ws, msg)
}
}

@ -0,0 +1,178 @@
package socket
import (
"JISQueueing/common"
"JISQueueing/db"
"github.com/gorilla/websocket"
"strconv"
"strings"
)
//Receive Commands: [status, pick, accept, complete]
//Send Commands: [new,
var connToTable = make(map[*websocket.Conn]int)
var connToStaff = make(map[*websocket.Conn]common.Staff)
func onNewStaff(who *websocket.Conn) {
connToTable[who] = -1
sendTaken(who)
}
func onStaffMessage(who *websocket.Conn, msg string) {
message := strings.SplitN(strings.TrimSpace(msg), " ", 2)
switch message[0] {
case "unpick":
if table, ok := connToTable[who]; !ok || table == -1 {
who.WriteMessage(websocket.TextMessage, []byte("error unpick you do not have a table assigned"))
return
}
connToTable[who] = -1
delete(connToStaff, who)
who.WriteMessage(websocket.TextMessage, []byte("success unpick"))
leaveStaff(connToTable[who])
case "pick":
args := strings.SplitN(msg, " ", 3)
if len(args) < 3 {
return
}
choice, err := strconv.Atoi(strings.TrimSpace(args[1]))
if err != nil {
return
}
staff, err := db.GetTokenOwner(strings.TrimSpace(args[2]))
if err == db.ErrTokenNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error pick "+err.Error()))
return
}
if isTaken(choice) {
who.WriteMessage(websocket.TextMessage, []byte("error the chosen table is not available"))
return
}
connToStaff[who] = staff
connToTable[who] = choice
newStaff(choice, who)
case "accept":
id, err := strconv.Atoi(strings.TrimSpace(message[1]))
if err != nil {
return
}
staff, ok := connToStaff[who]
if !ok {
who.WriteMessage(websocket.TextMessage, []byte("error accept your session token was found to be invalid. please relogin"))
return
}
if connToTable[who] == -1 {
who.WriteMessage(websocket.TextMessage, []byte("error accept you do not have a table chosen"))
}
ticket, err := db.GetTicket(id)
if err == db.ErrTicketNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error accept "+err.Error()))
return
}
if ticket.Staff != "{}" {
who.WriteMessage(websocket.TextMessage, []byte("error accept this ticket is already claimed"))
return
}
claimedTicket(ticket, staff, connToTable[who], who)
case "complete":
id, err := strconv.Atoi(strings.TrimSpace(message[1]))
staff := connToStaff[who]
table, ok := connToTable[who]
if err != nil {
return
}
if !ok {
who.WriteMessage(websocket.TextMessage, []byte("error complete you are not assigned to any table"))
}
ticket, err := db.GetTicket(id)
if err == db.ErrTicketNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error complete "+err.Error()))
return
}
if staff.Username != ticket.Staff {
who.WriteMessage(websocket.TextMessage, []byte("error complete you do not own this ticket"))
return
}
finishedTicket(ticket, id, table, who)
case "cancel":
id, err := strconv.Atoi(strings.TrimSpace(message[1]))
staff := connToStaff[who]
table, ok := connToTable[who]
if err != nil {
return
}
if !ok {
who.WriteMessage(websocket.TextMessage, []byte("error cancel you are not assigned to any table"))
}
ticket, err := db.GetTicket(id)
if err == db.ErrTicketNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error cancel "+err.Error()))
return
}
if staff.Username != ticket.Staff {
who.WriteMessage(websocket.TextMessage, []byte("error cancel you do not own this ticket"))
return
}
cancelTicket(ticket, id, table, who)
default:
who.WriteMessage(websocket.TextMessage, []byte("error "+message[0]+" not found"))
}
}
func sendStaffMessage(msg string) {
for who, num := range connToTable {
if num == -1 {
continue
}
who.WriteMessage(websocket.TextMessage, []byte(msg))
}
}
func onStaffDisconnect(who *websocket.Conn) {
num, ok := connToTable[who]
delete(connToTable, who)
delete(connToStaff, who)
if ok && num != -1 {
leaveStaff(connToTable[who])
}
}
func sendTaken(who *websocket.Conn) {
var taken = ""
for _, num := range connToTable {
if num != -1 {
taken = taken + " " + strconv.Itoa(num)
}
}
if taken != "" {
who.WriteMessage(websocket.TextMessage, []byte("taken"+taken))
}
}
func isTaken(table int) bool {
if table < 0 {
return true
}
for _, num := range connToTable {
if table == num {
return true
}
}
return false
}

@ -0,0 +1,74 @@
package server
import (
"JISQueueing/db"
"encoding/json"
nhttp "net/http"
)
type errorResponse struct {
Success bool `json:"success"`
Reason interface{} `json:"reason"`
}
type successResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
}
//InvalidUserInput : A helper function that tells the client that some request validation had failed. (Malformed requests)
func InvalidUserInput(w nhttp.ResponseWriter, r *nhttp.Request) {
writeJSONResponse(w, nhttp.StatusBadRequest, "Your request had failed checks.")
}
//Unauthorized : A helper function that tells the client that they either forgot to provide `token` or it was invalid.
//This function is also used in `/api/login` to tell the client that the user/password combination is incorrect.
func Unauthorized(w nhttp.ResponseWriter, r *nhttp.Request) {
writeJSONResponse(w, nhttp.StatusUnauthorized, "Identifying details not provided or invalid.")
}
//NotFound : A helper function that tells the client that the API endpoint they requested does not exist.
func NotFound(w nhttp.ResponseWriter, r *nhttp.Request) {
writeJSONResponse(w, nhttp.StatusNotFound, "The API endpoint was not found.")
}
//InternalServerError : A helper function that tells the client that the server had encountered an error processing their request.
func InternalServerError(w nhttp.ResponseWriter, r *nhttp.Request) {
writeJSONResponse(w, nhttp.StatusInternalServerError, "An internal server error occurred.")
}
//writeJSONResponse : A helper function that writes the given message into the standard response the client expects.
func writeJSONResponse(w nhttp.ResponseWriter, statusCode int, message interface{}) {
w.Header().Set("Content-Type", "application/json")
var toWrite interface{}
if statusCode == nhttp.StatusOK {
toWrite = successResponse{
Success: true,
Data: message,
}
} else {
w.WriteHeader(statusCode)
toWrite = errorResponse{
Success: false,
Reason: message,
}
}
result, _ := json.Marshal(toWrite)
w.Write(result)
}
//handleToken : A helper function that checks if the token provided by the client is valid.
//Returns if it is valid and sends the Unauthorized message if it isn't.
func handleToken(w nhttp.ResponseWriter, r *nhttp.Request) bool {
err := r.ParseForm()
if err != nil {
InvalidUserInput(w, r)
return false
}
if db.VerifyToken(r.Form.Get("token")) {
return true
}
Unauthorized(w, r)
return false
}