This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
JISQueueing/db/staff.go

161 lines
4.9 KiB
Go

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,
}
}