From 1018742a22f6ef00bf5c0e6c412b6615bffd150c Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Thu, 25 Mar 2021 23:09:17 +1300 Subject: [PATCH] Register API complete, email validation too --- internal/goscrobble/server.go | 66 +++++++++++++++++++++++++---------- internal/goscrobble/user.go | 63 ++++++++++++++++++++------------- internal/goscrobble/utils.go | 25 +++++++++++++ migrations/2_users.down.sql | 2 +- migrations/2_users.up.sql | 1 + 5 files changed, 113 insertions(+), 44 deletions(-) diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index ec7d5e57..45d6c143 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -36,7 +36,7 @@ func HandleRequests() { v1.HandleFunc("/profile/{id}", jwtMiddleware(serveEndpoint)) // No Auth - v1.HandleFunc("/register", serveEndpoint).Methods("POST") + v1.HandleFunc("/register", handleRegister).Methods("POST") v1.HandleFunc("/login", serveEndpoint).Methods("POST") v1.HandleFunc("/logout", serveEndpoint).Methods("POST") @@ -52,16 +52,30 @@ func HandleRequests() { } // MIDDLEWARE +// throwUnauthorized - Throws a 403 +func throwUnauthorized(w http.ResponseWriter, m string) { + jr := jsonResponse{ + Err: m, + } + js, _ := json.Marshal(&jr) + err := errors.New(string(js)) + http.Error(w, err.Error(), http.StatusUnauthorized) +} + +// throwUnauthorized - Throws a 403 : +func throwBadReq(w http.ResponseWriter, m string) { + jr := jsonResponse{ + Err: m, + } + js, _ := json.Marshal(&jr) + err := errors.New(string(js)) + http.Error(w, err.Error(), http.StatusBadRequest) +} // tokenMiddleware - Validates token to a user func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - jr := jsonResponse{ - Err: "Invalid API Token", - } - js, _ := json.Marshal(&jr) - err := errors.New(string(js)) - http.Error(res, err.Error(), http.StatusUnauthorized) + return func(w http.ResponseWriter, r *http.Request) { + throwUnauthorized(w, "Invalid API Token") return // next(res, req) } @@ -69,31 +83,45 @@ func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc { // jwtMiddleware - Validates middleware to a user func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(res http.ResponseWriter, req *http.Request) { - jr := jsonResponse{ - Err: "Invalid JWT Token", - } - js, _ := json.Marshal(&jr) - err := errors.New(string(js)) - http.Error(res, err.Error(), http.StatusUnauthorized) + return func(w http.ResponseWriter, r *http.Request) { + throwUnauthorized(w, "Invalid JWT Token") return // next(res, req) } } -// ENDPOINT HANDLING +// API ENDPOINT HANDLING + +// handleRegister - Does as it says! +func handleRegister(w http.ResponseWriter, r *http.Request) { + regReq := RegisterRequest{} + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(®Req) + if err != nil { + throwBadReq(w, err.Error()) + return + } + + err = createUser(®Req) + if err != nil { + throwBadReq(w, err.Error()) + return + } + + // Lets trick 'em for now ;) ;) + fmt.Fprintf(w, "{}") +} // serveEndpoint - API stuffs func serveEndpoint(w http.ResponseWriter, r *http.Request) { - var jsonInput map[string]interface{} - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&jsonInput) + json, err := decodeJson(r.Body) if err != nil { // If we can't decode. Lets tell them nicely. http.Error(w, "{\"error\":\"Invalid JSON\"}", http.StatusBadRequest) return } + log.Printf("%v", json) // Lets trick 'em for now ;) ;) fmt.Fprintf(w, "{}") } diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go index 2d36d029..cefaf838 100644 --- a/internal/goscrobble/user.go +++ b/internal/goscrobble/user.go @@ -3,6 +3,7 @@ package goscrobble import ( "errors" "fmt" + "time" "golang.org/x/crypto/bcrypt" ) @@ -10,44 +11,59 @@ import ( const bCryptCost = 16 type User struct { - UUID string `json:"uuid"` + UUID string `json:"uuid"` + CreatedAt time.Time `json:"created_at"` + Username string `json:"username"` + password []byte + Email string `json:"email"` + Verified bool `json:"verified"` + Active bool `json:"active"` + Admin bool `json:"admin"` +} + +// RegisterRequest - Incoming JSON +type RegisterRequest struct { Username string `json:"username"` - password []byte Email string `json:"email"` - Verified bool `json:"verified"` - Active bool `json:"active"` - Admin bool `json:"admin"` + Password string `json:"password"` } // createUser - Called from API -func createUser(username string, email string, password string) error { +func createUser(req *RegisterRequest) error { // Check if user already exists.. - if len(password) < 8 { + if len(req.Password) < 8 { return errors.New("Password must be at least 8 characters") } // Check username is set - if username == "" { + if req.Username == "" { return errors.New("A username is required") } + // If set an email.. validate it! + if req.Email != "" { + if !isEmailValid(req.Email) { + return errors.New("Invalid email address") + } + } + // Check if user or email exists! - if userAlreadyExists(username, email) { + if userAlreadyExists(req) { return errors.New("Username or email already exists") } // Lets hashit! - hash, err := hashPassword(password) + hash, err := hashPassword(req.Password) if err != nil { return err } - return insertUser(username, email, hash) + return insertUser(req.Username, req.Email, hash) } // insertUser - Does the dirtywork! func insertUser(username string, email string, password []byte) error { - _, err := db.Exec("INSERT INTO users (uuid, username, email, password) VALUES (UUID_TO_BIN(UUID(), true),'?','?','?')", username, email, password) + _, err := db.Exec("INSERT INTO users (uuid, created_at, username, email, password) VALUES (UUID_TO_BIN(UUID(), true),NOW(),?,?,?)", username, email, password) return err } @@ -69,14 +85,16 @@ func isValidPassword(password string, user User) bool { // userAlreadyExists - Returns bool indicating if a record exists for either username or email // Using two look ups to make use of DB indexes. -func userAlreadyExists(username string, email string) bool { - var usernameCount, emailCount int - err := db.QueryRow("SELECT COUNT(*) FROM users WHERE username = '?'", username).Scan(&usernameCount) - // Only run email check if there's an email... - if email != "" { - err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = '?'", email).Scan(&emailCount) - } else { - emailCount = 0 +func userAlreadyExists(req *RegisterRequest) bool { + var userExists int + err := db.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", req.Username).Scan(&userExists) + if userExists > 0 { + return true + } + + if req.Email != "" { + // Only run email check if there's an email... + err = db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", req.Email).Scan(&userExists) } if err != nil { @@ -84,8 +102,5 @@ func userAlreadyExists(username string, email string) bool { return true } - count := usernameCount + emailCount - - // If there is more than one.. Return true. User exists. - return count != 0 + return userExists > 0 } diff --git a/internal/goscrobble/utils.go b/internal/goscrobble/utils.go index e8475423..60570c5e 100644 --- a/internal/goscrobble/utils.go +++ b/internal/goscrobble/utils.go @@ -1 +1,26 @@ package goscrobble + +import ( + "encoding/json" + "io" + "regexp" +) + +var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + +// decodeJson - Returns a map[string]interface{} +func decodeJson(body io.ReadCloser) (map[string]interface{}, error) { + var jsonInput map[string]interface{} + decoder := json.NewDecoder(body) + err := decoder.Decode(&jsonInput) + + return jsonInput, err +} + +// isEmailValid - checks if the email provided passes the required structure and length. +func isEmailValid(e string) bool { + if len(e) < 3 && len(e) > 254 { + return false + } + return emailRegex.MatchString(e) +} diff --git a/migrations/2_users.down.sql b/migrations/2_users.down.sql index 365a2107..59e4c582 100644 --- a/migrations/2_users.down.sql +++ b/migrations/2_users.down.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS users; \ No newline at end of file +DROP TABLE IF EXISTS `users`; \ No newline at end of file diff --git a/migrations/2_users.up.sql b/migrations/2_users.up.sql index 8f8609a7..cdc764a6 100644 --- a/migrations/2_users.up.sql +++ b/migrations/2_users.up.sql @@ -1,5 +1,6 @@ CREATE TABLE IF NOT EXISTS `users` ( `uuid` BINARY(16) PRIMARY KEY, + `created_at` DATETIME NOT NULL, `username` VARCHAR(64) NOT NULL, `password` VARCHAR(60) NOT NULL, `email` VARCHAR(255) NULL DEFAULT NULL,