Skip to content

Commit fffc2b7

Browse files
committed
feat: enable removal and update of role from a user in an organization
1 parent 0d60d81 commit fffc2b7

File tree

13 files changed

+296
-105
lines changed

13 files changed

+296
-105
lines changed

api/api/versions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"version": "v1",
55
"status": "active",
6-
"release_date": "2025-10-04T20:22:44.016248162+05:30",
6+
"release_date": "2025-10-04T21:33:19.645770465+05:30",
77
"end_of_life": "0001-01-01T00:00:00Z",
88
"changes": [
99
"Initial API version"

api/doc/openapi.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,52 @@
11
package controller
22

3-
// import (
4-
// "net/http"
5-
6-
// "github.com/go-fuego/fuego"
7-
// "github.com/raghavyuva/nixopus-api/internal/features/notification"
8-
// "github.com/raghavyuva/nixopus-api/internal/features/organization/types"
9-
// shared_types "github.com/raghavyuva/nixopus-api/internal/types"
10-
// "github.com/raghavyuva/nixopus-api/internal/utils"
11-
// )
12-
13-
// func (c *OrganizationsController) RemoveUserFromOrganization(f fuego.ContextWithBody[types.RemoveUserFromOrganizationRequest]) (*shared_types.Response, error) {
14-
// _, r := f.Response(), f.Request()
15-
// user, err := f.Body()
16-
// if err != nil {
17-
// return nil, fuego.HTTPError{
18-
// Err: err,
19-
// Status: http.StatusBadRequest,
20-
// }
21-
// }
22-
23-
// loggedInUser := utils.GetUser(f.Response(), r)
24-
// if loggedInUser == nil {
25-
// return nil, fuego.HTTPError{
26-
// Err: nil,
27-
// Status: http.StatusUnauthorized,
28-
// }
29-
// }
30-
31-
// if err := c.service.RemoveUserFromOrganization(&user); err != nil {
32-
// return nil, fuego.HTTPError{
33-
// Err: err,
34-
// Status: http.StatusInternalServerError,
35-
// }
36-
// }
37-
38-
// org, err := c.service.GetOrganization(user.OrganizationID)
39-
// if err != nil {
40-
// return nil, fuego.HTTPError{
41-
// Err: err,
42-
// Status: http.StatusInternalServerError,
43-
// }
44-
// }
45-
46-
// userDetails := utils.GetUser(f.Response(), r)
47-
// if userDetails == nil {
48-
// return nil, fuego.HTTPError{
49-
// Err: nil,
50-
// Status: http.StatusUnauthorized,
51-
// }
52-
// }
53-
54-
// c.Notify(notification.NotificationPayloadTypeRemoveUserFromOrganization, loggedInUser, r, notification.RemoveUserFromOrganizationData{
55-
// NotificationBaseData: notification.NotificationBaseData{
56-
// IP: r.RemoteAddr,
57-
// Browser: r.UserAgent(),
58-
// },
59-
// OrganizationName: org.Name,
60-
// UserName: userDetails.Username,
61-
// UserEmail: userDetails.Email,
62-
// })
63-
64-
// return &shared_types.Response{
65-
// Status: "success",
66-
// Message: "User removed from organization successfully",
67-
// Data: nil,
68-
// }, nil
69-
// }
3+
import (
4+
"net/http"
5+
6+
"github.com/go-fuego/fuego"
7+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
8+
"github.com/raghavyuva/nixopus-api/internal/features/organization/types"
9+
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
10+
"github.com/raghavyuva/nixopus-api/internal/utils"
11+
)
12+
13+
// TODO: Here we need to make sure when a user is removed from an organization, if no organization is left for the user, we should remove the user from the system.
14+
func (c *OrganizationsController) RemoveUserFromOrganization(f fuego.ContextWithBody[types.RemoveUserFromOrganizationRequest]) (*shared_types.Response, error) {
15+
_, r := f.Response(), f.Request()
16+
user, err := f.Body()
17+
if err != nil {
18+
return nil, fuego.HTTPError{
19+
Err: err,
20+
Status: http.StatusBadRequest,
21+
}
22+
}
23+
24+
loggedInUser := utils.GetUser(f.Response(), r)
25+
if loggedInUser == nil {
26+
return nil, fuego.HTTPError{
27+
Err: nil,
28+
Status: http.StatusUnauthorized,
29+
}
30+
}
31+
32+
if err := c.validator.ValidateRequest(&user); err != nil {
33+
c.logger.Log(logger.Error, err.Error(), "")
34+
return nil, fuego.HTTPError{
35+
Err: err,
36+
Status: http.StatusBadRequest,
37+
}
38+
}
39+
40+
if err := c.service.RemoveUserFromOrganization(&user); err != nil {
41+
return nil, fuego.HTTPError{
42+
Err: err,
43+
Status: http.StatusInternalServerError,
44+
}
45+
}
46+
47+
return &shared_types.Response{
48+
Status: "success",
49+
Message: "User removed from organization successfully",
50+
Data: nil,
51+
}, nil
52+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package controller
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/go-fuego/fuego"
7+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
8+
"github.com/raghavyuva/nixopus-api/internal/features/organization/types"
9+
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
10+
"github.com/raghavyuva/nixopus-api/internal/utils"
11+
)
12+
13+
func (c *OrganizationsController) UpdateUserRole(f fuego.ContextWithBody[types.UpdateUserRoleRequest]) (*shared_types.Response, error) {
14+
_, r := f.Response(), f.Request()
15+
request, err := f.Body()
16+
if err != nil {
17+
return nil, fuego.HTTPError{
18+
Err: err,
19+
Status: http.StatusBadRequest,
20+
}
21+
}
22+
23+
loggedInUser := utils.GetUser(f.Response(), r)
24+
if loggedInUser == nil {
25+
return nil, fuego.HTTPError{
26+
Err: nil,
27+
Status: http.StatusUnauthorized,
28+
}
29+
}
30+
31+
if err := c.validator.ValidateRequest(&request); err != nil {
32+
c.logger.Log(logger.Error, err.Error(), "")
33+
return nil, fuego.HTTPError{
34+
Err: err,
35+
Status: http.StatusBadRequest,
36+
}
37+
}
38+
39+
if err := c.service.UpdateUserRole(&request); err != nil {
40+
return nil, fuego.HTTPError{
41+
Err: err,
42+
Status: http.StatusInternalServerError,
43+
}
44+
}
45+
46+
return &shared_types.Response{
47+
Status: "success",
48+
Message: "User role updated successfully",
49+
Data: nil,
50+
}, nil
51+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/uuid"
7+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
8+
"github.com/raghavyuva/nixopus-api/internal/features/organization/types"
9+
"github.com/supertokens/supertokens-golang/recipe/userroles"
10+
)
11+
12+
func (o *OrganizationService) RemoveUserFromOrganization(request *types.RemoveUserFromOrganizationRequest) error {
13+
o.logger.Log(logger.Info, "removing user from organization", request.UserID)
14+
15+
existingOrganization, err := o.storage.GetOrganization(request.OrganizationID)
16+
if err != nil || existingOrganization.ID == uuid.Nil {
17+
o.logger.Log(logger.Error, types.ErrOrganizationDoesNotExist.Error(), "")
18+
return types.ErrOrganizationDoesNotExist
19+
}
20+
21+
existingUser, err := o.user_storage.FindUserByID(request.UserID)
22+
if err != nil || existingUser.ID == uuid.Nil {
23+
o.logger.Log(logger.Error, types.ErrUserDoesNotExist.Error(), "")
24+
return types.ErrUserDoesNotExist
25+
}
26+
27+
existingUserInOrganization, err := o.storage.FindUserInOrganization(request.UserID, request.OrganizationID)
28+
if err != nil || existingUserInOrganization.ID == uuid.Nil {
29+
o.logger.Log(logger.Error, types.ErrUserNotInOrganization.Error(), "")
30+
return types.ErrUserNotInOrganization
31+
}
32+
33+
if err := o.storage.RemoveUserFromOrganization(request.UserID, request.OrganizationID); err != nil {
34+
o.logger.Log(logger.Error, types.ErrFailedToRemoveUserFromOrganization.Error(), err.Error())
35+
return types.ErrFailedToRemoveUserFromOrganization
36+
}
37+
38+
if existingUser.SupertokensUserID != "" {
39+
roleName := fmt.Sprintf("orgid_%s_admin", request.OrganizationID)
40+
if _, err := userroles.RemoveUserRole("public", existingUser.SupertokensUserID, roleName, nil); err != nil {
41+
o.logger.Log(logger.Warning, "failed to remove SuperTokens role", err.Error())
42+
}
43+
44+
roleName = fmt.Sprintf("orgid_%s_member", request.OrganizationID)
45+
if _, err := userroles.RemoveUserRole("public", existingUser.SupertokensUserID, roleName, nil); err != nil {
46+
o.logger.Log(logger.Warning, "failed to remove SuperTokens role", err.Error())
47+
}
48+
49+
roleName = fmt.Sprintf("orgid_%s_viewer", request.OrganizationID)
50+
if _, err := userroles.RemoveUserRole("public", existingUser.SupertokensUserID, roleName, nil); err != nil {
51+
o.logger.Log(logger.Warning, "failed to remove SuperTokens role", err.Error())
52+
}
53+
}
54+
55+
if err := o.cache.InvalidateOrgMembership(o.Ctx, request.UserID, request.OrganizationID); err != nil {
56+
o.logger.Log(logger.Error, "failed to invalidate organization membership cache", err.Error())
57+
}
58+
59+
o.logger.Log(logger.Info, "user removed from organization successfully", request.UserID)
60+
return nil
61+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/uuid"
7+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
8+
"github.com/raghavyuva/nixopus-api/internal/features/organization/types"
9+
"github.com/raghavyuva/nixopus-api/internal/features/supertokens"
10+
"github.com/supertokens/supertokens-golang/recipe/userroles"
11+
)
12+
13+
func (o *OrganizationService) UpdateUserRole(request *types.UpdateUserRoleRequest) error {
14+
o.logger.Log(logger.Info, "updating user role", request.UserID)
15+
16+
existingOrganization, err := o.storage.GetOrganization(request.OrganizationID)
17+
if err != nil || existingOrganization.ID == uuid.Nil {
18+
o.logger.Log(logger.Error, types.ErrOrganizationDoesNotExist.Error(), "")
19+
return types.ErrOrganizationDoesNotExist
20+
}
21+
22+
existingUser, err := o.user_storage.FindUserByID(request.UserID)
23+
if err != nil || existingUser.ID == uuid.Nil {
24+
o.logger.Log(logger.Error, types.ErrUserDoesNotExist.Error(), "")
25+
return types.ErrUserDoesNotExist
26+
}
27+
28+
existingUserInOrganization, err := o.storage.FindUserInOrganization(request.UserID, request.OrganizationID)
29+
if err != nil || existingUserInOrganization.ID == uuid.Nil {
30+
o.logger.Log(logger.Error, types.ErrUserNotInOrganization.Error(), "")
31+
return types.ErrUserNotInOrganization
32+
}
33+
34+
if existingUser.SupertokensUserID != "" {
35+
oldRoleName := fmt.Sprintf("orgid_%s_admin", request.OrganizationID)
36+
if _, err := userroles.RemoveUserRole("public", existingUser.SupertokensUserID, oldRoleName, nil); err != nil {
37+
o.logger.Log(logger.Warning, "failed to remove old SuperTokens role", err.Error())
38+
}
39+
40+
oldRoleName = fmt.Sprintf("orgid_%s_member", request.OrganizationID)
41+
if _, err := userroles.RemoveUserRole("public", existingUser.SupertokensUserID, oldRoleName, nil); err != nil {
42+
o.logger.Log(logger.Warning, "failed to remove old SuperTokens role", err.Error())
43+
}
44+
45+
oldRoleName = fmt.Sprintf("orgid_%s_viewer", request.OrganizationID)
46+
if _, err := userroles.RemoveUserRole("public", existingUser.SupertokensUserID, oldRoleName, nil); err != nil {
47+
o.logger.Log(logger.Warning, "failed to remove old SuperTokens role", err.Error())
48+
}
49+
50+
newRoleName := fmt.Sprintf("orgid_%s_%s", request.OrganizationID, request.Role)
51+
var permissions []string
52+
switch request.Role {
53+
case "admin":
54+
permissions = supertokens.GetAdminPermissions()
55+
case "member":
56+
permissions = supertokens.GetMemberPermissions()
57+
case "viewer":
58+
permissions = supertokens.GetViewerPermissions()
59+
default:
60+
permissions = supertokens.GetViewerPermissions()
61+
}
62+
63+
if _, err := userroles.CreateNewRoleOrAddPermissions(newRoleName, permissions, nil); err != nil {
64+
o.logger.Log(logger.Error, "failed to create SuperTokens role", err.Error())
65+
return types.ErrFailedToUpdateUserRole
66+
}
67+
68+
if _, err := userroles.AddRoleToUser("public", existingUser.SupertokensUserID, newRoleName, nil); err != nil {
69+
o.logger.Log(logger.Error, "failed to assign SuperTokens role", err.Error())
70+
return types.ErrFailedToUpdateUserRole
71+
}
72+
}
73+
74+
if err := o.cache.InvalidateOrgMembership(o.Ctx, request.UserID, request.OrganizationID); err != nil {
75+
o.logger.Log(logger.Error, "failed to invalidate organization membership cache", err.Error())
76+
}
77+
78+
o.logger.Log(logger.Info, "user role updated successfully", request.UserID)
79+
return nil
80+
}

api/internal/features/organization/types/organization.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ type InviteResendRequest struct {
5959
Role string `json:"role"`
6060
}
6161

62+
type UpdateUserRoleRequest struct {
63+
UserID string `json:"user_id"`
64+
OrganizationID string `json:"organization_id"`
65+
Role string `json:"role"`
66+
}
67+
6268
func NewOrganization(name string, description string) shared_types.Organization {
6369
return shared_types.Organization{
6470
ID: uuid.New(),

api/internal/features/organization/validation/validator.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ func (v *Validator) ValidateRequest(req interface{}) error {
8282
return v.validateInviteSend(*r)
8383
case *types.InviteResendRequest:
8484
return v.validateInviteResend(*r)
85+
case *types.UpdateUserRoleRequest:
86+
return v.validateUpdateUserRole(*r)
8587
default:
8688
return types.ErrInvalidRequestType
8789
}
@@ -230,7 +232,28 @@ func (v *Validator) validateInviteResend(req types.InviteResendRequest) error {
230232
return nil
231233
}
232234

235+
func (v *Validator) validateUpdateUserRole(req types.UpdateUserRoleRequest) error {
236+
if err := v.ValidateID(req.OrganizationID, types.ErrMissingOrganizationID); err != nil {
237+
return err
238+
}
239+
if err := v.ValidateID(req.UserID, types.ErrMissingUserID); err != nil {
240+
return err
241+
}
242+
if req.Role == "" {
243+
return types.ErrMissingRoleID
244+
}
245+
246+
organization, err := v.storage.GetOrganization(req.OrganizationID)
247+
if err != nil {
248+
return err
249+
}
250+
if organization == nil {
251+
return types.ErrOrganizationNotFound
252+
}
253+
254+
return nil
255+
}
256+
233257
func (v *Validator) ParseRequestBody(req interface{}, body io.ReadCloser, decoded interface{}) error {
234258
return json.NewDecoder(body).Decode(decoded)
235259
}
236-

api/internal/routes.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,8 @@ func (router *Router) ContainerRoutes(s *fuego.Server, containerController *cont
418418

419419
func (router *Router) OrganizationRoutes(f *fuego.Server, organizationController *organization.OrganizationsController) {
420420
fuego.Get(f, "/users", organizationController.GetOrganizationUsers)
421-
// fuego.Post(f, "/remove-user", organizationController.RemoveUserFromOrganization)
421+
fuego.Post(f, "/remove-user", organizationController.RemoveUserFromOrganization)
422+
fuego.Post(f, "/update-user-role", organizationController.UpdateUserRole)
422423
fuego.Put(f, "", organizationController.UpdateOrganization)
423424
fuego.Post(f, "", organizationController.CreateOrganization)
424425
fuego.Delete(f, "", organizationController.DeleteOrganization)

view/app/settings/hooks/use-team-settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function useTeamSettings() {
105105
await updateUserRole({
106106
user_id: userId,
107107
organization_id: activeOrganization?.id || '',
108-
role_name: role
108+
role: role.toLowerCase()
109109
});
110110
await refetchUsers();
111111
toast.success(t('settings.teams.messages.userUpdated'));

0 commit comments

Comments
 (0)