4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
242 lines
7.7 KiB
Go
242 lines
7.7 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
|
storepb "github.com/usememos/memos/proto/gen/store"
|
|
"github.com/usememos/memos/store"
|
|
)
|
|
|
|
func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) {
|
|
// Parse resource name: users/{user}/settings/{setting}
|
|
user, settingKey, err := s.resolveUserAndSettingKeyFromName(ctx, request.Name)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err)
|
|
}
|
|
userID := user.ID
|
|
|
|
currentUser, err := s.fetchCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Only allow user to get their own settings
|
|
if currentUser.ID != userID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
// Convert setting key string to store enum
|
|
storeKey, err := convertSettingKeyToStore(settingKey)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err)
|
|
}
|
|
|
|
userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
|
UserID: &userID,
|
|
Key: storeKey,
|
|
})
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err)
|
|
}
|
|
|
|
return convertUserSettingFromStore(userSetting, user, storeKey), nil
|
|
}
|
|
|
|
func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.UpdateUserSettingRequest) (*v1pb.UserSetting, error) {
|
|
// Parse resource name: users/{user}/settings/{setting}
|
|
user, settingKey, err := s.resolveUserAndSettingKeyFromName(ctx, request.Setting.Name)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err)
|
|
}
|
|
userID := user.ID
|
|
|
|
currentUser, err := s.fetchCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Only allow user to update their own settings
|
|
if currentUser.ID != userID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
|
|
}
|
|
|
|
// Convert setting key string to store enum
|
|
storeKey, err := convertSettingKeyToStore(settingKey)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err)
|
|
}
|
|
|
|
var updatedSetting *v1pb.UserSetting
|
|
switch storeKey {
|
|
case storepb.UserSetting_GENERAL:
|
|
existingUserSetting, _ := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
|
|
UserID: &userID,
|
|
Key: storeKey,
|
|
})
|
|
|
|
generalSetting := &storepb.GeneralUserSetting{}
|
|
if existingUserSetting != nil {
|
|
// Start with existing general setting values.
|
|
generalSetting = existingUserSetting.GetGeneral()
|
|
}
|
|
|
|
updatedGeneral := &v1pb.UserSetting_GeneralSetting{
|
|
MemoVisibility: generalSetting.GetMemoVisibility(),
|
|
Locale: generalSetting.GetLocale(),
|
|
Theme: generalSetting.GetTheme(),
|
|
}
|
|
|
|
incomingGeneral := request.Setting.GetGeneralSetting()
|
|
if incomingGeneral == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "general setting is required")
|
|
}
|
|
for _, field := range request.UpdateMask.Paths {
|
|
switch field {
|
|
case "memo_visibility":
|
|
updatedGeneral.MemoVisibility = incomingGeneral.MemoVisibility
|
|
case "theme":
|
|
updatedGeneral.Theme = incomingGeneral.Theme
|
|
case "locale":
|
|
updatedGeneral.Locale = incomingGeneral.Locale
|
|
default:
|
|
// Ignore unsupported fields.
|
|
}
|
|
}
|
|
|
|
updatedSetting = &v1pb.UserSetting{
|
|
Name: request.Setting.Name,
|
|
Value: &v1pb.UserSetting_GeneralSetting_{
|
|
GeneralSetting: updatedGeneral,
|
|
},
|
|
}
|
|
case storepb.UserSetting_TAGS:
|
|
var shouldUpdateTags bool
|
|
for _, field := range request.UpdateMask.Paths {
|
|
switch field {
|
|
case "tags":
|
|
shouldUpdateTags = true
|
|
default:
|
|
return nil, status.Errorf(codes.InvalidArgument, "unsupported update mask path for tags setting: %s", field)
|
|
}
|
|
}
|
|
if !shouldUpdateTags {
|
|
return nil, status.Errorf(codes.InvalidArgument, "update mask must include tags")
|
|
}
|
|
|
|
incomingTags := request.Setting.GetTagsSetting()
|
|
if incomingTags == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "tags setting is required")
|
|
}
|
|
if err := validateUserTagsSetting(incomingTags); err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid user tags setting: %v", err)
|
|
}
|
|
updatedSetting = &v1pb.UserSetting{
|
|
Name: request.Setting.Name,
|
|
Value: &v1pb.UserSetting_TagsSetting_{
|
|
TagsSetting: incomingTags,
|
|
},
|
|
}
|
|
default:
|
|
return nil, status.Errorf(codes.InvalidArgument, "setting type %s should not be updated via UpdateUserSetting", storeKey.String())
|
|
}
|
|
|
|
// Convert API setting to store setting
|
|
storeSetting, err := convertUserSettingToStore(updatedSetting, userID, storeKey)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to convert setting: %v", err)
|
|
}
|
|
|
|
// Upsert the setting
|
|
if _, err := s.Store.UpsertUserSetting(ctx, storeSetting); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
|
|
}
|
|
|
|
return s.GetUserSetting(ctx, &v1pb.GetUserSettingRequest{Name: request.Setting.Name})
|
|
}
|
|
|
|
func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListUserSettingsRequest) (*v1pb.ListUserSettingsResponse, error) {
|
|
user, err := s.resolveUserFromName(ctx, request.Parent)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid parent name: %v", err)
|
|
}
|
|
userID := user.ID
|
|
|
|
currentUser, err := s.fetchCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Only allow user to list their own settings
|
|
if currentUser.ID != userID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{
|
|
UserID: &userID,
|
|
})
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to list user settings: %v", err)
|
|
}
|
|
|
|
settings := make([]*v1pb.UserSetting, 0, len(userSettings))
|
|
for _, storeSetting := range userSettings {
|
|
apiSetting := convertUserSettingFromStore(storeSetting, user, storeSetting.Key)
|
|
if apiSetting != nil {
|
|
settings = append(settings, apiSetting)
|
|
}
|
|
}
|
|
|
|
hasGeneral := false
|
|
for _, setting := range settings {
|
|
if setting.GetGeneralSetting() != nil {
|
|
hasGeneral = true
|
|
}
|
|
}
|
|
if !hasGeneral {
|
|
defaultGeneral := &v1pb.UserSetting{
|
|
Name: fmt.Sprintf("%s/settings/%s", BuildUserName(user.Username), convertSettingKeyFromStore(storepb.UserSetting_GENERAL)),
|
|
Value: &v1pb.UserSetting_GeneralSetting_{
|
|
GeneralSetting: getDefaultUserGeneralSetting(),
|
|
},
|
|
}
|
|
settings = append([]*v1pb.UserSetting{defaultGeneral}, settings...)
|
|
}
|
|
response := &v1pb.ListUserSettingsResponse{
|
|
Settings: settings,
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (s *APIV1Service) authorizeUserResourceAccess(ctx context.Context, userID int32, allowAdmin bool) (*store.User, error) {
|
|
currentUser, err := s.fetchCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
if currentUser.ID == userID || (allowAdmin && currentUser.Role == store.RoleAdmin) {
|
|
return currentUser, nil
|
|
}
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|