diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..ddc79b7 --- /dev/null +++ b/config/config.go @@ -0,0 +1,15 @@ +package config + +const MIN_USERNAME_LENGTH = 4 + +const MAX_USERNAME_LENGTH = 20 + +const MIN_PASSWORD_LENGTH = 8 + +const MAX_PASSWORD_LENGTH = 128 + +const PASSWORD_REGEX = `^[A-Za-z0-9!@#$%^&*(),.?":{}|<>]+$` + +const USERNAME_REGEX = `^[a-zA-Z0-9_]+$` + +const BCRYPT_COST = 16 diff --git a/database/db.go b/database/db.go index ded201f..6bcce27 100644 --- a/database/db.go +++ b/database/db.go @@ -73,3 +73,32 @@ func InsertUser(db *sql.DB, username string, passwordHash string) (string, error } return userId, err } + +func GetPasswordHash(db *sql.DB, username string) (string, error) { + query := ` + SELECT password_hash FROM Accounts + WHERE LOWER(username) = LOWER($1); + ` + + var passwordHash string + err := db.QueryRow(query, username).Scan(&passwordHash) + if err != nil { + return "", fmt.Errorf("error getting password: %v", err) + } + + return passwordHash, err +} + +func GetUserId(db *sql.DB, username string) (string, error) { + query := ` + SELECT user_id, username AS dbUsername FROM Accounts + WHERE LOWER(username) = $1; + ` + + var dbUsername string + err := db.QueryRow(query, username).Scan(&dbUsername, &dbUsername) + if err != nil { + return "", fmt.Errorf("error getting user id: %v", err) + } + return dbUsername, err +} diff --git a/handlers/signup.go b/handlers/auth.go similarity index 50% rename from handlers/signup.go rename to handlers/auth.go index a0aa4f1..7146f04 100644 --- a/handlers/signup.go +++ b/handlers/auth.go @@ -1,11 +1,12 @@ package handlers import ( + "fmt" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" - "log" "os" + "relay-server/config" "relay-server/database" "relay-server/helpers" "relay-server/models" @@ -25,12 +26,10 @@ func Signup(c *fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "password is empty"}) } - // Checks if passwords have valid length and characters + // Checks if passwords or username have valid length and characters if !helpers.IsValidPassword(u.Password) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid password"}) - } - // Checks if username have valid length and characters - if !helpers.IsValidUsername(u.Username) { + } else if !helpers.IsValidUsername(u.Username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid username"}) } @@ -41,9 +40,9 @@ func Signup(c *fiber.Ctx) error { } // Create password hash - passwordHash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + passwordHash, err := bcrypt.GenerateFromPassword([]byte(u.Password), config.BCRYPT_COST) if err != nil { - log.Printf("error hashing password: %v", err) + fmt.Printf("error hashing password: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "internal server error"}) } @@ -70,5 +69,60 @@ func Signup(c *fiber.Ctx) error { // If everything went well sent username and user_id assigned by database return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Successfully signed up", "username": u.Username, "user_id": userId}) - +} + +func Login(c *fiber.Ctx) error { + db, _ := database.InitDatabase() + u := new(models.LoginStruct) + + if err := c.BodyParser(u); err != nil { + return err + } + + // Checks if username or passwords are empty + if u.Username == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username is empty"}) + } else if u.Password == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "password is empty"}) + } + + // Checks if username or passwords have valid length and characters + if !helpers.IsValidUsername(u.Username) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid username"}) + } else if !helpers.IsValidPassword(u.Password) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid password"}) + } + + // Checks if username exist in database + exist, _ := database.CheckUserExists(db, u.Username) + if !exist { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user does not exists"}) + } + + // Verifies password matching + passwordHash, _ := database.GetPasswordHash(db, u.Password) + if bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(u.Password)) != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid password"}) + } + + userId, err := database.GetUserId(db, u.Username) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "internal server error"}) + } + // Generate token with user id and username + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": userId, + "username": u.Username, + }) + // Sign token + signedToken, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) + + // Set token to cookies + cookie := new(fiber.Cookie) + cookie.Name = "token" + cookie.Value = signedToken + cookie.Expires = time.Now().Add(30 * 24 * time.Hour) + cookie.HTTPOnly = true + + return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Successfully signed up", "username": u.Username, "user_id": userId}) } diff --git a/helpers/filter.go b/helpers/filter.go index f1cab62..bb0bd60 100644 --- a/helpers/filter.go +++ b/helpers/filter.go @@ -2,20 +2,9 @@ package helpers import ( "regexp" + "relay-server/config" ) -const MIN_USERNAME_LENGTH = 4 - -const MAX_USERNAME_LENGTH = 20 - -const MIN_PASSWORD_LENGTH = 8 - -const MAX_PASSWORD_LENGTH = 128 - -const PASSWORD_REGEX = `^[A-Za-z0-9!@#$%^&*(),.?":{}|<>]+$` - -const USERNAME_REGEX = `^[a-zA-Z0-9_]+$` - func IsValidUsername(username interface{}) bool { // Checks if username is type of string strUsername, ok := username.(string) @@ -23,15 +12,15 @@ func IsValidUsername(username interface{}) bool { return false } - match, _ := regexp.MatchString(USERNAME_REGEX, strUsername) + match, _ := regexp.MatchString(config.USERNAME_REGEX, strUsername) if !match { return false } // Checks if username length is valid - if len(strUsername) < MIN_USERNAME_LENGTH { + if len(strUsername) < config.MIN_USERNAME_LENGTH { return false } - if len(strUsername) > MAX_USERNAME_LENGTH { + if len(strUsername) > config.MAX_USERNAME_LENGTH { return false } @@ -44,15 +33,15 @@ func IsValidPassword(password interface{}) bool { return false } - if len(strPassword) < MIN_PASSWORD_LENGTH { + if len(strPassword) < config.MIN_PASSWORD_LENGTH { return false } - if len(strPassword) > MAX_PASSWORD_LENGTH { + if len(strPassword) > config.MAX_PASSWORD_LENGTH { return false } - match, _ := regexp.MatchString(PASSWORD_REGEX, strPassword) + match, _ := regexp.MatchString(config.PASSWORD_REGEX, strPassword) if !match { return false } diff --git a/main.go b/main.go index dbe8272..a2a2786 100644 --- a/main.go +++ b/main.go @@ -20,17 +20,18 @@ func main() { log.Fatal(err) } }(db) + app.Get("/", func(c *fiber.Ctx) error { return c.SendString("Hello, World!") }) - app.Get("/users", func(c *fiber.Ctx) error { - users, _ := database.GetUsers(db) - return c.JSON(fiber.Map{"users": users}) - }) + //app.Get("/users", func(c *fiber.Ctx) error { + // users, _ := database.GetUsers(db) + // return c.JSON(fiber.Map{"users": users}) + //}) app.Post("/api/auth/signup", handlers.Signup) - + app.Post("/api/auth/login", handlers.Login) err = app.Listen(":3000") if err != nil { return diff --git a/models/signup.go b/models/models.go similarity index 52% rename from models/signup.go rename to models/models.go index 6b864dc..5fc7c30 100644 --- a/models/signup.go +++ b/models/models.go @@ -1,5 +1,10 @@ package models +type LoginStruct struct { + Username string `json:"username" xml:"username" form:"username"` + Password string `json:"password" xml:"password" form:"password"` +} + type SignupStruct struct { Username string `json:"username" xml:"username" form:"username"` Password string `json:"password" xml:"password" form:"password"`