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