diff --git a/backend/api/controllers/v1/users/core.go b/backend/api/controllers/v1/users/core.go new file mode 100644 index 0000000..c08a3bf --- /dev/null +++ b/backend/api/controllers/v1/users/core.go @@ -0,0 +1,15 @@ +package users + +import "os" + +var ( + signupEnabled bool + unsafeSignup bool + forceNoExpiryTokens bool +) + +func init() { + signupEnabled = os.Getenv("HERMES_SIGNUP_ENABLED") != "" + unsafeSignup = os.Getenv("HERMES_UNSAFE_ADMIN_SIGNUP_ENABLED") != "" + forceNoExpiryTokens = os.Getenv("HERMES_FORCE_DISABLE_REFRESH_TOKEN_EXPIRY") != "" +} diff --git a/backend/api/controllers/v1/users/create.go b/backend/api/controllers/v1/users/create.go index 48a846b..65da7ca 100644 --- a/backend/api/controllers/v1/users/create.go +++ b/backend/api/controllers/v1/users/create.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "fmt" "net/http" - "os" "strings" "github.com/go-playground/validator/v10" @@ -29,18 +28,6 @@ type UserCreationRequest struct { IsBot bool } -var ( - signupEnabled bool - unsafeSignup bool - forceNoExpiryTokens bool -) - -func init() { - signupEnabled = os.Getenv("HERMES_SIGNUP_ENABLED") != "" - unsafeSignup = os.Getenv("HERMES_UNSAFE_ADMIN_SIGNUP_ENABLED") != "" - forceNoExpiryTokens = os.Getenv("HERMES_FORCE_DISABLE_REFRESH_TOKEN_EXPIRY") != "" -} - func CreateUser(c *gin.Context) { var req UserCreationRequest diff --git a/backend/api/controllers/v1/users/login.go b/backend/api/controllers/v1/users/login.go new file mode 100644 index 0000000..2f9922f --- /dev/null +++ b/backend/api/controllers/v1/users/login.go @@ -0,0 +1,157 @@ +package users + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "net/http" + + "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/api/jwtcore" + "github.com/charmbracelet/log" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "golang.org/x/crypto/bcrypt" +) + +type UserLoginRequest struct { + Username *string + Email *string + + Password string `validate:"required"` +} + +func LoginUser(c *gin.Context) { + var req UserLoginRequest + + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) + + return + } + + if err := validator.New().Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + if req.Email == nil && req.Username == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing both email and username in body", + }) + + return + } + + userFindRequestArguments := make([]interface{}, 1) + userFindRequest := "" + + if req.Email != nil { + userFindRequestArguments[0] = &req.Email + userFindRequest += "email = ?" + } + + if req.Username != nil { + userFindRequestArguments[0] = &req.Username + userFindRequest += "username = ?" + } + + var user *dbcore.User + userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User not found", + }) + + return + } + + decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password))) + _, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password)) + + if err != nil { + log.Warnf("failed to decode password in database: %s", userRequest.Error) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse database result for password", + }) + + return + } + + err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password)) + + if err != nil { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Invalid password", + }) + + return + } + + tokenRandomData := make([]byte, 80) + + if _, err := rand.Read(tokenRandomData); err != nil { + log.Warnf("Failed to read random data to use as token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + token := &dbcore.Token{ + UserID: user.ID, + + Token: base64.StdEncoding.EncodeToString(tokenRandomData), + DisableExpiry: forceNoExpiryTokens, + CreationIPAddr: c.ClientIP(), + } + + if result := dbcore.DB.Create(&token); result.Error != nil { + log.Warnf("Failed to create user: %s", result.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add refresh token into database", + }) + + return + } + + jwt, err := jwtcore.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, + "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), + }) +} diff --git a/backend/api/main.go b/backend/api/main.go index c18ed84..d13a0ab 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -92,6 +92,7 @@ func main() { // Initialize routes engine.POST("/api/v1/users/create", users.CreateUser) + engine.POST("/api/v1/users/login", users.LoginUser) log.Infof("Listening on: %s", listeningAddress) err = engine.Run(listeningAddress) diff --git a/routes/Hermes API/Users/Log In.bru b/routes/Hermes API/Users/Log In.bru index 70eed07..6081bf4 100644 --- a/routes/Hermes API/Users/Log In.bru +++ b/routes/Hermes API/Users/Log In.bru @@ -5,14 +5,14 @@ meta { } post { - url: http://127.0.0.1:3000/api/v1/users/login + url: http://127.0.0.1:8000/api/v1/users/login body: json auth: none } body:json { { - "email": "me@greysoh.dev", - "password": "password" + "email": "test@example.com", + "password": "hunter123" } }