WIP: server+db module #2

Draft
hamza wants to merge 8 commits from websocket into master
24 changed files with 2214 additions and 20 deletions

@ -1,29 +1,13 @@
kind: pipeline
name: Build
steps:
- name: Build
image: golang:1.12
volumes:
- name: deps
path: /go
commands:
- go build .
- name: Unit Tests
image: golang:1.12
volumes:
- name: deps
path: /go
commands:
- go test -tag=unit .
- name: Integration Tests
image: golang:1.12
volumes:
- name: deps
path: /go
commands:
- go test -tag=integration .
- go test ./...
volumes:
- name: deps

@ -0,0 +1,348 @@
# Websocket Protocol Documentation
## Table of contents
- [Staff Endpoints](#staff-endpoints)
- [Server To Staff](#server-to-staff)
- [claimed](#claimed)
- [new](#new)
- [taken](#taken)
- [Staff to Server](#staff-to-server)
- [accept](#accept)
- [cancel](#cancel)
- [complete](#complete)
- [pick](#pick)
- [unpick](#unpick)
- [Kiosk Endpoints](#kiosk-endpoints)
- [Kiosk to Server](#kiosk-to-server)
- [new](#new-1)
- [query](#query)
- [Display Endpoints](#display-endpoints)
- [Server to Display](#server-to-display)
- [cancel](#cancel-1)
- [claimed](#claimed-1)
- [complete](#complete-1)
- [pick](#pick-1)
- [unpick](#unpick-1)
## Staff Endpoints
These endpoints are just going to be used between a staff member handling clients and the server.
### Server to Staff
#### claimed
This message is sent to the staff when another staff member has already claimed a ticket.
This message serves the function of letting staff members know when a ticket is no longer
needing a staff member to claim it.
##### example:
```
claimed {"id":0,"email":"example@example.com","name":"Example Name","staff":"StaffMember","start":"2019-10-01T04:12:19Z","end":"1970-01-01T00:00:00Z"}
```
This example message would tell the staff member that the ticket was claimed by 'StaffMember'.
#### new
This message is sent to the staff when a kiosk creates a new ticket.
##### example:
```
new {"id":0,"email":"example@example.com","name":"Example Name","staff":"{}","start":"2019-10-01T04:12:19Z","end":"1970-01-01T00:00:00Z"}
```
This example message would tell the staff member that a client by the name of 'Example Name' has created a new ticket.
#### taken
This message is sent to staff members when they haven't picked an assigned table yet.
The purpose of the message is to allow staff members know what tables are available to pick.
#### example:
```
taken 0 3 5
```
This sample message would tell the staff member that table 0, 3, and 5 are already taken and are not available.
### Staff to Server
#### accept
This message is sent from the staff member when they want to claim a ticket.
##### example
```
accept 0
```
This sample message would claim a ticket with the ID of 0
##### responses:
###### error accept your session token was found to be invalid. please relogin
This means that the internal token used to identify you was deamed incorrect or invalid. You must relog
###### error accept you do not have a table chosen
You must be assigned to a table before handling tickets.
###### error accept ticket id does not exist
The ticket with the provided id you are trying to accept does not exist.
###### error accept this ticket is now claimed
The ticket you are trying to claim has already been claimed by another user.
###### error accept the ticket has already been completed or cancelled
This means that when the ticket you are trying to accept is already claimed by someone else
###### success claimed `{id}`
The staff member has successfully claimed the ticket.
#### cancel
This message is sent from the staff member when a client decided to cancel a ticket, or does not show up.
##### example:
```
cancel 0
```
This example message would cancel the ticket with the id of 0
##### responses:
###### error cancel you are not assigned to any table
You must be assigned to a table before handling tickets.
###### error cancel ticket id does not exist
The ticket with the provided id you are trying to cancel does not exist.
###### error cancel you do not own this ticket
The ticket you are trying to cancel must be owned by you.
###### error cancel the ticket has already been completed or cancelled
This means the ticket you are trying to cancel cannot be cancelled
as it has already previously been, or has been completed.
###### success cancel `{id}`
The ticket was successfully cancelled with no issues.
#### complete
This message is sent from the staff member when a staff member completes a ticket
##### example:
```
complete 0
```
This example message would complete the ticket with the id of 0
##### responses:
###### error complete you are not assigned to any table
You must be assigned to a table before handling tickets.
###### error complete ticket id does not exist
The ticket with the provided id you are trying to complete does not exist.
###### error complete you do not own this ticket
The ticket you are trying to complete must be owned by you.
###### error complete the ticket has already been completed or cancelled
This means the ticket you are trying to cancel cannot be cancelled
as it has already previously been, or has been completed.
###### success complete `{id}`
The ticket was successfully completed with no issues.
#### pick
This command lets a staff member pick a table
##### example:
```
pick 0 abcdefghijklmnoprstuvwxy
```
This command would assign the client the table 0, and validate their identity with their token
##### responses:
###### error pick the provided token does not belong to any staff member
You are using an invalid token. Try to relog
###### error the chosen table is not available
This means that the table you are chosing (0, in this case) is already taken,
or you are picking an invalid number.
###### success pick `{table}`
You successfully claimed the table and can start accepting tickets
#### unpick
This command lets a staff member unset their chosen table
##### example:
```
unpick
```
This would unassign the table the client has chosen
##### responses:
###### error unpick you do not have a table assigned
This means you do not have a current table you have picked
###### success unpick
This means you have successfully reset your chosen table
## Kiosk Endpoints
These endpoints are used between the kiosk (client-facing machine), and the server
### Kiosk to Server
#### new
This message is sent from the kiosk when a new ticket is requested to be created
##### example:
```
new example@example.com Example Name
```
This sample command will create a new ticket under the email of
example@example.com, and their name Example Name
##### responses:
###### success new `{jsonEncodedTicket}`
The ticket was successfully created for the client
#### query
This message is sent from the kiosk when the kiosk tries to autocomplete the client's name
##### example:
```
query example@example.com
```
This sample command will try to find the name for the client
who's email is example@example.com
##### responses:
###### error query visitor email does not exist
The email you are trying to query has no corresponding user,
this is the first time that user is visiting
###### success query `{name}`
This message returns the corresponding name that was found
in our database for the example proivded.
## Display Endpoints
These endpoints will be used by the display machine
### Server to Display
#### cancel
This command will tell the display to clear the table's ticket
##### example:
```
cancel 0
```
This will cancel the ticket that was planned to happen on table 0
#### claimed
This command will tell the display that a ticket was claimed by a staff memeber
##### example:
```
claimed 0 1
```
This will tell the display that table 1 has claimed the ticket with id 1
#### complete
This tells the display that the ticket on a table has been successfully completed
##### example:
```
complete 0
```
This tells the display that the ticket on table 0 has been completed
#### pick
This tells the display that a table has been claimed by a staff member
and to show it as online on the display
##### example:
```
pick 0
```
This tells the display that the table 0 has been picked by a staff member
#### unpick
This tells the display that a client has left serving on a table
##### example:
```
unpick 0
```
This tells the display that the table 0 is no longer going to be accepting/serving tickets.

@ -0,0 +1,41 @@
package main
import (
"encoding/json"
"github.com/go-redis/redis/v7"
"os/exec"
"time"
)
type Message struct {
ID string
Name string
Email string
}
func main() {
client := redis.NewClient(&redis.Options{
Addr: "10.1.3.100:6379",
Password: "",
DB: 0,
})
println("waiting for messages...")
channel := client.Subscribe("qq")
_, err := channel.Receive()
if err != nil {
panic(err)
}
for msg := range channel.Channel() {
if msg.Channel != "qq" {
return
}
bytes := []byte(msg.Payload)
var data Message
json.Unmarshal(bytes, &data)
exec.Command("./makejpg.sh", data.ID, data.Name, data.Email)
println("Printing", data.ID, data.Name, data.Email, "at", time.Now().String())
}
}

@ -0,0 +1,52 @@
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"`
}
//Config Struct represents the structure that holds the entire app config
type Configuration struct {
Debug bool
IP int
Database string
Server struct {
SSL bool
CertFile string
KeyFile string
Port int
}
Redis struct {
Addr string
Channel string
}
Mail struct {
Host string
Port int
Username string
Logo string
}
}

@ -0,0 +1,59 @@
package db
import (
"JISQueueing/common"
"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_DB(config common.Configuration) {
openDatabase(config.Database)
}
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=? COLLATE NOCASE", 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=? COLLATE NOCASE", 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=? COLLATE NOCASE", 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,14 @@
module gitea.teamortix.com/Team-Ortix/JISQueueing
module JISQueueing
go 1.12
require (
github.com/go-redis/redis/v7 v7.0.0-beta.5
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1
github.com/mattn/go-sqlite3 v1.11.0
github.com/spf13/viper v1.6.2 // indirect
github.com/xhit/go-simple-mail v2.2.2+incompatible
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
)

180
go.sum

@ -0,0 +1,180 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis/v7 v7.0.0-beta.5 h1:7bdbDkv2nKZm6Tydrvmay3xOvVaxpAT4ZsNTrSDMZUE=
github.com/go-redis/redis/v7 v7.0.0-beta.5/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
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.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
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/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
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=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74=
github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -1,5 +1,60 @@
package main
import (
"JISQueueing/common"
"JISQueueing/db"
"JISQueueing/server"
"fmt"
"github.com/spf13/viper"
"log"
"net/http"
"strconv"
"github.com/gorilla/handlers"
)
func main() {
//TODO: Populate with flags and calls to `http/`
}
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/queuing")
viper.SetDefault("debug", false)
viper.SetDefault("ip", "10.1.3.100")
viper.SetDefault("database", "/etc/queuing/database.sqlite")
viper.SetDefault("server.ssl", true)
viper.SetDefault("server.certFile", "/etc/queuing/server.crt")
viper.SetDefault("server.keyFile", "/etc/queuing/server.key")
viper.SetDefault("server.port", 433)
viper.SetDefault("redis.addr", "10.1.3.100:6379")
viper.SetDefault("redis.channel", "qq")
viper.SetDefault("mail.host", "mail1.jisedu.or.id")
viper.SetDefault("mail.port", 25)
viper.SetDefault("mail.username", "IT.Q@jisedu.or.id")
viper.SetDefault("mail.logo", "/etc/queuing/logo.jpg")
var config common.Configuration
err := viper.Unmarshal(&config)
if err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
}
db.Init_DB(config)
hostLocation := ":" + strconv.Itoa(config.Server.Port)
mux := server.NewServerMux(config)
cors := handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
handlers.AllowedMethods([]string{"POST", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}),
)(mux)
if config.Server.SSL {
crt := config.Server.CertFile
key := config.Server.KeyFile
log.Fatal(http.ListenAndServeTLS(hostLocation, crt, key, cors))
} else {
log.Fatal(http.ListenAndServe(hostLocation, cors))
}
}

@ -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/common"
"JISQueueing/server/socket"
"net/http"
"github.com/gorilla/mux"
)
//NewServerMux creates a new http handler object handling API requests
func NewServerMux(config common.Configuration) 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("/static").Handler(http.FileServer(http.Dir("./static")))
router.PathPrefix("/").HandlerFunc(indexHandler)
socket.StartRedisServer(config)
socket.SetConfiguration(config)
return router
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./static/index.html")
}

@ -0,0 +1,33 @@
package socket
import (
"fmt"
"github.com/gorilla/websocket"
)
var displays = make(map[*websocket.Conn]bool)
func onNewDisplay(who *websocket.Conn) {
displays[who] = true
for _, conn := range onlineStaff {
if conn.Table == -1 {
continue
}
who.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("status %d %d %d", conn.Table, conn.Status, conn.CurrentTicket)))
}
}
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,105 @@
package socket
import (
"JISQueueing/common"
"JISQueueing/db"
"encoding/json"
"strconv"
"github.com/gorilla/websocket"
)
var queue = make([]common.Ticket, 0)
var config common.Configuration
func SetConfiguration(configFile common.Configuration) {
config = configFile
}
func newTicket(ticket common.Ticket, who *websocket.Conn) {
queue = append(queue, ticket)
jsonTicket, _ := json.Marshal(ticket)
who.WriteMessage(websocket.TextMessage, []byte("success new "+string(jsonTicket)))
for _, conn := range onlineStaff {
if conn.Status == 1 {
notifyTicket(conn.Conn)
}
}
sendMessageToRedisChannel(Message{ticket.ID, ticket.Name, ticket.Email})
}
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
}
removeTicket(ticket.ID)
ticket.Staff = staff.Username
jsonTicket, _ := json.Marshal(ticket)
sendDisplayMessage("claimed " + strconv.Itoa(table) + " " + strconv.Itoa(ticket.ID))
who.WriteMessage(websocket.TextMessage, []byte("success claimed "+string(jsonTicket)))
for _, conn := range onlineStaff {
if conn.Status == 1 {
conn.Conn.WriteMessage(websocket.TextMessage, []byte("info claimed "+string(jsonTicket)))
notifyTicket(conn.Conn)
}
}
}
func finishedTicket(id int, table int, who *websocket.Conn) {
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)))
notifyTicket(who)
//ticket, _ := db.GetTicket(id)
sendDisplayMessage("complete " + strconv.Itoa(table))
}
func cancelTicket(id int, table int, who *websocket.Conn) {
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)))
notifyTicket(who)
sendDisplayMessage("cancel " + strconv.Itoa(table))
}
func newStaff(table int, who *websocket.Conn) {
who.WriteMessage(websocket.TextMessage, []byte("success pick "+strconv.Itoa(table)))
for _, conn := range onlineStaff {
sendTaken(conn.Conn)
}
sendDisplayMessage("pick " + strconv.Itoa(table))
notifyTicket(who)
}
func leaveStaff(table int) {
sendDisplayMessage("unpick " + strconv.Itoa(table))
for _, conn := range onlineStaff {
sendTaken(conn.Conn)
}
}
func notifyTicket(who *websocket.Conn) {
if len(queue) == 0 {
return
}
jsonTicket, _ := json.Marshal(queue[0])
who.WriteMessage(websocket.TextMessage, []byte("info new "+string(jsonTicket)))
}
func removeTicket(id int) {
for i, t := range queue {
if id == t.ID {
copy(queue[i:], queue[i+1:])
queue[len(queue)-1] = common.Ticket{}
queue = queue[:len(queue)-1]
return
}
}
}

@ -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,40 @@
package socket
import (
json "encoding/json"
"github.com/go-redis/redis/v7"
)
var client *redis.Client
var channel *redis.PubSub
type Message struct {
ID int
Name string
Email string
}
func StartRedisServer() {
client = redis.NewClient(&redis.Options{
Addr: config.Redis.Addr,
})
_, err := client.Ping().Result()
if err != nil {
panic(err)
}
channel = client.Subscribe("qq")
_, err = channel.Receive()
if err != nil {
panic(err)
}
}
func sendMessageToRedisChannel(msg Message) {
mesg, _ := json.Marshal(msg)
status := client.Publish(config.Redis.Channel, string(mesg))
if status.Err() != nil {
println("Error while sending message to redis:", status.Err().Error())
}
}

@ -0,0 +1,78 @@
package socket
import (
"JISQueueing/common"
"bytes"
"github.com/spf13/viper"
"github.com/xhit/go-simple-mail"
"html/template"
"log"
"time"
)
var (
tmpl = template.New("email")
body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
*{margin:0;padding:0}body{font-family:Helvetica,sans-serif}h2{font-size:2.25rem;padding:12px 0}h2 strong{color:#00509e}.desc{font-size:1.3rem;padding:32px 0}.desc h3{padding:6px 0}.bye{color:#555;font-style:italic}.dash{display:inline-block;padding-left:12px}.first{display:inline-block}.second{padding-left:22px}
</style>
</head>
<body>
<h2>Hi <strong>{{{.Name}}}</strong>,</h2>
<div class="desc">
<h3>Thank you for coming to the JIS IT Helpdesk.</h3>
<h3>Your queue number is {{{.ID}}}.</h3>
</div>
<div class="bye">
<div class="dash">-</div>
<h4 class="first">This message was automatically generated by The Dragon Queue.</h4>
<h4 class="second"> Any questions? Please feel free to <a href="mailto:hgunawan@jisedu.or.id">contact us</a></h4>
<img src="cid:logo.svg" alt="JIS"/>
</div>
</body>
</html>
`
)
func sendMail(ticket common.Ticket) {
server := mail.NewSMTPClient()
server.Host = viper.GetString("mail.host")
server.Port = viper.GetInt("mail.port")
server.Username = viper.GetString("mail.username")
server.Encryption = mail.EncryptionNone
server.KeepAlive = false
server.ConnectTimeout = 10 * time.Second
server.SendTimeout = 10 * time.Second
client, err := server.Connect()
if err != nil {
log.Fatal(err)
}
email := mail.NewMSG()
t, _ := tmpl.Parse(body)
var result bytes.Buffer
t.Execute(&result, ticket)
email.
SetFrom("JIS Queuing HelpDesk").
AddTo(ticket.Email).
SetSubject("Thank you for coming to the JIS IT HelpDesk").
SetBody(mail.TextHTML, result.String()).
AddInline(viper.GetString("mail.logo"), "logo.svg")
err = email.Send(client)
if err != nil {
log.Println(err)
}
}

@ -0,0 +1,81 @@
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,
CheckOrigin: func(r *http.Request) bool {
return true
//return strings.Contains(r.Host, "localhost")
},
}
//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,240 @@
package socket
import (
"JISQueueing/common"
"JISQueueing/db"
"strconv"
"strings"
"github.com/gorilla/websocket"
)
//Receive Commands: [status, pick, accept, complete]
//Send Commands: [new,
type staffConnection struct {
Conn *websocket.Conn
Staff common.Staff
Table int
Status int
CurrentTicket int
}
var onlineStaff = make(map[*websocket.Conn]staffConnection)
func onNewStaff(who *websocket.Conn) {
onlineStaff[who] = staffConnection{
Conn: who,
Staff: common.Staff{
Username: "{}",
},
Table: -1,
Status: 0,
CurrentTicket: -1,
}
sendTaken(who)
}
func onStaffMessage(who *websocket.Conn, msg string) {
message := strings.SplitN(strings.TrimSpace(msg), " ", 2)
switch message[0] {
case "accept":
id, err := strconv.Atoi(strings.TrimSpace(message[1]))
conn, ok := onlineStaff[who]
if err != nil || !ok {
return
}
if conn.Staff.Username == "{}" || conn.Status == 0 {
who.WriteMessage(websocket.TextMessage, []byte("error accept your session token was found to be invalid. please relogin"))
return
}
if conn.Table == -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
}
conn.Status = 2
conn.CurrentTicket = id
onlineStaff[who] = conn
claimedTicket(ticket, conn.Staff, conn.Table, who)
case "cancel":
id, err := strconv.Atoi(strings.TrimSpace(message[1]))
conn, ok := onlineStaff[who]
if err != nil || !ok {
return
}
ticket, err := db.GetTicket(id)
if err == db.ErrTicketNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error cancel "+err.Error()))
return
}
if conn.Staff.Username != ticket.Staff {
who.WriteMessage(websocket.TextMessage, []byte("error cancel you do not own this ticket"))
return
}
conn.Status = 1
conn.CurrentTicket = -1
onlineStaff[who] = conn
cancelTicket(id, conn.Table, who)
case "complete":
id, err := strconv.Atoi(strings.TrimSpace(message[1]))
conn, ok := onlineStaff[who]
if err != nil || !ok {
return
}
ticket, err := db.GetTicket(id)
if err == db.ErrTicketNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error complete "+err.Error()))
return
}
if conn.Staff.Username != ticket.Staff {
who.WriteMessage(websocket.TextMessage, []byte("error complete you do not own this ticket"))
return
}
conn.Status = 1
conn.CurrentTicket = -1
onlineStaff[who] = conn
finishedTicket(id, conn.Table, 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
}
if onlineStaff[who].Table != -1 {
leaveStaff(onlineStaff[who].Table)
}
onlineStaff[who] = staffConnection{
Conn: who,
Staff: staff,
Table: choice,
Status: 1,
CurrentTicket: -1,
}
newStaff(choice, who)
case "status":
if conn, ok := onlineStaff[who]; !ok || conn.Table == -1 {
who.WriteMessage(websocket.TextMessage, []byte("error unpick you do not have a table assigned"))
return
}
notifyTicket(who)
case "unpick":
conn, ok := onlineStaff[who]
if !ok || conn.Table == -1 {
who.WriteMessage(websocket.TextMessage, []byte("error unpick you do not have a table assigned"))
return
}
table := conn.Table
conn.Table = -1
conn.Status = 0
staff := conn.Staff
staff.Username = "{}"
conn.Staff = staff
onlineStaff[who] = conn
leaveStaff(table)
who.WriteMessage(websocket.TextMessage, []byte("success unpick"))
case "valid":
if len(message) < 2 {
return
}
_, err := db.GetTokenOwner(strings.TrimSpace(message[1]))
if err == db.ErrTokenNotFound {
who.WriteMessage(websocket.TextMessage, []byte("error valid "+err.Error()))
return
}
who.WriteMessage(websocket.TextMessage, []byte("success valid"))
default:
who.WriteMessage(websocket.TextMessage, []byte("error "+message[0]+" not found"))
}
}
func sendStaffMessage(msg string) {
for who, conn := range onlineStaff {
if conn.Table == -1 {
continue
}
who.WriteMessage(websocket.TextMessage, []byte(msg))
}
}
func onStaffDisconnect(who *websocket.Conn) {
conn, ok := onlineStaff[who]
if !ok || conn.Table == -1 {
return
}
if conn.Status == 2 {
cancelTicket(conn.CurrentTicket, conn.Table, who)
}
delete(onlineStaff, who)
leaveStaff(conn.Table)
}
func sendTaken(who *websocket.Conn) {
var taken = ""
for _, conn := range onlineStaff {
if conn.Table != -1 {
taken = taken + " " + strconv.Itoa(conn.Table)
}
}
if taken != "" {
who.WriteMessage(websocket.TextMessage, []byte("info taken"+taken))
} else {
who.WriteMessage(websocket.TextMessage, []byte("info taken"))
}
}
func isTaken(table int) bool {
if table < 1 || table > 5 {
return true
}
for _, conn := range onlineStaff {
if table == conn.Table {
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
}