server: add websocket module and http module
parent
28fbf77529
commit
81623db66a
@ -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
|
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
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"JISQueueing/common"
|
||||||
|
"JISQueueing/server"
|
||||||
|
"flag"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hashToStaff = make(map[string]common.Staff)
|
||||||
|
|
||||||
func main() {
|
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
|
||||||
|
}
|
Reference in New Issue