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
|
||||
|
||||
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
|
||||
}
|
Reference in New Issue