diff --git a/database/db.go b/database/db.go index f53f4d7..ded201f 100644 --- a/database/db.go +++ b/database/db.go @@ -17,12 +17,12 @@ func InitDatabase() (*sql.DB, error) { password := os.Getenv("PG_PASSWORD") host := os.Getenv("PG_HOST") connStr := fmt.Sprintf("user=postgres host=%s dbname=relay password=%s sslmode=disable", host, password) - db, err := sql.Open("postgres", connStr) + DB, err := sql.Open("postgres", connStr) if err != nil { log.Fatal(err) } - return db, nil + return DB, nil } @@ -44,3 +44,32 @@ func GetUsers(db *sql.DB) ([]string, error) { } return users, err } + +func CheckUserExists(db *sql.DB, username string) (bool, error) { + query := `SELECT COUNT(1) FROM accounts WHERE username= $1` + + var count int + + err := db.QueryRow(query, username).Scan(&count) + + if err != nil { + return false, fmt.Errorf("error checking username exists: %v", err) + } + + return count > 0, err +} + +func InsertUser(db *sql.DB, username string, passwordHash string) (string, error) { + query := ` + INSERT INTO Accounts (username, password_hash) + VALUES ($1, $2) + RETURNING user_id; + ` + + var userId string + err := db.QueryRow(query, username, passwordHash).Scan(&userId) + if err != nil { + return "", fmt.Errorf("error inserting user: %v", err) + } + return userId, err +} diff --git a/go.mod b/go.mod index bbef2f7..0e43da8 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ require ( github.com/gofiber/fiber/v2 v2.52.6 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.32.0 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -19,5 +21,5 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect ) diff --git a/go.sum b/go.sum index de0d8d0..1f5110d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -25,7 +27,13 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/handlers/signup.go b/handlers/signup.go new file mode 100644 index 0000000..a0aa4f1 --- /dev/null +++ b/handlers/signup.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + "log" + "os" + "relay-server/database" + "relay-server/helpers" + "relay-server/models" + "time" +) + +func Signup(c *fiber.Ctx) error { + db, _ := database.InitDatabase() + u := new(models.SignupStruct) + 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 passwords 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) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid username"}) + } + + // Checks if username already exist in database + exist, _ := database.CheckUserExists(db, u.Username) + if exist { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user already exists"}) + } + + // Create password hash + passwordHash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + log.Printf("error hashing password: %v", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "internal server error"}) + } + + // Insert username and password hash to database + userId, err := database.InsertUser(db, u.Username, string(passwordHash)) + 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 + + // 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}) + +} diff --git a/helpers/filter.go b/helpers/filter.go new file mode 100644 index 0000000..f1cab62 --- /dev/null +++ b/helpers/filter.go @@ -0,0 +1,61 @@ +package helpers + +import ( + "regexp" +) + +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) + if !ok { + return false + } + + match, _ := regexp.MatchString(USERNAME_REGEX, strUsername) + if !match { + return false + } + // Checks if username length is valid + if len(strUsername) < MIN_USERNAME_LENGTH { + return false + } + if len(strUsername) > MAX_USERNAME_LENGTH { + return false + } + + return true +} + +func IsValidPassword(password interface{}) bool { + strPassword, ok := password.(string) + if !ok { + return false + } + + if len(strPassword) < MIN_PASSWORD_LENGTH { + return false + } + + if len(strPassword) > MAX_PASSWORD_LENGTH { + return false + } + + match, _ := regexp.MatchString(PASSWORD_REGEX, strPassword) + if !match { + return false + } + + return true +} diff --git a/main.go b/main.go index ba43b79..dbe8272 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "github.com/gofiber/fiber/v2" "log" "relay-server/database" + "relay-server/handlers" ) func main() { @@ -19,11 +20,19 @@ 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.Listen(":3000") + app.Post("/api/auth/signup", handlers.Signup) + + err = app.Listen(":3000") + if err != nil { + return + } } diff --git a/models/signup.go b/models/signup.go new file mode 100644 index 0000000..6b864dc --- /dev/null +++ b/models/signup.go @@ -0,0 +1,6 @@ +package models + +type SignupStruct struct { + Username string `json:"username" xml:"username" form:"username"` + Password string `json:"password" xml:"password" form:"password"` +}