mirror of https://github.com/usememos/memos
chore: migrate user setting to api v1 package (#1855)
* chore: migrate to api v1 package * chore: updatepull/1856/head
parent
07e82c3f4a
commit
b44f2b5ffb
@ -0,0 +1,137 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import "github.com/usememos/memos/server/profile"
|
||||||
|
|
||||||
|
// ActivityType is the type for an activity.
|
||||||
|
type ActivityType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// User related.
|
||||||
|
|
||||||
|
// ActivityUserCreate is the type for creating users.
|
||||||
|
ActivityUserCreate ActivityType = "user.create"
|
||||||
|
// ActivityUserUpdate is the type for updating users.
|
||||||
|
ActivityUserUpdate ActivityType = "user.update"
|
||||||
|
// ActivityUserDelete is the type for deleting users.
|
||||||
|
ActivityUserDelete ActivityType = "user.delete"
|
||||||
|
// ActivityUserAuthSignIn is the type for user signin.
|
||||||
|
ActivityUserAuthSignIn ActivityType = "user.auth.signin"
|
||||||
|
// ActivityUserAuthSignUp is the type for user signup.
|
||||||
|
ActivityUserAuthSignUp ActivityType = "user.auth.signup"
|
||||||
|
// ActivityUserSettingUpdate is the type for updating user settings.
|
||||||
|
ActivityUserSettingUpdate ActivityType = "user.setting.update"
|
||||||
|
|
||||||
|
// Memo related.
|
||||||
|
|
||||||
|
// ActivityMemoCreate is the type for creating memos.
|
||||||
|
ActivityMemoCreate ActivityType = "memo.create"
|
||||||
|
// ActivityMemoUpdate is the type for updating memos.
|
||||||
|
ActivityMemoUpdate ActivityType = "memo.update"
|
||||||
|
// ActivityMemoDelete is the type for deleting memos.
|
||||||
|
ActivityMemoDelete ActivityType = "memo.delete"
|
||||||
|
|
||||||
|
// Shortcut related.
|
||||||
|
|
||||||
|
// ActivityShortcutCreate is the type for creating shortcuts.
|
||||||
|
ActivityShortcutCreate ActivityType = "shortcut.create"
|
||||||
|
// ActivityShortcutUpdate is the type for updating shortcuts.
|
||||||
|
ActivityShortcutUpdate ActivityType = "shortcut.update"
|
||||||
|
// ActivityShortcutDelete is the type for deleting shortcuts.
|
||||||
|
ActivityShortcutDelete ActivityType = "shortcut.delete"
|
||||||
|
|
||||||
|
// Resource related.
|
||||||
|
|
||||||
|
// ActivityResourceCreate is the type for creating resources.
|
||||||
|
ActivityResourceCreate ActivityType = "resource.create"
|
||||||
|
// ActivityResourceDelete is the type for deleting resources.
|
||||||
|
ActivityResourceDelete ActivityType = "resource.delete"
|
||||||
|
|
||||||
|
// Tag related.
|
||||||
|
|
||||||
|
// ActivityTagCreate is the type for creating tags.
|
||||||
|
ActivityTagCreate ActivityType = "tag.create"
|
||||||
|
// ActivityTagDelete is the type for deleting tags.
|
||||||
|
ActivityTagDelete ActivityType = "tag.delete"
|
||||||
|
|
||||||
|
// Server related.
|
||||||
|
|
||||||
|
// ActivityServerStart is the type for starting server.
|
||||||
|
ActivityServerStart ActivityType = "server.start"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActivityLevel is the level of activities.
|
||||||
|
type ActivityLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ActivityInfo is the INFO level of activities.
|
||||||
|
ActivityInfo ActivityLevel = "INFO"
|
||||||
|
// ActivityWarn is the WARN level of activities.
|
||||||
|
ActivityWarn ActivityLevel = "WARN"
|
||||||
|
// ActivityError is the ERROR level of activities.
|
||||||
|
ActivityError ActivityLevel = "ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityUserCreatePayload struct {
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityUserAuthSignInPayload struct {
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityUserAuthSignUpPayload struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityMemoCreatePayload struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityShortcutCreatePayload struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityResourceCreatePayload struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityTagCreatePayload struct {
|
||||||
|
TagName string `json:"tagName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityServerStartPayload struct {
|
||||||
|
ServerID string `json:"serverId"`
|
||||||
|
Profile *profile.Profile `json:"profile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Activity struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
|
||||||
|
// Standard fields
|
||||||
|
CreatorID int `json:"creatorId"`
|
||||||
|
CreatedTs int64 `json:"createdTs"`
|
||||||
|
|
||||||
|
// Domain specific fields
|
||||||
|
Type ActivityType `json:"type"`
|
||||||
|
Level ActivityLevel `json:"level"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityCreate is the API message for creating an activity.
|
||||||
|
type ActivityCreate struct {
|
||||||
|
// Standard fields
|
||||||
|
CreatorID int
|
||||||
|
|
||||||
|
// Domain specific fields
|
||||||
|
Type ActivityType `json:"type"`
|
||||||
|
Level ActivityLevel
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
// Visibility is the type of a visibility.
|
||||||
|
type Visibility string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Public is the PUBLIC visibility.
|
||||||
|
Public Visibility = "PUBLIC"
|
||||||
|
// Protected is the PROTECTED visibility.
|
||||||
|
Protected Visibility = "PROTECTED"
|
||||||
|
// Private is the PRIVATE visibility.
|
||||||
|
Private Visibility = "PRIVATE"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v Visibility) String() string {
|
||||||
|
switch v {
|
||||||
|
case Public:
|
||||||
|
return "PUBLIC"
|
||||||
|
case Protected:
|
||||||
|
return "PROTECTED"
|
||||||
|
case Private:
|
||||||
|
return "PRIVATE"
|
||||||
|
}
|
||||||
|
return "PRIVATE"
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemSettingName string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SystemSettingServerIDName is the name of server id.
|
||||||
|
SystemSettingServerIDName SystemSettingName = "server-id"
|
||||||
|
// SystemSettingSecretSessionName is the name of secret session.
|
||||||
|
SystemSettingSecretSessionName SystemSettingName = "secret-session"
|
||||||
|
// SystemSettingAllowSignUpName is the name of allow signup setting.
|
||||||
|
SystemSettingAllowSignUpName SystemSettingName = "allow-signup"
|
||||||
|
// SystemSettingDisablePublicMemosName is the name of disable public memos setting.
|
||||||
|
SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos"
|
||||||
|
// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
|
||||||
|
SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
|
||||||
|
// SystemSettingAdditionalStyleName is the name of additional style.
|
||||||
|
SystemSettingAdditionalStyleName SystemSettingName = "additional-style"
|
||||||
|
// SystemSettingAdditionalScriptName is the name of additional script.
|
||||||
|
SystemSettingAdditionalScriptName SystemSettingName = "additional-script"
|
||||||
|
// SystemSettingCustomizedProfileName is the name of customized server profile.
|
||||||
|
SystemSettingCustomizedProfileName SystemSettingName = "customized-profile"
|
||||||
|
// SystemSettingStorageServiceIDName is the name of storage service ID.
|
||||||
|
SystemSettingStorageServiceIDName SystemSettingName = "storage-service-id"
|
||||||
|
// SystemSettingLocalStoragePathName is the name of local storage path.
|
||||||
|
SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path"
|
||||||
|
// SystemSettingOpenAIConfigName is the name of OpenAI config.
|
||||||
|
SystemSettingOpenAIConfigName SystemSettingName = "openai-config"
|
||||||
|
// SystemSettingTelegramBotToken is the name of Telegram Bot Token.
|
||||||
|
SystemSettingTelegramBotTokenName SystemSettingName = "telegram-bot-token"
|
||||||
|
SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
|
||||||
|
type CustomizedProfile struct {
|
||||||
|
// Name is the server name, default is `memos`
|
||||||
|
Name string `json:"name"`
|
||||||
|
// LogoURL is the url of logo image.
|
||||||
|
LogoURL string `json:"logoUrl"`
|
||||||
|
// Description is the server description.
|
||||||
|
Description string `json:"description"`
|
||||||
|
// Locale is the server default locale.
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
// Appearance is the server default appearance.
|
||||||
|
Appearance string `json:"appearance"`
|
||||||
|
// ExternalURL is the external url of server. e.g. https://usermemos.com
|
||||||
|
ExternalURL string `json:"externalUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenAIConfig struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key SystemSettingName) String() string {
|
||||||
|
switch key {
|
||||||
|
case SystemSettingServerIDName:
|
||||||
|
return "server-id"
|
||||||
|
case SystemSettingSecretSessionName:
|
||||||
|
return "secret-session"
|
||||||
|
case SystemSettingAllowSignUpName:
|
||||||
|
return "allow-signup"
|
||||||
|
case SystemSettingDisablePublicMemosName:
|
||||||
|
return "disable-public-memos"
|
||||||
|
case SystemSettingMaxUploadSizeMiBName:
|
||||||
|
return "max-upload-size-mib"
|
||||||
|
case SystemSettingAdditionalStyleName:
|
||||||
|
return "additional-style"
|
||||||
|
case SystemSettingAdditionalScriptName:
|
||||||
|
return "additional-script"
|
||||||
|
case SystemSettingCustomizedProfileName:
|
||||||
|
return "customized-profile"
|
||||||
|
case SystemSettingStorageServiceIDName:
|
||||||
|
return "storage-service-id"
|
||||||
|
case SystemSettingLocalStoragePathName:
|
||||||
|
return "local-storage-path"
|
||||||
|
case SystemSettingOpenAIConfigName:
|
||||||
|
return "openai-config"
|
||||||
|
case SystemSettingTelegramBotTokenName:
|
||||||
|
return "telegram-bot-token"
|
||||||
|
case SystemSettingMemoDisplayWithUpdatedTsName:
|
||||||
|
return "memo-display-with-updated-ts"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemSetting struct {
|
||||||
|
Name SystemSettingName `json:"name"`
|
||||||
|
// Value is a JSON string with basic value.
|
||||||
|
Value string `json:"value"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemSettingUpsert struct {
|
||||||
|
Name SystemSettingName `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
|
||||||
|
|
||||||
|
func (upsert SystemSettingUpsert) Validate() error {
|
||||||
|
switch settingName := upsert.Name; settingName {
|
||||||
|
case SystemSettingServerIDName:
|
||||||
|
return fmt.Errorf("updating %v is not allowed", settingName)
|
||||||
|
case SystemSettingAllowSignUpName:
|
||||||
|
var value bool
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingDisablePublicMemosName:
|
||||||
|
var value bool
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingMaxUploadSizeMiBName:
|
||||||
|
var value int
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingAdditionalStyleName:
|
||||||
|
var value string
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingAdditionalScriptName:
|
||||||
|
var value string
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingCustomizedProfileName:
|
||||||
|
customizedProfile := CustomizedProfile{
|
||||||
|
Name: "memos",
|
||||||
|
LogoURL: "",
|
||||||
|
Description: "",
|
||||||
|
Locale: "en",
|
||||||
|
Appearance: "system",
|
||||||
|
ExternalURL: "",
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingStorageServiceIDName:
|
||||||
|
// Note: 0 is the default value(database) for storage service ID.
|
||||||
|
value := 0
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case SystemSettingLocalStoragePathName:
|
||||||
|
value := ""
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingOpenAIConfigName:
|
||||||
|
value := OpenAIConfig{}
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingTelegramBotTokenName:
|
||||||
|
if upsert.Value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Bot Token with Reverse Proxy shoule like `http.../bot<token>`
|
||||||
|
if strings.HasPrefix(upsert.Value, "http") {
|
||||||
|
slashIndex := strings.LastIndexAny(upsert.Value, "/")
|
||||||
|
if strings.HasPrefix(upsert.Value[slashIndex:], "/bot") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("token start with `http` must end with `/bot<token>`")
|
||||||
|
}
|
||||||
|
fragments := strings.Split(upsert.Value, ":")
|
||||||
|
if len(fragments) != 2 {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
case SystemSettingMemoDisplayWithUpdatedTsName:
|
||||||
|
var value bool
|
||||||
|
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||||
|
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid system setting name")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemSettingFind struct {
|
||||||
|
Name SystemSettingName `json:"name"`
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserSettingKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UserSettingLocaleKey is the key type for user locale.
|
||||||
|
UserSettingLocaleKey UserSettingKey = "locale"
|
||||||
|
// UserSettingAppearanceKey is the key type for user appearance.
|
||||||
|
UserSettingAppearanceKey UserSettingKey = "appearance"
|
||||||
|
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
|
||||||
|
UserSettingMemoVisibilityKey UserSettingKey = "memo-visibility"
|
||||||
|
// UserSettingTelegramUserID is the key type for telegram UserID of memos user.
|
||||||
|
UserSettingTelegramUserIDKey UserSettingKey = "telegram-user-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string format of UserSettingKey type.
|
||||||
|
func (key UserSettingKey) String() string {
|
||||||
|
switch key {
|
||||||
|
case UserSettingLocaleKey:
|
||||||
|
return "locale"
|
||||||
|
case UserSettingAppearanceKey:
|
||||||
|
return "appearance"
|
||||||
|
case UserSettingMemoVisibilityKey:
|
||||||
|
return "memo-visibility"
|
||||||
|
case UserSettingTelegramUserIDKey:
|
||||||
|
return "telegram-user-id"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
UserSettingLocaleValue = []string{
|
||||||
|
"de",
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"hr",
|
||||||
|
"it",
|
||||||
|
"ja",
|
||||||
|
"ko",
|
||||||
|
"nl",
|
||||||
|
"pl",
|
||||||
|
"pt-BR",
|
||||||
|
"ru",
|
||||||
|
"sl",
|
||||||
|
"sv",
|
||||||
|
"tr",
|
||||||
|
"uk",
|
||||||
|
"vi",
|
||||||
|
"zh-Hans",
|
||||||
|
"zh-Hant",
|
||||||
|
}
|
||||||
|
UserSettingAppearanceValue = []string{"system", "light", "dark"}
|
||||||
|
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserSetting struct {
|
||||||
|
UserID int
|
||||||
|
Key UserSettingKey `json:"key"`
|
||||||
|
// Value is a JSON string with basic value
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSettingUpsert struct {
|
||||||
|
UserID int `json:"-"`
|
||||||
|
Key UserSettingKey `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (upsert UserSettingUpsert) Validate() error {
|
||||||
|
if upsert.Key == UserSettingLocaleKey {
|
||||||
|
localeValue := "en"
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &localeValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal user setting locale value")
|
||||||
|
}
|
||||||
|
if !slices.Contains(UserSettingLocaleValue, localeValue) {
|
||||||
|
return fmt.Errorf("invalid user setting locale value")
|
||||||
|
}
|
||||||
|
} else if upsert.Key == UserSettingAppearanceKey {
|
||||||
|
appearanceValue := "system"
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal user setting appearance value")
|
||||||
|
}
|
||||||
|
if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
|
||||||
|
return fmt.Errorf("invalid user setting appearance value")
|
||||||
|
}
|
||||||
|
} else if upsert.Key == UserSettingMemoVisibilityKey {
|
||||||
|
memoVisibilityValue := Private
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
|
||||||
|
}
|
||||||
|
if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
|
||||||
|
return fmt.Errorf("invalid user setting memo visibility value")
|
||||||
|
}
|
||||||
|
} else if upsert.Key == UserSettingTelegramUserIDKey {
|
||||||
|
var s string
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid user setting telegram user id value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(s); err != nil {
|
||||||
|
return fmt.Errorf("invalid user setting telegram user id value")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("invalid user setting key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSettingFind struct {
|
||||||
|
UserID *int
|
||||||
|
|
||||||
|
Key UserSettingKey `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSettingDelete struct {
|
||||||
|
UserID int
|
||||||
|
}
|
Loading…
Reference in New Issue