| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- package usermanager
- import (
- "errors"
- "time"
- "math/rand"
- "strconv"
- "git.mmnx.de/Moe/databaseutils"
- "git.mmnx.de/Moe/configutils"
- "git.mmnx.de/Moe/templatehelpers"
- "github.com/dgrijalva/jwt-go"
- "github.com/kataras/iris"
- "golang.org/x/crypto/bcrypt"
- "fmt"
- )
- var (
- Users *[]User // stores all currently logged in users
- )
- const ( // Error constants
- ERR_USER_NOT_FOUND = "ERR_USER_NOT_FOUND"
- ERR_PASSWORD_MISMATCH = "ERR_PASSWORD_MISMATCH"
- ERR_SESSION_TIMED_OUT = "ERR_SESSION_TIMED_OUT"
- ERR_INVALID_TOKEN = "ERR_INVALID_TOKEN"
- ERR_USERNAME_TAKEN = "ERR_USERNAME_TAKEN"
- ERR_INVALID_PARAM = "ERR_INVALID_PARAM"
- ERR_NO_CHANGES = "ERR_NO_CHANGES"
- )
- type User struct { // User
- ID string
- Username string
- Password string
- Admin string
- TokenUsed string
- }
- type PageParams struct{
- NotificationType string
- Notification string
- ReqDir string
- Admin string
- }
- type PageUserParams struct{
- NotificationType string
- Notification string
- ReqDir string
- Username string
- Admin string
- Custom []string
- }
- type PageUserParamsMessage struct{
- NotificationType string
- Notification string
- ReqDir string
- Username string
- Email string
- Admin string
- Message string
- }
- func (user *User) Login(username string, password string) (string, error) {
- hmacSampleSecret := []byte(configutils.Conf.CryptoKey) // crypto key for JWT encryption
- row, err := databaseutils.DBUtil.GetRow("*", "users", "username", username) // get user from db
- if err != nil {
- if err.Error() == databaseutils.ERR_EMPTY_RESULT { // empty result -> user not found
- return "", errors.New(ERR_USER_NOT_FOUND)
- } else {
- return "", errors.New("Unknown error")
- }
- }
- err = bcrypt.CompareHashAndPassword([]byte(row[2]), []byte(password))
- if err == nil { // if sent' pw hash == stored pw hash
- expire, _ := time.ParseDuration("168h") // 7 days
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
- "username": username,
- "userid": row[0],
- "nbf": time.Now().Unix(),
- "exp": time.Now().Add(expire).Unix(),
- "token": "nigger", // TODO db based tokens
- })
- tokenString, _ := token.SignedString(hmacSampleSecret)
- user.ID = row[0]
- user.Username = row[1]
- user.Password = string(row[2])
- user.Admin = string(row[3])
- user.TokenUsed = string(row[4])
- if err != nil {
- fmt.Printf("Error: ", err.Error())
- }
- *Users = append(*Users, *user) // store user in logged-in-users list
- return tokenString, nil // return tokenString (Cookie)
- } else {
- return "", errors.New(ERR_PASSWORD_MISMATCH) // wrong password
- }
- }
- func (user *User) Logout(userID string) {
- userArrayID := SearchUser(userID) // get logged in users list index
- user.ID = "" // empty
- user.Username = ""
- user.Password = ""
- user.Admin = ""
- user.TokenUsed = ""
- (*Users)[userArrayID] = *user
- return
- }
- func LogoutHandler(ctx *iris.Context) {
- userID := ctx.GetString("userID")
- user, err := GetUserFromDB(userID)
- if err != nil {
- templatehelpers.ShowError(err.Error(), ctx, "account")
- return
- }
- user.Logout(userID);
- ctx.SetCookieKV("token", "")
- ctx.Redirect("/")
- }
- func (user *User) Update() error {
- colsVals := make([][]string, 2)
- colsVals[0] = []string{"username", user.Username}
- colsVals[1] = []string{"password", user.Password}
- err := databaseutils.DBUtil.UpdateRow("users", "id", string(user.ID), colsVals)
- if err != nil {
- fmt.Println("ERROOR UPDATING: " + err.Error())
- }
- return nil
- }
- func SearchUser(userID string) int {
- for i := range *Users {
- if (*Users)[i].ID == userID {
- return i
- }
- }
- return -1
- }
- func SearchUserByUsername(username string) int {
- for i := range *Users {
- if (*Users)[i].Username == username {
- return i
- }
- }
- return -1
- }
- func VerifyUserLoggedIn(tokenString string) (bool, string, error) { // TODO renew JWT from time to time preventing expiry
- if tokenString == "" { // if no tokenString("Cookie") exists fail
- return false, "-1", errors.New(ERR_INVALID_TOKEN)
- }
- hmacSampleSecret := []byte(configutils.Conf.CryptoKey) // crypto key for JWT encryption
- token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
- if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
- return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
- }
- return hmacSampleSecret, nil
- })
- if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { // if token is valid
- if userID, ok := claims["userid"].(string); ok { // extract userID
- sliceID := SearchUser(userID) // verify that user has a session on the server
- if sliceID != -1 { // searchUser returns -1 if there's no such user
- return true, userID, nil // logged in, TODO: "0" template comparision dynamic
- } else {
- return false, "-1", errors.New(ERR_SESSION_TIMED_OUT) // Session probably expired - may also be faked? TODO more checks?
- }
- } else {
- return false, "-1", errors.New("Unknown error") // This should never happen, prolly can't convert something in claims then..
- }
- } else {
- return false, "-1", errors.New(ERR_INVALID_TOKEN) // Token is invalid, expired or whatever, TODO switch with ERR_SESSION_TIMED_OUT when database based session system
- }
- }
- func AuthHandler(ctx *iris.Context) {
- tokenString := ctx.GetCookie("token")
- isAuthed, userID, err := VerifyUserLoggedIn(tokenString)
- if err != nil {
- // fmt.Println("Auth error: ", err.Error())
- }
- if isAuthed {
- ctx.Set("userID", userID) // save userID for in-context use
- ctx.Next() // successfully authed, next handler
- } else {
- if err := ctx.Render("login_box.html", PageUserParams{"1", err.Error(), "login", "", "0", []string{}}); err != nil {
- //println(err.Error()) // TODO log this somewhere
- } // failed to auth
- }
- }
- func CanBeAuthedHandler(ctx *iris.Context) {
- tokenString := ctx.GetCookie("token")
- isAuthed, userID, err := VerifyUserLoggedIn(tokenString)
- if isAuthed {
- ctx.Set("userID", userID) // save userID for in-context use
- } else if err != nil {
- if !((err.Error() != "ERR_SESSION_TIMED_OUT") || (err.Error() != "ERR_INVALID_TOKEN")) { // ignore ERR_SESSION_TIMED_OUT and ERR_INVALID_TOKEN
- templatehelpers.ShowError(err.Error(), ctx, "register")
- return
- }
- }
- ctx.Next() // authed users can now use their accounts, next handler
- }
- func AdminHandler(ctx *iris.Context) {
- userID := ctx.GetString("userID")
- user, err := GetUser(userID)
- if user.Admin != "1" { // check if user is admin
- err = errors.New("User no Admin: " + userID)
- fmt.Println(err.Error())
- ctx.Redirect("/")
- return
- } else {
- ctx.Next()
- }
- }
- func GenerateTokens(numTokens int) []string {
- const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
- tokens := make([]string, 0)
- dbTokens := make([][]string, 0)
- for i := 0; i < numTokens; i++ {
- b := make([]byte, 16)
- for i := range b {
- b[i] = letterBytes[rand.Intn(len(letterBytes))]
- }
- tokens = append(tokens, string(b))
- dbTokens = [][]string{[]string{"value", string(b)}, []string{"used", "0"}}
- err := databaseutils.DBUtil.InsertRow("tokens", dbTokens)
- if err != nil {
- fmt.Println(err.Error())
- return []string{""}
- }
- }
- return tokens
- }
- func GetTokens(used bool) []string {
- dbTokens, err := databaseutils.DBUtil.GetRows("*", "tokens", "used", "0") // get unused tokens
- if used {
- dbTokens, err = databaseutils.DBUtil.GetRows("*", "tokens", "used", "1") // get used tokens
- }
- if err != nil {
- fmt.Println(err.Error()) // TODO: nicer / outsource
- }
- tokens := make([]string, 0)
- for i, _ := range dbTokens {
- tokens = append(tokens, dbTokens[i][1])
- }
- return tokens
- }
- func GetUser(userID string) (User, error) {
- usersArrayID := SearchUser(userID)
- if usersArrayID == -1 { // TODO check if unneccessary (AuthHandler) (Ddepends on where used: TODO CHECK)
- return User{}, errors.New("User not logged in")
- }
- user := (*Users)[usersArrayID] // user must be logged in to do this -> get from users list
- return user, nil
- }
- func GetUserFromDB(userID string) (User, error) {
- row, err := databaseutils.DBUtil.GetRow("*", "users", "id", userID) // get user from db
- if err != nil {
- return User{}, err
- }
- return User{row[0], row[1], string(row[2]), string(row[3]), string(row[4])}, nil
- }
- func SearchUserByUsernameInDB(username string) int {
- user, err := databaseutils.DBUtil.GetRow("*", "users", "username", username)
- if err != nil {
- if err.Error() != "ERR_EMPTY_RESULT" {
- fmt.Println(err.Error())
- }
- return -1
- }
- userID, err := strconv.Atoi(user[0])
- if err != nil {
- fmt.Println(err.Error())
- }
- return userID
- }
- func SearchUserByTokenInDB(token string) (int, error) {
- user, err := databaseutils.DBUtil.GetRowsDoubleCond("users", "tokens", "`token-id` = `tokens`.`id`", "`tokens`.`value` = '" + token + "'")
- if err != nil {
- return -1, err
- }
- userID, err := strconv.Atoi(user[0][0])
- if err != nil {
- return -1, err
- }
- return userID, nil
- }
- func RegisterUserWithToken(username string, password string, token string) error {
- tokenID := databaseutils.DBUtil.GetString("id", "tokens", "value", token)
- user := [][]string{[]string{"username", username}, []string{"password", password}, []string{"admin", "0"}, []string{"token-id", tokenID}}
- err := databaseutils.DBUtil.InsertRow("users", user)
- if err != nil {
- fmt.Println(err.Error())
- return err
- }
- err = databaseutils.DBUtil.UpdateRow("tokens", "value", token, [][]string{[]string{"used", "1"}})
- if err != nil {
- fmt.Println(err.Error())
- return err
- }
- return nil
- }
- func VerifyUserUpdate(username string, password string, userID string) error {
- tmpUser, err := GetUserFromDB(userID)
- if err != nil {
- return err
- }
- if SearchUserByUsernameInDB(username) != -1 && username != tmpUser.Username { // username can't be changed as there already exists a user with that name or it's the old name
- return errors.New(ERR_USERNAME_TAKEN)
- }
- if username == "" { // if not left empty change
- return errors.New(ERR_INVALID_PARAM)
- }
- if password == "" { // if not left empty we change it
- return errors.New(ERR_INVALID_PARAM)
- }
- return nil
- }
- // Processes the update of an user, username and password are the new "wanted" values, can also be empty string ("")
- func UserUpdateProcessor(username string, password string, userID string) error {
- user, err := GetUserFromDB(userID)
- hashedPassword := ""
- if err != nil {
- return err
- }
- if username == "" {
- username = user.Username
- }
- if password == "" {
- password = user.Password
- hashedPassword = user.Password // we dont need to / _can't_ re-hash the hash
- }
- if err = VerifyUserUpdate(username, password, userID); err != nil {
- return err
- }
- if hashedPassword == "" {
- hashedPassword, err = func (hashedPassword []byte, err error) (string, error) { // hash password, we use an anonymous function to convert int to string
- if err != nil { // should never happen
- return "", err
- }
- return string(hashedPassword), nil
- }(bcrypt.GenerateFromPassword([]byte(password), 15)) // this is the actual hashing call
- }
- user.Username = username
- user.Password = hashedPassword
- if err = user.Update(); err != nil {
- return err
- }
- return nil
- }
- func IsTokenUsed(tokens []string, token string) bool {
- usedToken := false
- for i, _ := range tokens {
- if token == tokens[i] {
- usedToken = true
- break
- }
- }
- return usedToken
- }
- func RegisterHandler(ctx *iris.Context) {
- token := ctx.FormValueString("token") // POST values from login form
- username := ctx.FormValueString("username")
- password := ctx.FormValueString("password")
- unusedTokens := GetTokens(false) // get all unused tokens
- usedTokens := GetTokens(true) // get all used tokens
- unusedToken := IsTokenUsed(unusedTokens, token) // check if token is unused
- usedToken := IsTokenUsed(usedTokens, token) // check if token is used
- if !unusedToken && !usedToken { // token doesnt exist
- templatehelpers.ShowError(ERR_INVALID_TOKEN, ctx, "register")
- return
- }
- tokenUserID, err := SearchUserByTokenInDB(token)
- if err != nil { // id of user, we're going to change if exists
- if err.Error() != "ERR_EMPTY_RESULT" { // if no user found for that token let them register
- templatehelpers.ShowError(err.Error(), ctx, "register")
- return
- }
- }
- tokenUserIDStr := strconv.FormatInt(int64(tokenUserID), 10)
- user := User{} // new user
- if tokenUserIDStr == "-1" { // register a new account
- passwordBin, _ := bcrypt.GenerateFromPassword([]byte(password), 15) // hash password
- err := RegisterUserWithToken(username, string(passwordBin), token) // register user
- if err != nil {
- templatehelpers.ShowError(err.Error(), ctx, "register")
- return
- }
- tokenString, err := user.Login(username, password) // try to login
- if err != nil {
- templatehelpers.ShowError(err.Error(), ctx, "login")
- } else {
- ctx.SetCookieKV("token", tokenString) // set tokenString as cookie
- templatehelpers.ShowNotification("registration successfull", ctx, "home")
- }
- } else { // used token -> update
- if err := UserUpdateProcessor(username, password, tokenUserIDStr); err != nil { // simply try to update
- templatehelpers.ShowError(err.Error(), ctx, "register")
- return
- } else {
- user.Logout(tokenUserIDStr) // log user out from system
- templatehelpers.ShowNotification("reset successfull", ctx, "login")
- }
- }
- }
|