enhancement: refine the profile photo service and introduce httpDataProviders which allows reusing the endpoints

This commit is contained in:
Florian Schade
2025-05-20 14:27:21 +02:00
parent eccc900918
commit 250400639a
6 changed files with 452 additions and 117 deletions

View File

@@ -16,6 +16,7 @@ packages:
HTTPClient:
Permissions:
RoleService:
UsersUserProfilePhotoProvider:
github.com/opencloud-eu/reva/v2/pkg/events:
config:
dir: "mocks"

View File

@@ -0,0 +1,191 @@
// Code generated by mockery. DO NOT EDIT.
package mocks
import (
context "context"
io "io"
mock "github.com/stretchr/testify/mock"
)
// UsersUserProfilePhotoProvider is an autogenerated mock type for the UsersUserProfilePhotoProvider type
type UsersUserProfilePhotoProvider struct {
mock.Mock
}
type UsersUserProfilePhotoProvider_Expecter struct {
mock *mock.Mock
}
func (_m *UsersUserProfilePhotoProvider) EXPECT() *UsersUserProfilePhotoProvider_Expecter {
return &UsersUserProfilePhotoProvider_Expecter{mock: &_m.Mock}
}
// DeletePhoto provides a mock function with given fields: ctx, id
func (_m *UsersUserProfilePhotoProvider) DeletePhoto(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for DeletePhoto")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// UsersUserProfilePhotoProvider_DeletePhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePhoto'
type UsersUserProfilePhotoProvider_DeletePhoto_Call struct {
*mock.Call
}
// DeletePhoto is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *UsersUserProfilePhotoProvider_Expecter) DeletePhoto(ctx interface{}, id interface{}) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
return &UsersUserProfilePhotoProvider_DeletePhoto_Call{Call: _e.mock.On("DeletePhoto", ctx, id)}
}
func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) Run(run func(ctx context.Context, id string)) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) Return(_a0 error) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) RunAndReturn(run func(context.Context, string) error) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
_c.Call.Return(run)
return _c
}
// GetPhoto provides a mock function with given fields: ctx, id
func (_m *UsersUserProfilePhotoProvider) GetPhoto(ctx context.Context, id string) ([]byte, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for GetPhoto")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UsersUserProfilePhotoProvider_GetPhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPhoto'
type UsersUserProfilePhotoProvider_GetPhoto_Call struct {
*mock.Call
}
// GetPhoto is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *UsersUserProfilePhotoProvider_Expecter) GetPhoto(ctx interface{}, id interface{}) *UsersUserProfilePhotoProvider_GetPhoto_Call {
return &UsersUserProfilePhotoProvider_GetPhoto_Call{Call: _e.mock.On("GetPhoto", ctx, id)}
}
func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) Run(run func(ctx context.Context, id string)) *UsersUserProfilePhotoProvider_GetPhoto_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) Return(_a0 []byte, _a1 error) *UsersUserProfilePhotoProvider_GetPhoto_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) RunAndReturn(run func(context.Context, string) ([]byte, error)) *UsersUserProfilePhotoProvider_GetPhoto_Call {
_c.Call.Return(run)
return _c
}
// UpdatePhoto provides a mock function with given fields: ctx, id, rc
func (_m *UsersUserProfilePhotoProvider) UpdatePhoto(ctx context.Context, id string, rc io.Reader) error {
ret := _m.Called(ctx, id, rc)
if len(ret) == 0 {
panic("no return value specified for UpdatePhoto")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, io.Reader) error); ok {
r0 = rf(ctx, id, rc)
} else {
r0 = ret.Error(0)
}
return r0
}
// UsersUserProfilePhotoProvider_UpdatePhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePhoto'
type UsersUserProfilePhotoProvider_UpdatePhoto_Call struct {
*mock.Call
}
// UpdatePhoto is a helper method to define mock.On call
// - ctx context.Context
// - id string
// - rc io.Reader
func (_e *UsersUserProfilePhotoProvider_Expecter) UpdatePhoto(ctx interface{}, id interface{}, rc interface{}) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
return &UsersUserProfilePhotoProvider_UpdatePhoto_Call{Call: _e.mock.On("UpdatePhoto", ctx, id, rc)}
}
func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Run(run func(ctx context.Context, id string, rc io.Reader)) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(io.Reader))
})
return _c
}
func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Return(_a0 error) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) RunAndReturn(run func(context.Context, string, io.Reader) error) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
_c.Call.Return(run)
return _c
}
// NewUsersUserProfilePhotoProvider creates a new instance of UsersUserProfilePhotoProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewUsersUserProfilePhotoProvider(t interface {
mock.TestingT
Cleanup(func())
}) *UsersUserProfilePhotoProvider {
mock := &UsersUserProfilePhotoProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -7,7 +7,6 @@ import (
"net/http"
"github.com/go-chi/render"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/opencloud/pkg/log"
@@ -21,7 +20,7 @@ type (
GetPhoto(ctx context.Context, id string) ([]byte, error)
// UpdatePhoto retrieves the requested photo
UpdatePhoto(ctx context.Context, id string, rc io.Reader) error
UpdatePhoto(ctx context.Context, id string, r io.Reader) error
// DeletePhoto deletes the requested photo
DeletePhoto(ctx context.Context, id string) error
@@ -34,9 +33,6 @@ var (
// ErrNoBytes is returned when no bytes are found
ErrNoBytes = errors.New("no bytes")
// ErrNoUser is returned when no user is found
ErrNoUser = errors.New("no user found")
)
// UsersUserProfilePhotoService is the implementation of the UsersUserProfilePhotoProvider interface
@@ -71,8 +67,8 @@ func (s UsersUserProfilePhotoService) DeletePhoto(ctx context.Context, id string
}
// UpdatePhoto updates the requested photo
func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, rc io.Reader) error {
photo, err := io.ReadAll(rc)
func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, r io.Reader) error {
photo, err := io.ReadAll(r)
if err != nil {
return err
}
@@ -98,66 +94,61 @@ func NewUsersUserProfilePhotoApi(usersUserProfilePhotoService UsersUserProfilePh
}, nil
}
// GetProfilePhoto provides the requested photo
func (api UsersUserProfilePhotoApi) GetProfilePhoto(w http.ResponseWriter, r *http.Request) {
id, ok := api.getUserID(w, r)
if !ok {
return
}
// GetProfilePhoto creates a handler which renders the corresponding photo
func (api UsersUserProfilePhotoApi) GetProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, ok := h(w, r)
if !ok {
return
}
photo, err := api.usersUserProfilePhotoService.GetPhoto(r.Context(), id)
if err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to get photo")
return
}
photo, err := api.usersUserProfilePhotoService.GetPhoto(r.Context(), v)
if err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to get photo")
return
}
render.Status(r, http.StatusOK)
_, _ = w.Write(photo)
render.Status(r, http.StatusOK)
_, _ = w.Write(photo)
}
}
// UpsertProfilePhoto updates or inserts (initial create) the requested photo
func (api UsersUserProfilePhotoApi) UpsertProfilePhoto(w http.ResponseWriter, r *http.Request) {
id, ok := api.getUserID(w, r)
if !ok {
return
}
// UpsertProfilePhoto creates a handler which updates or creates the corresponding photo
func (api UsersUserProfilePhotoApi) UpsertProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, ok := h(w, r)
if !ok {
return
}
if err := api.usersUserProfilePhotoService.UpdatePhoto(r.Context(), id, r.Body); err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to update photo")
return
}
defer func() {
_ = r.Body.Close()
}()
if err := api.usersUserProfilePhotoService.UpdatePhoto(r.Context(), v, r.Body); err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "failed to update photo")
return
}
defer func() {
_ = r.Body.Close()
}()
render.Status(r, http.StatusOK)
render.Status(r, http.StatusOK)
}
}
// DeleteProfilePhoto deletes the requested photo
func (api UsersUserProfilePhotoApi) DeleteProfilePhoto(w http.ResponseWriter, r *http.Request) {
id, ok := api.getUserID(w, r)
if !ok {
return
}
// DeleteProfilePhoto creates a handler which deletes the corresponding photo
func (api UsersUserProfilePhotoApi) DeleteProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, ok := h(w, r)
if !ok {
return
}
if err := api.usersUserProfilePhotoService.DeletePhoto(r.Context(), id); err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to delete photo")
return
}
if err := api.usersUserProfilePhotoService.DeletePhoto(r.Context(), v); err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "failed to delete photo")
return
}
render.Status(r, http.StatusOK)
}
func (api UsersUserProfilePhotoApi) getUserID(w http.ResponseWriter, r *http.Request) (string, bool) {
u, ok := revactx.ContextGetUser(r.Context())
if !ok {
api.logger.Debug().Msg(ErrNoUser.Error())
errorcode.GeneralException.Render(w, r, http.StatusMethodNotAllowed, ErrNoUser.Error())
return "", false
}
return u.GetId().GetOpaqueId(), true
render.Status(r, http.StatusOK)
}
}

View File

@@ -1,13 +1,118 @@
package svc_test
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
svc "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0"
)
func TestNewUsersUserProfilePhotoApi(t *testing.T) {
panic("add UsersUserProfilePhotoApi tests")
}
func TestUsersUserProfilePhotoApi(t *testing.T) {
var (
usersUserProfilePhotoProvider = mocks.NewUsersUserProfilePhotoProvider(t)
dummyDataProvider = func(w http.ResponseWriter, r *http.Request) (string, bool) {
return "123", true
}
)
func TestNewUsersUserProfilePhotoService(t *testing.T) {
panic("add UsersUserProfilePhotoService tests")
api, err := svc.NewUsersUserProfilePhotoApi(usersUserProfilePhotoProvider, log.NopLogger())
assert.NoError(t, err)
t.Run("GetProfilePhoto", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
ep := api.GetProfilePhoto(dummyDataProvider)
t.Run("fails if photo provider errors", func(t *testing.T) {
w := httptest.NewRecorder()
usersUserProfilePhotoProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) {
return nil, errors.New("any")
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
})
t.Run("successfully returns the requested photo", func(t *testing.T) {
w := httptest.NewRecorder()
usersUserProfilePhotoProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) {
return []byte("photo"), nil
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "photo", w.Body.String())
})
})
t.Run("DeleteProfilePhoto", func(t *testing.T) {
r := httptest.NewRequest(http.MethodDelete, "/", nil)
ep := api.DeleteProfilePhoto(dummyDataProvider)
t.Run("fails if photo provider errors", func(t *testing.T) {
w := httptest.NewRecorder()
usersUserProfilePhotoProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error {
return errors.New("any")
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("successfully deletes the requested photo", func(t *testing.T) {
w := httptest.NewRecorder()
usersUserProfilePhotoProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error {
return nil
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
})
t.Run("UpsertProfilePhoto", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPut, "/", strings.NewReader("body"))
ep := api.UpsertProfilePhoto(dummyDataProvider)
t.Run("fails if photo provider errors", func(t *testing.T) {
w := httptest.NewRecorder()
usersUserProfilePhotoProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error {
return errors.New("any")
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("successfully upserts the photo", func(t *testing.T) {
w := httptest.NewRecorder()
usersUserProfilePhotoProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error {
return nil
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
})
}

View File

@@ -0,0 +1,42 @@
package svc
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/go-chi/chi/v5"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
)
// HTTPDataHandler returns data from the request, it should exit early and return false in the case of any error
type HTTPDataHandler[T any] func(w http.ResponseWriter, r *http.Request) (T, bool)
var (
// ErrNoUser is returned when no user is found
ErrNoUser = errors.New("no user found")
)
// GetUserIDFromCTX extracts the user from the request
func GetUserIDFromCTX(w http.ResponseWriter, r *http.Request) (string, bool) {
u, ok := revactx.ContextGetUser(r.Context())
if !ok {
errorcode.GeneralException.Render(w, r, http.StatusMethodNotAllowed, ErrNoUser.Error())
}
return u.GetId().GetOpaqueId(), ok
}
func GetSlugValue(key string) HTTPDataHandler[string] {
return func(w http.ResponseWriter, r *http.Request) (string, bool) {
v, err := url.PathUnescape(chi.URLParam(r, key))
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, fmt.Sprintf(`failed to get slug: "%s"`, key))
}
return v, err == nil
}
}

View File

@@ -144,14 +144,57 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
identity.IdentityCacheWithGroupsTTL(time.Duration(options.Config.Spaces.GroupsCacheTTL)),
)
storage, err := metadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return Graph{}, err
}
baseGraphService := BaseGraphService{
logger: &options.Logger,
identityCache: identityCache,
gatewaySelector: options.GatewaySelector,
config: options.Config,
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)),
}
drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector)
if err != nil {
return Graph{}, err
}
drivesDriveItemApi, err := NewDrivesDriveItemApi(drivesDriveItemService, baseGraphService, options.Logger)
if err != nil {
return Graph{}, err
}
driveItemPermissionsService, err := NewDriveItemPermissionsService(options.Logger, options.GatewaySelector, identityCache, options.Config)
if err != nil {
return Graph{}, err
}
driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger, options.Config)
if err != nil {
return Graph{}, err
}
usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(storage)
if err != nil {
return Graph{}, err
}
usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger)
if err != nil {
return Graph{}, err
}
svc := Graph{
BaseGraphService: BaseGraphService{
logger: &options.Logger,
identityCache: identityCache,
gatewaySelector: options.GatewaySelector,
config: options.Config,
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)),
},
BaseGraphService: baseGraphService,
mux: m,
specialDriveItemsCache: spacePropertiesCache,
eventsPublisher: options.EventsPublisher,
@@ -206,47 +249,6 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
requireAdmin = options.RequireAdminMiddleware
}
drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector)
if err != nil {
return svc, err
}
drivesDriveItemApi, err := NewDrivesDriveItemApi(drivesDriveItemService, svc.BaseGraphService, options.Logger)
if err != nil {
return svc, err
}
driveItemPermissionsService, err := NewDriveItemPermissionsService(options.Logger, options.GatewaySelector, identityCache, options.Config)
if err != nil {
return svc, err
}
driveItemPermissionsApi, err := NewDriveItemPermissionsApi(driveItemPermissionsService, options.Logger, options.Config)
if err != nil {
return svc, err
}
storage, err := metadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return svc, err
}
usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(storage)
if err != nil {
return svc, err
}
usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger)
if err != nil {
return svc, err
}
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.Use(middleware.StripSlashes)
@@ -315,11 +317,11 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
})
r.Get("/drives", svc.GetDrives(APIVersion_1))
r.Post("/changePassword", svc.ChangeOwnPassword)
r.Route("/photo", func(r chi.Router) {
r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto)
r.Put("/", usersUserProfilePhotoApi.UpsertProfilePhoto)
r.Patch("/", usersUserProfilePhotoApi.UpsertProfilePhoto)
r.Delete("/", usersUserProfilePhotoApi.DeleteProfilePhoto)
r.Route("/photo/$value", func(r chi.Router) {
r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto(GetUserIDFromCTX))
r.Put("/", usersUserProfilePhotoApi.UpsertProfilePhoto(GetUserIDFromCTX))
r.Patch("/", usersUserProfilePhotoApi.UpsertProfilePhoto(GetUserIDFromCTX))
r.Delete("/", usersUserProfilePhotoApi.DeleteProfilePhoto(GetUserIDFromCTX))
})
})
r.Route("/users", func(r chi.Router) {
@@ -329,6 +331,9 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
r.Get("/", svc.GetUser)
r.Get("/drive", svc.GetUserDrive)
r.Post("/exportPersonalData", svc.ExportPersonalData)
r.Route("/photo/$value", func(r chi.Router) {
r.Get("/", usersUserProfilePhotoApi.GetProfilePhoto(GetSlugValue("userID")))
})
r.With(requireAdmin).Delete("/", svc.DeleteUser)
r.With(requireAdmin).Patch("/", svc.PatchUser)
if svc.roleService != nil {