Skip to content

Commit 840e79d

Browse files
zsigggRichDom2185
andauthored
Change /groups/{group id}/users route (#115)
* fix: inject user id into context in development env * feat: add ID column to list view of Users * feat: add role attribute to /groups/{groupId}/users response a userId could have multiple roles, if it is present in the usergroups table in multiple rows (with different groupId). however, since we specify the groupId in the /group/{groupId}/users route, it makes sense that we only retrieve users that have that groupId. - in controller, get groupId from context - for each user obtained from the users table, call GetUserRoleByID function from usergroups model - store the roles obtained in an array - pass array to updated ListFrom function from users view, which now include a role attribute in its JSON response * fix: DeleteUser in users model previously, the DeleteUser function was not working as it chained `.Delete(&user)` after `.First(&user)`. this resulted in a query as shown, and its corresponding error: * `ERROR: table name "users" specified more than once (SQLSTATE 42712) [8.059ms] [rows:1] UPDATE "users" SET "deleted_at"='2024-02-12 20:52:41.02' FROM "users" WHERE id = 4 AND "users"."deleted_at" IS NULL AND "users"."id" = 4` the intent of `.First(&user)` was likely to store the user to be deleted first in the `user` variable with the use of a `SELECT` SQL statement. however, chaining another method at the end of a finisher method likely led to some errors (see chaining [here](https://gorm.io/docs/method_chaining.html)). thus, this commit attempts to separate the two statements, preserving the initial intent of storing the user to be deleted before deleting, and then returning this user. * feat: function to update row in users model * feat: function to update row in usergroups model * feat: add /users/{userID}/role put route missing validation for parameters, but seemed unnecessary since there is only one route with parameter validation (creating user) * fix: package name for update role's params * Use range iterator for users list view * Remove unused UpdateUser function * Prevent self-update of roles * Fix error message * Add `UpdateRole` param validation * Refactor logic --------- Co-authored-by: Richard Dominick <[email protected]>
1 parent 7288818 commit 840e79d

File tree

8 files changed

+177
-8
lines changed

8 files changed

+177
-8
lines changed

controller/usergroups/update.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package usergroups
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strconv"
8+
9+
"github.com/go-chi/chi/v5"
10+
"github.com/sirupsen/logrus"
11+
"github.com/source-academy/stories-backend/controller"
12+
"github.com/source-academy/stories-backend/internal/auth"
13+
"github.com/source-academy/stories-backend/internal/database"
14+
apierrors "github.com/source-academy/stories-backend/internal/errors"
15+
userpermissiongroups "github.com/source-academy/stories-backend/internal/permissiongroups/users"
16+
"github.com/source-academy/stories-backend/model"
17+
usergroupparams "github.com/source-academy/stories-backend/params/usergroups"
18+
userviews "github.com/source-academy/stories-backend/view/users"
19+
)
20+
21+
func HandleUpdateRole(w http.ResponseWriter, r *http.Request) error {
22+
userIDStr := chi.URLParam(r, "userID")
23+
userID, err := strconv.Atoi(userIDStr)
24+
if err != nil {
25+
return apierrors.ClientBadRequestError{
26+
Message: fmt.Sprintf("Invalid userID: %v", err),
27+
}
28+
}
29+
30+
err = auth.CheckPermissions(r, userpermissiongroups.Update(uint(userID)))
31+
if err != nil {
32+
logrus.Error(err)
33+
return apierrors.ClientForbiddenError{
34+
Message: fmt.Sprintf("Error updating user: %v", err),
35+
}
36+
}
37+
38+
var params usergroupparams.UpdateRole
39+
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
40+
e, ok := err.(*json.UnmarshalTypeError)
41+
if !ok {
42+
logrus.Error(err)
43+
return apierrors.ClientBadRequestError{
44+
Message: fmt.Sprintf("Bad JSON parsing: %v", err),
45+
}
46+
}
47+
48+
// TODO: Investigate if we should use errors.Wrap instead
49+
return apierrors.ClientUnprocessableEntityError{
50+
Message: fmt.Sprintf("Invalid JSON format: %s should be a %s.", e.Field, e.Type),
51+
}
52+
}
53+
54+
err = params.Validate()
55+
if err != nil {
56+
logrus.Error(err)
57+
return apierrors.ClientUnprocessableEntityError{
58+
Message: fmt.Sprintf("JSON validation failed: %v", err),
59+
}
60+
}
61+
62+
updateModel := *params.ToModel(uint(userID))
63+
64+
// Get DB instance
65+
db, err := database.GetDBFrom(r)
66+
if err != nil {
67+
logrus.Error(err)
68+
return err
69+
}
70+
71+
// TODO: Refactor logic
72+
userGroup, err := model.UpdateUserGroupByUserID(db, updateModel.UserID, &updateModel)
73+
if err != nil {
74+
logrus.Error(err)
75+
return err
76+
}
77+
78+
user, err := model.GetUserByID(db, int(userGroup.UserID))
79+
if err != nil {
80+
logrus.Error(err)
81+
return err
82+
}
83+
84+
controller.EncodeJSONResponse(w, userviews.SummaryFrom(user, userGroup))
85+
return nil
86+
}

controller/users/list.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"github.com/source-academy/stories-backend/controller"
99
"github.com/source-academy/stories-backend/internal/auth"
1010
"github.com/source-academy/stories-backend/internal/database"
11+
groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
1112
apierrors "github.com/source-academy/stories-backend/internal/errors"
1213
userpermissiongroups "github.com/source-academy/stories-backend/internal/permissiongroups/users"
14+
"github.com/source-academy/stories-backend/internal/usergroups"
1315
"github.com/source-academy/stories-backend/model"
1416
userviews "github.com/source-academy/stories-backend/view/users"
1517
)
@@ -36,6 +38,21 @@ func HandleList(w http.ResponseWriter, r *http.Request) error {
3638
return err
3739
}
3840

39-
controller.EncodeJSONResponse(w, userviews.ListFrom(users))
41+
groupId, err := usergroups.GetGroupIDFrom(r)
42+
if err != nil {
43+
logrus.Error(err)
44+
return err
45+
}
46+
47+
roles := make([]groupenums.Role, len(users))
48+
for i, user := range users {
49+
roles[i], err = model.GetUserRoleByID(db, user.ID, *groupId)
50+
if err != nil {
51+
logrus.Error(err)
52+
return err
53+
}
54+
}
55+
56+
controller.EncodeJSONResponse(w, userviews.ListFrom(users, roles))
4057
return nil
4158
}

internal/auth/middleware.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ func MakeMiddlewareFrom(conf *config.Config) func(http.Handler) http.Handler {
2828
// Skip auth in development mode
2929
if conf.Environment == envutils.ENV_DEVELOPMENT {
3030
return func(next http.Handler) http.Handler {
31-
return next
31+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32+
r = injectUserIDToContext(r, 1)
33+
next.ServeHTTP(w, r)
34+
})
3235
}
3336
}
3437

internal/permissiongroups/users/users.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ func Update(userID uint) permissions.PermissionGroup {
3030
Groups: []permissions.PermissionGroup{
3131
userpermissions.
3232
GetRolePermission(userpermissions.CanUpdateUsers),
33-
IsSelf{UserID: userID},
3433
},
3534
}
3635
}

internal/router/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
6464
r.Route("/users", func(r chi.Router) {
6565
r.Get("/", handleAPIError(users.HandleList))
6666
r.Get("/{userID}", handleAPIError(users.HandleRead))
67+
r.Put("/{userID}/role", handleAPIError(usergroupscontroller.HandleUpdateRole))
6768
r.Delete("/{userID}", handleAPIError(users.HandleDelete))
6869
r.Post("/", handleAPIError(users.HandleCreate))
6970
r.Post("/batch", handleAPIError(usergroupscontroller.HandleBatchCreate))

model/usergroups.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,31 @@ func CreateUserGroup(db *gorm.DB, userGroup *UserGroup) error {
3838
}
3939
return nil
4040
}
41+
42+
func GetUserRoleByID(db *gorm.DB, userID uint, groupID uint) (groupenums.Role, error) {
43+
userGroup, err := GetUserGroupByID(db, userID, groupID)
44+
if err != nil {
45+
return userGroup.Role, database.HandleDBError(err, "userGroup")
46+
}
47+
return userGroup.Role, nil
48+
}
49+
50+
func UpdateUserGroupByUserID(db *gorm.DB, userID uint, updates *UserGroup) (UserGroup, error) {
51+
var userGroup UserGroup
52+
53+
// Check if the user is trying to update another user's role
54+
if updates.UserID != 0 && updates.UserID != userID {
55+
return userGroup, database.HandleDBError(nil, "userGroup")
56+
}
57+
58+
err := db.Model(&userGroup).
59+
Preload(clause.Associations).
60+
Where(UserGroup{UserID: userID}).
61+
Updates(&userGroup).
62+
Error
63+
64+
if err != nil {
65+
return userGroup, database.HandleDBError(err, "userGroup")
66+
}
67+
return userGroup, nil
68+
}

params/usergroups/update.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package usergroupparams
2+
3+
import (
4+
"fmt"
5+
6+
groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
7+
"github.com/source-academy/stories-backend/model"
8+
)
9+
10+
type UpdateRole struct {
11+
Role groupenums.Role `json:"role"`
12+
}
13+
14+
func (params *UpdateRole) ToModel(userID uint) *model.UserGroup {
15+
return &model.UserGroup{
16+
UserID: userID,
17+
Role: params.Role,
18+
}
19+
}
20+
21+
func (params *UpdateRole) Validate() error {
22+
// Extra params won't do anything, e.g. authorID can't be changed.
23+
// TODO: Error on extra params?
24+
if !params.Role.IsValid() {
25+
return fmt.Errorf("Invalid role %s.", params.Role)
26+
}
27+
return nil
28+
}

view/users/list.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
package userviews
22

3-
import "github.com/source-academy/stories-backend/model"
3+
import (
4+
groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
5+
"github.com/source-academy/stories-backend/model"
6+
)
47

58
type ListView struct {
6-
Name string `json:"name"`
7-
Username string `json:"username"`
8-
LoginProvider string `json:"provider"`
9+
ID uint `json:"id"`
10+
Name string `json:"name"`
11+
Username string `json:"username"`
12+
LoginProvider string `json:"provider"`
13+
Role groupenums.Role `json:"role"`
914
}
1015

11-
func ListFrom(users []model.User) []ListView {
16+
func ListFrom(users []model.User, roles []groupenums.Role) []ListView {
1217
usersListView := make([]ListView, len(users))
1318
for i, user := range users {
1419
usersListView[i] = ListView{
1520
// Unlike other views, we do not fallback an empty name to
1621
// the username for the users' list view.
22+
ID: user.ID,
1723
Name: user.FullName,
1824
Username: user.Username,
1925
LoginProvider: user.LoginProvider.ToString(),
26+
Role: roles[i],
2027
}
2128
}
2229
return usersListView

0 commit comments

Comments
 (0)