161 lines
4.9 KiB
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,
|
|
}
|
|
}
|