diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index a1d88a1..52d63fb 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -2306,10 +2306,11 @@ const docTemplate = `{ "type": { "type": "string", "enum": [ - "obligation", - "license" + "OBLIGATION", + "LICENSE", + "USER" ], - "example": "license" + "example": "LICENSE" }, "type_id": { "type": "integer", diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index e5baf15..cd44322 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -2299,10 +2299,11 @@ "type": { "type": "string", "enum": [ - "obligation", - "license" + "OBLIGATION", + "LICENSE", + "USER" ], - "example": "license" + "example": "LICENSE" }, "type_id": { "type": "integer", diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index f618eab..d20ac69 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -29,9 +29,10 @@ definitions: type: string type: enum: - - obligation - - license - example: license + - OBLIGATION + - LICENSE + - USER + example: LICENSE type: string type_id: example: 34 diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index b5c9f76..b16b11a 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -78,69 +78,88 @@ func CreateUser(c *gin.Context) { return } - user := models.User(input) - *user.UserName = html.EscapeString(strings.TrimSpace(*user.UserName)) - *user.DisplayName = html.EscapeString(strings.TrimSpace(*user.DisplayName)) - err := utils.HashPassword(&user) - if err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "password hashing failed", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } + _ = db.DB.Transaction(func(tx *gorm.DB) error { - result := db.DB.Where(models.User{UserName: user.UserName}).FirstOrCreate(&user) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + username := c.GetString("username") + user := models.User(input) + *user.UserName = html.EscapeString(strings.TrimSpace(*user.UserName)) + *user.DisplayName = html.EscapeString(strings.TrimSpace(*user.DisplayName)) + err := utils.HashPassword(&user) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "password hashing failed", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return nil + } + + result := tx.Where(models.User{UserName: user.UserName}).FirstOrCreate(&user) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + er := models.LicenseError{ + Status: http.StatusConflict, + Message: "Failed to create the new user", + Error: "User with this email id already exists", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusConflict, er) + } else { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to create the new user", + Error: result.Error.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + } + return nil + } else if result.RowsAffected == 0 { + errMessage := fmt.Sprintf("Error: User with username '%s' already exists", *user.UserName) + if !*user.Active { + errMessage = fmt.Sprintf("Error: User with username '%s' already exists, but is deactivated", *user.UserName) + } er := models.LicenseError{ Status: http.StatusConflict, - Message: "Failed to create the new user", - Error: "User with this email id already exists", + Message: "can not create user", + Error: errMessage, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusConflict, er) - } else { + return nil + } + + if err := utils.AddChangelogsForUser(tx, username, &user, &models.User{}); err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, - Message: "Failed to create the new user", - Error: result.Error.Error(), + Message: "Failed to update changelogs", + Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusInternalServerError, er) + return err } - return - } else if result.RowsAffected == 0 { - errMessage := fmt.Sprintf("Error: User with username '%s' already exists", *user.UserName) - if !*user.Active { - errMessage = fmt.Sprintf("Error: User with username '%s' already exists, but is deactivated", *user.UserName) - } - er := models.LicenseError{ - Status: http.StatusConflict, - Message: "can not create user", - Error: errMessage, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + + res := models.UserResponse{ + Data: []models.User{user}, + Status: http.StatusCreated, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, } - c.JSON(http.StatusConflict, er) - return - } - res := models.UserResponse{ - Data: []models.User{user}, - Status: http.StatusCreated, - Meta: &models.PaginationMeta{ - ResourceCount: 1, - }, - } + c.JSON(http.StatusCreated, res) + + return nil + }) - c.JSON(http.StatusCreated, res) } // CreateOidcUser creates a new user via oidc id token @@ -387,90 +406,107 @@ func CreateOidcUser(c *gin.Context) { // @Security ApiKeyAuth // @Router /users/{username} [patch] func UpdateUser(c *gin.Context) { - var user models.User - username := c.Param("username") + _ = db.DB.Transaction(func(tx *gorm.DB) error { + var olduser models.User + var updates models.UserUpdate + username := c.Param("username") - if err := db.DB.Where(models.User{UserName: &username}).First(&user).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: "no user with such username exists", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + if err := tx.Where(models.User{UserName: &username}).First(&olduser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "no user with such username exists", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return nil } - c.JSON(http.StatusNotFound, er) - return - } - var input models.UserUpdate - if err := c.ShouldBindJSON(&input); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + // var input models.UserUpdate + if err := c.ShouldBindJSON(&updates); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return nil } - c.JSON(http.StatusBadRequest, er) - return - } - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.Struct(&input); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "can not update user with these field values", - Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.Struct(&updates); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "can not update user with these field values", + Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return nil } - c.JSON(http.StatusBadRequest, er) - return - } - updatedUser := models.User(input) - if updatedUser.UserName != nil { - *updatedUser.UserName = html.EscapeString(strings.TrimSpace(*updatedUser.UserName)) - } - if updatedUser.DisplayName != nil { - *updatedUser.DisplayName = html.EscapeString(strings.TrimSpace(*updatedUser.DisplayName)) - } - if updatedUser.UserPassword != nil { - err := utils.HashPassword(&updatedUser) - if err != nil { + updatedUser := models.User(updates) + if updatedUser.UserName != nil { + *updatedUser.UserName = html.EscapeString(strings.TrimSpace(*updatedUser.UserName)) + } + if updatedUser.DisplayName != nil { + *updatedUser.DisplayName = html.EscapeString(strings.TrimSpace(*updatedUser.DisplayName)) + } + if updatedUser.UserPassword != nil { + err := utils.HashPassword(&updatedUser) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "password hashing failed", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return nil + } + } + + updatedUser.Id = olduser.Id + if err := tx.Clauses(clause.Returning{}).Updates(&updatedUser).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "password hashing failed", + Status: http.StatusInternalServerError, + Message: "Failed to update user", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) - return + c.JSON(http.StatusInternalServerError, er) + return nil + } + if err := utils.AddChangelogsForUser(tx, username, &olduser, &updatedUser); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update user", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return err } - } - updatedUser.Id = user.Id - if err := db.DB.Clauses(clause.Returning{}).Updates(&updatedUser).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Failed to update user", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + res := models.UserResponse{ + Data: []models.User{updatedUser}, + Status: http.StatusCreated, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, } - c.JSON(http.StatusInternalServerError, er) - return - } - res := models.UserResponse{ - Data: []models.User{updatedUser}, - Status: http.StatusOK, - Meta: &models.PaginationMeta{ - ResourceCount: 1, - }, - } - c.JSON(http.StatusOK, res) + c.JSON(http.StatusCreated, res) + + return nil + }) } // UpdateProfile updates one's user profile @@ -487,87 +523,105 @@ func UpdateUser(c *gin.Context) { // @Security ApiKeyAuth // @Router /users [patch] func UpdateProfile(c *gin.Context) { - var user models.User - username := c.GetString("username") + _ = db.DB.Transaction(func(tx *gorm.DB) error { + var olduser models.User + var changes models.ProfileUpdate - if err := db.DB.Where(models.User{UserName: &username}).First(&user).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: "no user with such username exists", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } + username := c.GetString("username") - var input models.ProfileUpdate - if err := c.ShouldBindJSON(&input); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + if err := tx.Where(models.User{UserName: &username}).First(&olduser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "no user with such username exists", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return nil } - c.JSON(http.StatusBadRequest, er) - return - } - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.Struct(&input); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "can not update profile with these field values", - Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + if err := c.ShouldBindJSON(&changes); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return nil } - c.JSON(http.StatusBadRequest, er) - return - } - updatedUser := models.User(input) - if updatedUser.DisplayName != nil { - *updatedUser.DisplayName = html.EscapeString(strings.TrimSpace(*updatedUser.DisplayName)) - } - if updatedUser.UserPassword != nil { - err := utils.HashPassword(&updatedUser) - if err != nil { + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.Struct(&changes); err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, - Message: "password hashing failed", - Error: err.Error(), + Message: "can not update profile with these field values", + Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusBadRequest, er) - return + return nil + } + + updatedUser := models.User(changes) + if updatedUser.DisplayName != nil { + *updatedUser.DisplayName = html.EscapeString(strings.TrimSpace(*updatedUser.DisplayName)) + } + if updatedUser.UserPassword != nil { + err := utils.HashPassword(&updatedUser) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "password hashing failed", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return nil + } } - } - updatedUser.Id = user.Id - if err := db.DB.Clauses(clause.Returning{}).Updates(&updatedUser).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Failed to update user", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + updatedUser.Id = olduser.Id + if err := tx.Clauses(clause.Returning{}).Updates(&updatedUser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update user", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return nil + } + if err := utils.AddChangelogsForUser(tx, username, &olduser, &updatedUser); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update profile", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return err } - c.JSON(http.StatusInternalServerError, er) - return - } - res := models.UserResponse{ - Data: []models.User{updatedUser}, - Status: http.StatusOK, - Meta: &models.PaginationMeta{ - ResourceCount: 1, - }, - } - c.JSON(http.StatusOK, res) + res := models.UserResponse{ + Data: []models.User{updatedUser}, + Status: http.StatusCreated, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, + } + + c.JSON(http.StatusCreated, res) + + return nil + }) + } // DeleteUser marks an existing user record as inactive @@ -584,33 +638,51 @@ func UpdateProfile(c *gin.Context) { // @Security ApiKeyAuth // @Router /users/{username} [delete] func DeleteUser(c *gin.Context) { - var user models.User - username := c.Param("username") - active := true - if err := db.DB.Where(models.User{UserName: &username, Active: &active}).First(&user).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: "no user with such username exists", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + _ = db.DB.Transaction(func(tx *gorm.DB) error { + var olduser models.User + username := c.Param("username") + active := true + if err := tx.Where(models.User{UserName: &username, Active: &active}).First(&olduser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "no user with such username exists", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return nil } - c.JSON(http.StatusNotFound, er) - return - } - *user.Active = false - if err := db.DB.Updates(&user).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "failed to delete user", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), + updatedUser := olduser + updateActive := false + updatedUser.Active = &updateActive + + if err := db.DB.Updates(&updatedUser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "failed to delete user", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return nil } - c.JSON(http.StatusNotFound, er) - return - } - c.Status(http.StatusNoContent) + if err := utils.AddChangelogsForUser(tx, username, &olduser, &updatedUser); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update profile", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return err + } + + c.Status(http.StatusNoContent) + return nil + }) } // GetAllUser retrieves a list of all users from the database. diff --git a/pkg/models/types.go b/pkg/models/types.go index e235dbd..972c2e4 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2023 Siemens AG // SPDX-FileContributor: Gaurav Mishra // SPDX-FileContributor: Dearsh Oberoi +// SPDX-FileContributor: 2025 Chayan Das <01chayandas@gmail.com> // // SPDX-License-Identifier: GPL-2.0-only @@ -358,7 +359,7 @@ type Audit struct { UserId int64 `json:"user_id" gorm:"column:user_id" example:"123"` User User `gorm:"foreignKey:UserId;references:Id" json:"user"` Timestamp time.Time `json:"timestamp" gorm:"column:timestamp" example:"2023-12-01T18:10:25.00+05:30"` - Type string `json:"type" gorm:"column:type" enums:"obligation,license" example:"license"` + Type string `json:"type" gorm:"column:type" enums:"OBLIGATION,LICENSE,USER" example:"LICENSE"` TypeId int64 `json:"type_id" gorm:"column:type_id" example:"34"` Entity interface{} `json:"entity" gorm:"-" swaggertype:"object"` ChangeLogs []ChangeLog `json:"-"` diff --git a/pkg/utils/util.go b/pkg/utils/util.go index b0fbc3e..6aba46b 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -816,3 +816,38 @@ func AddChangelogsForLicense(tx *gorm.DB, username string, return nil } + +// AddChangelogsForUser adds changelogs for the updated fields on user update +func AddChangelogsForUser(tx *gorm.DB, username string, + newUser, oldUser *models.User) error { + var changes []models.ChangeLog + + AddChangelog("UserName", oldUser.UserName, newUser.UserName, &changes) + AddChangelog("DisplayName", oldUser.DisplayName, newUser.DisplayName, &changes) + AddChangelog("UserEmail", oldUser.UserEmail, newUser.UserEmail, &changes) + AddChangelog("UserLevel", oldUser.UserLevel, newUser.UserLevel, &changes) + AddChangelog("UserPassword", oldUser.UserPassword, newUser.UserPassword, &changes) + AddChangelog("Active", oldUser.Active, newUser.Active, &changes) + + if len(changes) != 0 { + var user models.User + if err := tx.Where(models.User{UserName: &username}).First(&user).Error; err != nil { + return err + } + + audit := models.Audit{ + UserId: user.Id, + TypeId: newUser.Id, + Timestamp: time.Now(), + Type: "USER", + ChangeLogs: changes, + } + + if err := tx.Create(&audit).Error; err != nil { + return err + } + } + + return nil + +}