2021-03-23 08:43:44 +00:00
package goscrobble
2021-03-24 09:28:05 +00:00
import (
2021-03-25 23:21:28 +00:00
"database/sql"
"encoding/json"
2021-03-24 09:28:05 +00:00
"errors"
"fmt"
2021-03-25 23:21:28 +00:00
"log"
"net"
"strings"
2021-03-25 10:09:17 +00:00
"time"
2021-03-24 09:28:05 +00:00
2021-03-25 23:21:28 +00:00
"github.com/dgrijalva/jwt-go"
2021-03-24 09:28:05 +00:00
"golang.org/x/crypto/bcrypt"
)
const bCryptCost = 16
type User struct {
2021-03-25 23:21:28 +00:00
UUID string ` json:"uuid" `
CreatedAt time . Time ` json:"created_at" `
CreatedIp net . IP ` json:"created_ip" `
ModifiedAt time . Time ` json:"modified_at" `
ModifiedIP net . IP ` jsos:"modified_ip" `
Username string ` json:"username" `
Password [ ] byte ` json:"password" `
Email string ` json:"email" `
Verified bool ` json:"verified" `
Active bool ` json:"active" `
Admin bool ` json:"admin" `
2021-03-25 10:09:17 +00:00
}
// RegisterRequest - Incoming JSON
type RegisterRequest struct {
2021-03-24 09:28:05 +00:00
Username string ` json:"username" `
Email string ` json:"email" `
2021-03-25 10:09:17 +00:00
Password string ` json:"password" `
2021-03-24 09:28:05 +00:00
}
2021-03-25 23:21:28 +00:00
// RegisterRequest - Incoming JSON
type LoginRequest struct {
Username string ` json:"username" `
Password string ` json:"password" `
}
// LoginResponse - JWT issued
type LoginResponse struct {
Token string ` json:"token" `
}
2021-03-24 09:28:05 +00:00
// createUser - Called from API
2021-03-25 23:21:28 +00:00
func createUser ( req * RegisterRequest , ip net . IP ) error {
2021-03-24 09:28:05 +00:00
// Check if user already exists..
2021-03-25 10:09:17 +00:00
if len ( req . Password ) < 8 {
2021-03-24 09:28:05 +00:00
return errors . New ( "Password must be at least 8 characters" )
}
2021-03-25 23:21:28 +00:00
// Check Username is set
2021-03-25 10:09:17 +00:00
if req . Username == "" {
2021-03-24 09:28:05 +00:00
return errors . New ( "A username is required" )
}
2021-03-25 23:21:28 +00:00
// Check max length for Username
if len ( req . Username ) > 64 {
return errors . New ( "Username cannot be longer than 64 characters" )
}
// Check username doesn't contain @
if strings . Contains ( req . Username , "@" ) {
return errors . New ( "Username contains invalid characters" )
}
2021-03-25 10:09:17 +00:00
// If set an email.. validate it!
if req . Email != "" {
if ! isEmailValid ( req . Email ) {
return errors . New ( "Invalid email address" )
}
}
2021-03-24 09:28:05 +00:00
// Check if user or email exists!
2021-03-25 10:09:17 +00:00
if userAlreadyExists ( req ) {
2021-03-24 09:28:05 +00:00
return errors . New ( "Username or email already exists" )
}
// Lets hashit!
2021-03-25 10:09:17 +00:00
hash , err := hashPassword ( req . Password )
2021-03-24 09:28:05 +00:00
if err != nil {
return err
}
2021-03-25 23:21:28 +00:00
return insertUser ( req . Username , req . Email , hash , ip )
}
func loginUser ( logReq * LoginRequest , ip net . IP ) ( [ ] byte , error ) {
var resp [ ] byte
var user User
if logReq . Username == "" {
2021-03-27 04:05:05 +00:00
return resp , errors . New ( "A username is required" )
2021-03-25 23:21:28 +00:00
}
if logReq . Password == "" {
2021-03-27 04:05:05 +00:00
return resp , errors . New ( "A password is required" )
2021-03-25 23:21:28 +00:00
}
if strings . Contains ( logReq . Username , "@" ) {
err := db . QueryRow ( "SELECT BIN_TO_UUID(uuid), username, email, password FROM users WHERE email = ? AND active = 1" , logReq . Username ) . Scan ( & user . UUID , & user . Username , & user . Email , & user . Password )
if err != nil {
if err == sql . ErrNoRows {
return resp , errors . New ( "Invalid Username or Password" )
}
}
} else {
err := db . QueryRow ( "SELECT BIN_TO_UUID(uuid), username, email, password FROM users WHERE username = ? AND active = 1" , logReq . Username ) . Scan ( & user . UUID , & user . Username , & user . Email , & user . Password )
if err == sql . ErrNoRows {
return resp , errors . New ( "Invalid Username or Password" )
}
}
if ! isValidPassword ( logReq . Password , user ) {
return resp , errors . New ( "Invalid Username or Password" )
}
// Issue JWT + Response
token , err := generateJwt ( user )
if err != nil {
log . Printf ( "Error generating JWT: %v" , err )
return resp , errors . New ( "Error logging in" )
}
loginResp := LoginResponse {
Token : token ,
}
resp , _ = json . Marshal ( & loginResp )
return resp , nil
}
func generateJwt ( user User ) ( string , error ) {
atClaims := jwt . MapClaims { }
atClaims [ "sub" ] = user . UUID
atClaims [ "username" ] = user . Username
atClaims [ "email" ] = user . Email
atClaims [ "exp" ] = time . Now ( ) . Add ( JwtExpiry ) . Unix ( )
at := jwt . NewWithClaims ( jwt . SigningMethodHS512 , atClaims )
token , err := at . SignedString ( JwtToken )
if err != nil {
return "" , err
}
return token , nil
2021-03-24 09:28:05 +00:00
}
// insertUser - Does the dirtywork!
2021-03-25 23:21:28 +00:00
func insertUser ( username string , email string , password [ ] byte , ip net . IP ) error {
2021-03-28 08:52:34 +00:00
token := generateToken ( 32 )
_ , err := db . Exec ( "INSERT INTO users (uuid, created_at, created_ip, modified_at, modified_ip, username, email, password, token) " +
"VALUES (UUID_TO_BIN(UUID(), true),NOW(),?,NOW(),?,?,?,?,?)" , ip , ip , username , email , password , token )
2021-03-25 23:21:28 +00:00
return err
}
2021-03-28 08:52:34 +00:00
func updateUser ( uuid string , field string , value string , ip net . IP ) error {
2021-03-25 23:21:28 +00:00
_ , err := db . Exec ( "UPDATE users SET ? = ?, modified_at = NOW(), modified_ip = ? WHERE uuid = ?" , field , value , uuid , ip )
return err
}
func updateUserDirect ( uuid string , field string , value string ) error {
_ , err := db . Exec ( "UPDATE users SET ? = ? WHERE uuid = ?" , field , value , uuid )
2021-03-24 09:28:05 +00:00
return err
}
// hashPassword - Returns bcrypt hash
func hashPassword ( password string ) ( [ ] byte , error ) {
return bcrypt . GenerateFromPassword ( [ ] byte ( password ) , bCryptCost )
}
// isValidPassword - Checks if password is valid
func isValidPassword ( password string , user User ) bool {
2021-03-25 23:21:28 +00:00
err := bcrypt . CompareHashAndPassword ( user . Password , [ ] byte ( password ) )
2021-03-24 09:28:05 +00:00
if err != nil {
return false
}
return true
}
// userAlreadyExists - Returns bool indicating if a record exists for either username or email
// Using two look ups to make use of DB indexes.
2021-03-25 10:09:17 +00:00
func userAlreadyExists ( req * RegisterRequest ) bool {
2021-03-25 23:21:28 +00:00
count , err := getDbCount ( "SELECT COUNT(*) FROM users WHERE username = ?" , req . Username )
if err != nil {
fmt . Printf ( "Error querying for duplicate users: %v" , err )
return true
}
if count > 0 {
2021-03-25 10:09:17 +00:00
return true
}
if req . Email != "" {
// Only run email check if there's an email...
2021-03-25 23:21:28 +00:00
count , err = getDbCount ( "SELECT COUNT(*) FROM users WHERE email = ?" , req . Email )
2021-03-24 09:28:05 +00:00
}
if err != nil {
fmt . Printf ( "Error querying for duplicate users: %v" , err )
return true
}
2021-03-25 23:21:28 +00:00
return count > 0
2021-03-24 09:28:05 +00:00
}