chore: migrate user setting to api v1 package (#1855)

* chore: migrate to api v1 package

* chore: update
This commit is contained in:
boojack 2023-06-26 23:06:53 +08:00 committed by GitHub
parent 07e82c3f4a
commit b44f2b5ffb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 764 additions and 173 deletions

137
api/v1/activity.go Normal file
View file

@ -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"`
}

View file

@ -8,7 +8,6 @@ import (
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
@ -41,16 +40,15 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
userFind := &api.UserFind{
user, err := s.Store.GetUser(ctx, &store.FindUserMessage{
Username: &signin.Username,
}
user, err := s.Store.FindUser(ctx, userFind)
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
} else if user.RowStatus == api.Archived {
} else if user.RowStatus == store.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
}
@ -110,20 +108,19 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
}
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
user, err := s.Store.GetUser(ctx, &store.FindUserMessage{
Username: &userInfo.Identifier,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
}
if user == nil {
userCreate := &api.UserCreate{
userCreate := &store.UserMessage{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: api.NormalUser,
Role: store.NormalUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
Password: userInfo.Email,
OpenID: common.GenUUID(),
}
password, err := common.RandomString(20)
@ -135,12 +132,12 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err = s.Store.CreateUser(ctx, userCreate)
user, err = s.Store.CreateUserV1(ctx, userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
}
if user.RowStatus == api.Archived {
if user.RowStatus == store.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
}
@ -160,27 +157,27 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
}
userCreate := &api.UserCreate{
Username: signup.Username,
// The new signup user should be normal user by default.
Role: api.NormalUser,
Nickname: signup.Username,
Password: signup.Password,
OpenID: common.GenUUID(),
}
hostUserType := api.Host
existedHostUsers, err := s.Store.FindUserList(ctx, &api.UserFind{
hostUserType := store.Host
existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUserMessage{
Role: &hostUserType,
})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
}
userCreate := &store.UserMessage{
Username: signup.Username,
// The new signup user should be normal user by default.
Role: store.NormalUser,
Nickname: signup.Username,
OpenID: common.GenUUID(),
}
if len(existedHostUsers) == 0 {
// Change the default role to host if there is no host user.
userCreate.Role = api.Host
userCreate.Role = store.Host
} else {
allowSignUpSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingAllowSignUpName,
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSettingMessage{
Name: SystemSettingAllowSignUpName.String(),
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
@ -198,17 +195,13 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
}
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(ctx, userCreate)
user, err := s.Store.CreateUserV1(ctx, userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
@ -228,9 +221,9 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
})
}
func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *api.User) error {
func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *store.UserMessage) error {
ctx := c.Request().Context()
payload := api.ActivityUserAuthSignInPayload{
payload := ActivityUserAuthSignInPayload{
UserID: user.ID,
IP: echo.ExtractIPFromRealIPHeader()(c.Request()),
}
@ -238,10 +231,10 @@ func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *api.User)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
activity, err := s.Store.CreateActivityV1(ctx, &store.ActivityMessage{
CreatorID: user.ID,
Type: api.ActivityUserAuthSignIn,
Level: api.ActivityInfo,
Type: string(ActivityUserAuthSignIn),
Level: string(ActivityInfo),
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
@ -250,9 +243,9 @@ func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *api.User)
return err
}
func (s *APIV1Service) createAuthSignUpActivity(c echo.Context, user *api.User) error {
func (s *APIV1Service) createAuthSignUpActivity(c echo.Context, user *store.UserMessage) error {
ctx := c.Request().Context()
payload := api.ActivityUserAuthSignUpPayload{
payload := ActivityUserAuthSignUpPayload{
Username: user.Username,
IP: echo.ExtractIPFromRealIPHeader()(c.Request()),
}
@ -260,10 +253,10 @@ func (s *APIV1Service) createAuthSignUpActivity(c echo.Context, user *api.User)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
activity, err := s.Store.CreateActivityV1(ctx, &store.ActivityMessage{
CreatorID: user.ID,
Type: api.ActivityUserAuthSignUp,
Level: api.ActivityInfo,
Type: string(ActivityUserAuthSignUp),
Level: string(ActivityInfo),
Payload: string(payloadBytes),
})
if err != nil || activity == nil {

25
api/v1/memo.go Normal file
View file

@ -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"
}

194
api/v1/system_setting.go Normal file
View file

@ -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"`
}

134
api/v1/user_setting.go Normal file
View file

@ -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
}

View file

@ -8,7 +8,7 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/store"
)
const (
@ -64,7 +64,7 @@ func GenerateRefreshToken(userName string, userID int, secret string) (string, e
}
// GenerateTokensAndSetCookies generates jwt token and saves it to the http-only cookie.
func GenerateTokensAndSetCookies(c echo.Context, user *api.User, secret string) error {
func GenerateTokensAndSetCookies(c echo.Context, user *store.UserMessage, secret string) error {
accessToken, err := GenerateAccessToken(user.Username, user.ID, secret)
if err != nil {
return errors.Wrap(err, "failed to generate access token")

View file

@ -10,9 +10,9 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/usememos/memos/server/auth"
"github.com/usememos/memos/store"
)
const (
@ -136,7 +136,7 @@ func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.Ha
}
// Even if there is no error, we still need to make sure the user still exists.
user, err := server.Store.FindUser(ctx, &api.UserFind{
user, err := server.Store.GetUser(ctx, &store.FindUserMessage{
ID: &userID,
})
if err != nil {

View file

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
"github.com/usememos/memos/api"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/common"
"github.com/usememos/memos/store"
@ -37,9 +38,9 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
if createMemoRequest.Visibility == "" {
userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSettingMessage{
UserID: &userID,
Key: api.UserSettingMemoVisibilityKey,
Key: apiv1.UserSettingMemoVisibilityKey.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)

View file

@ -7,7 +7,9 @@ import (
"path"
"strconv"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/common"
"github.com/usememos/memos/plugin/telegram"
"github.com/usememos/memos/store"
@ -37,14 +39,13 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
}
var creatorID int
userSettingList, err := t.store.FindUserSettingList(ctx, &api.UserSettingFind{
Key: api.UserSettingTelegramUserIDKey,
userSettingMessageList, err := t.store.ListUserSettings(ctx, &store.FindUserSettingMessage{
Key: apiv1.UserSettingTelegramUserIDKey.String(),
})
if err != nil {
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Fail to find memo user: %s", err), nil)
return err
return errors.Wrap(err, "Failed to find userSettingList")
}
for _, userSetting := range userSettingList {
for _, userSetting := range userSettingMessageList {
var value string
if err := json.Unmarshal([]byte(userSetting.Value), &value); err != nil {
continue

View file

@ -9,7 +9,9 @@ import (
"github.com/pkg/errors"
"github.com/usememos/memos/api"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/common"
"github.com/usememos/memos/store"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@ -83,7 +85,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userSettingUpsert := &api.UserSettingUpsert{}
userSettingUpsert := &apiv1.UserSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
}
@ -92,10 +94,15 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
}
userSettingUpsert.UserID = userID
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
userSettingMessage, err := s.Store.UpsertUserSettingV1(ctx, &store.UserSettingMessage{
UserID: userID,
Key: userSettingUpsert.Key.String(),
Value: userSettingUpsert.Value,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
}
userSetting := convertUserSettingFromStore(userSettingMessage)
return c.JSON(http.StatusOK, composeResponse(userSetting))
})
@ -115,12 +122,21 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
userSettingMessageList, err := s.Store.ListUserSettings(ctx, &store.FindUserSettingMessage{
UserID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
userSettingList := []*api.UserSetting{}
for _, userSettingMessage := range userSettingMessageList {
userSettingV1 := convertUserSettingFromStore(userSettingMessage)
userSettingList = append(userSettingList, &api.UserSetting{
UserID: userSettingV1.UserID,
Key: api.UserSettingKey(userSettingV1.Key),
Value: userSettingV1.Value,
})
}
user.UserSettingList = userSettingList
return c.JSON(http.StatusOK, composeResponse(user))
})
@ -202,12 +218,21 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
userSettingMessageList, err := s.Store.ListUserSettings(ctx, &store.FindUserSettingMessage{
UserID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
userSettingList := []*api.UserSetting{}
for _, userSettingMessage := range userSettingMessageList {
userSettingV1 := convertUserSettingFromStore(userSettingMessage)
userSettingList = append(userSettingList, &api.UserSetting{
UserID: userSettingV1.UserID,
Key: api.UserSettingKey(userSettingV1.Key),
Value: userSettingV1.Value,
})
}
user.UserSettingList = userSettingList
return c.JSON(http.StatusOK, composeResponse(user))
})
@ -271,3 +296,11 @@ func (s *Server) createUserCreateActivity(c echo.Context, user *api.User) error
}
return err
}
func convertUserSettingFromStore(userSetting *store.UserSettingMessage) *apiv1.UserSetting {
return &apiv1.UserSetting{
UserID: userSetting.UserID,
Key: apiv1.UserSettingKey(userSetting.Key),
Value: userSetting.Value,
}
}

View file

@ -7,6 +7,51 @@ import (
"github.com/usememos/memos/api"
)
type ActivityMessage struct {
ID int
// Standard fields
CreatorID int
CreatedTs int64
// Domain specific fields
Type string
Level string
Payload string
}
// CreateActivityV1 creates an instance of Activity.
func (s *Store) CreateActivityV1(ctx context.Context, create *ActivityMessage) (*ActivityMessage, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
query := `
INSERT INTO activity (
creator_id,
type,
level,
payload
)
VALUES (?, ?, ?, ?)
RETURNING id, created_ts
`
if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.Type, create.Level, create.Payload).Scan(
&create.ID,
&create.CreatedTs,
); err != nil {
return nil, FormatError(err)
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
activityMessage := create
return activityMessage, nil
}
// activityRaw is the store model for an Activity.
// Fields have exactly the same meanings as Activity.
type activityRaw struct {
@ -38,10 +83,6 @@ func (raw *activityRaw) toActivity() *api.Activity {
// CreateActivity creates an instance of Activity.
func (s *Store) CreateActivity(ctx context.Context, create *api.ActivityCreate) (*api.Activity, error) {
if s.Profile.Mode == "prod" {
return nil, nil
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)

View file

@ -2,14 +2,8 @@ package store
import (
"fmt"
"github.com/usememos/memos/api"
)
func getUserSettingCacheKey(userSetting userSettingRaw) string {
return fmt.Sprintf("%d-%s", userSetting.UserID, userSetting.Key.String())
}
func getUserSettingFindCacheKey(userSettingFind *api.UserSettingFind) string {
return fmt.Sprintf("%d-%s", userSettingFind.UserID, userSettingFind.Key.String())
func getUserSettingCacheKeyV1(userID int, key string) string {
return fmt.Sprintf("%d-%s", userID, key)
}

View file

@ -291,7 +291,7 @@ func listIdentityProviders(ctx context.Context, tx *sql.Tx, find *FindIdentityPr
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
return nil, err
}
return identityProviderMessages, nil

View file

@ -14,9 +14,9 @@ type Store struct {
db *sql.DB
systemSettingCache sync.Map // map[string]*systemSettingRaw
userCache sync.Map // map[int]*userRaw
userSettingCache sync.Map // map[string]*userSettingRaw
userSettingCache sync.Map // map[string]*UserSettingMessage
shortcutCache sync.Map // map[int]*shortcutRaw
idpCache sync.Map // map[int]*identityProviderMessage
idpCache sync.Map // map[int]*IdentityProviderMessage
resourceCache sync.Map // map[int]*resourceRaw
}

View file

@ -10,6 +10,101 @@ import (
"github.com/usememos/memos/common"
)
type SystemSettingMessage struct {
Name string
Value string
Description string
}
type FindSystemSettingMessage struct {
Name string
}
func (s *Store) ListSystemSettings(ctx context.Context, find *FindSystemSettingMessage) ([]*SystemSettingMessage, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := listSystemSettings(ctx, tx, find)
if err != nil {
return nil, err
}
for _, systemSettingMessage := range list {
s.systemSettingCache.Store(systemSettingMessage.Name, systemSettingMessage)
}
return list, nil
}
func (s *Store) GetSystemSetting(ctx context.Context, find *FindSystemSettingMessage) (*SystemSettingMessage, error) {
if find.Name != "" {
if cache, ok := s.systemSettingCache.Load(find.Name); ok {
return cache.(*SystemSettingMessage), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := listSystemSettings(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
systemSettingMessage := list[0]
s.systemSettingCache.Store(systemSettingMessage.Name, systemSettingMessage)
return systemSettingMessage, nil
}
func listSystemSettings(ctx context.Context, tx *sql.Tx, find *FindSystemSettingMessage) ([]*SystemSettingMessage, error) {
where, args := []string{"1 = 1"}, []any{}
if find.Name != "" {
where, args = append(where, "name = ?"), append(args, find.Name)
}
query := `
SELECT
name,
value,
description
FROM system_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
list := []*SystemSettingMessage{}
for rows.Next() {
systemSettingMessage := &SystemSettingMessage{}
if err := rows.Scan(
&systemSettingMessage.Name,
&systemSettingMessage.Value,
&systemSettingMessage.Description,
); err != nil {
return nil, FormatError(err)
}
list = append(list, systemSettingMessage)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
type systemSettingRaw struct {
Name api.SystemSettingName
Value string

View file

@ -10,23 +10,6 @@ import (
"github.com/usememos/memos/common"
)
func (s *Store) SeedDataForNewUser(ctx context.Context, user *api.User) error {
// Create a memo for the user.
_, err := s.CreateMemo(ctx, &MemoMessage{
CreatorID: user.ID,
Content: "#inbox Welcome to Memos!",
Visibility: Private,
})
if err != nil {
return err
}
_, err = s.UpsertTag(ctx, &api.TagUpsert{
CreatorID: user.ID,
Name: "inbox",
})
return err
}
// Role is the type of a role.
type Role string
@ -289,10 +272,6 @@ func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.Us
s.userCache.Store(userRaw.ID, userRaw)
user := userRaw.toUser()
if err := s.SeedDataForNewUser(ctx, user); err != nil {
return nil, err
}
return user, nil
}

View file

@ -4,33 +4,35 @@ import (
"context"
"database/sql"
"strings"
"github.com/usememos/memos/api"
)
type userSettingRaw struct {
type UserSettingMessage struct {
UserID int
Key api.UserSettingKey
Key string
Value string
}
func (raw *userSettingRaw) toUserSetting() *api.UserSetting {
return &api.UserSetting{
UserID: raw.UserID,
Key: raw.Key,
Value: raw.Value,
}
type FindUserSettingMessage struct {
UserID *int
Key string
}
func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) {
func (s *Store) UpsertUserSettingV1(ctx context.Context, upsert *UserSettingMessage) (*UserSettingMessage, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userSettingRaw, err := upsertUserSetting(ctx, tx, upsert)
if err != nil {
query := `
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET value = EXCLUDED.value
`
if _, err := tx.ExecContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value); err != nil {
return nil, err
}
@ -38,39 +40,34 @@ func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUp
return nil, err
}
s.userSettingCache.Store(getUserSettingCacheKey(*userSettingRaw), userSettingRaw)
userSetting := userSettingRaw.toUserSetting()
return userSetting, nil
userSettingMessage := upsert
s.userSettingCache.Store(getUserSettingCacheKeyV1(userSettingMessage.UserID, userSettingMessage.Key), userSettingMessage)
return userSettingMessage, nil
}
func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) {
func (s *Store) ListUserSettings(ctx context.Context, find *FindUserSettingMessage) ([]*UserSettingMessage, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userSettingRawList, err := findUserSettingList(ctx, tx, find)
userSettingList, err := listUserSettings(ctx, tx, find)
if err != nil {
return nil, err
}
list := []*api.UserSetting{}
for _, raw := range userSettingRawList {
s.userSettingCache.Store(getUserSettingCacheKey(*raw), raw)
list = append(list, raw.toUserSetting())
for _, userSetting := range userSettingList {
s.userSettingCache.Store(getUserSettingCacheKeyV1(userSetting.UserID, userSetting.Key), userSetting)
}
return list, nil
return userSettingList, nil
}
func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) (*api.UserSetting, error) {
if userSetting, ok := s.userSettingCache.Load(getUserSettingFindCacheKey(find)); ok {
if userSetting == nil {
return nil, nil
func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSettingMessage) (*UserSettingMessage, error) {
if find.UserID != nil {
if cache, ok := s.userSettingCache.Load(getUserSettingCacheKeyV1(*find.UserID, find.Key)); ok {
return cache.(*UserSettingMessage), nil
}
return userSetting.(*userSettingRaw).toUserSetting(), nil
}
tx, err := s.db.BeginTx(ctx, nil)
@ -79,51 +76,25 @@ func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind)
}
defer tx.Rollback()
list, err := findUserSettingList(ctx, tx, find)
list, err := listUserSettings(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
s.userSettingCache.Store(getUserSettingFindCacheKey(find), nil)
return nil, nil
}
userSettingRaw := list[0]
s.userSettingCache.Store(getUserSettingCacheKey(*userSettingRaw), userSettingRaw)
return userSettingRaw.toUserSetting(), nil
userSettingMessage := list[0]
s.userSettingCache.Store(getUserSettingCacheKeyV1(userSettingMessage.UserID, userSettingMessage.Key), userSettingMessage)
return userSettingMessage, nil
}
func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) {
query := `
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET
value = EXCLUDED.value
RETURNING user_id, key, value
`
var userSettingRaw userSettingRaw
if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}
return &userSettingRaw, nil
}
func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) {
func listUserSettings(ctx context.Context, tx *sql.Tx, find *FindUserSettingMessage) ([]*UserSettingMessage, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.Key.String(); v != "" {
if v := find.Key; v != "" {
where, args = append(where, "key = ?"), append(args, v)
}
if v := find.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *find.UserID)
}
@ -141,25 +112,24 @@ func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingF
}
defer rows.Close()
userSettingRawList := make([]*userSettingRaw, 0)
userSettingMessageList := make([]*UserSettingMessage, 0)
for rows.Next() {
var userSettingRaw userSettingRaw
var userSettingMessage UserSettingMessage
if err := rows.Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
&userSettingMessage.UserID,
&userSettingMessage.Key,
&userSettingMessage.Value,
); err != nil {
return nil, FormatError(err)
}
userSettingRawList = append(userSettingRawList, &userSettingRaw)
userSettingMessageList = append(userSettingMessageList, &userSettingMessage)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return userSettingRawList, nil
return userSettingMessageList, nil
}
func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error {

View file

@ -26,9 +26,6 @@ func TestMemoRelationServer(t *testing.T) {
user, err := s.postAuthSignup(signup)
require.NoError(t, err)
require.Equal(t, signup.Username, user.Username)
memoList, err := s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 1)
memo, err := s.postMemoCreate(&api.CreateMemoRequest{
Content: "test memo",
})
@ -45,9 +42,9 @@ func TestMemoRelationServer(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, "test memo2", memo2.Content)
memoList, err = s.getMemoList()
memoList, err := s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 3)
require.Len(t, memoList, 2)
require.Len(t, memo2.RelationList, 1)
err = s.deleteMemoRelation(memo2.ID, memo.ID, api.MemoRelationReference)
require.NoError(t, err)

View file

@ -26,17 +26,14 @@ func TestMemoServer(t *testing.T) {
user, err := s.postAuthSignup(signup)
require.NoError(t, err)
require.Equal(t, signup.Username, user.Username)
memoList, err := s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 1)
memo, err := s.postMemoCreate(&api.CreateMemoRequest{
Content: "test memo",
})
require.NoError(t, err)
require.Equal(t, "test memo", memo.Content)
memoList, err = s.getMemoList()
memoList, err := s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 2)
require.Len(t, memoList, 1)
updatedContent := "updated memo"
memo, err = s.patchMemo(&api.PatchMemoRequest{
ID: memo.ID,
@ -62,7 +59,7 @@ func TestMemoServer(t *testing.T) {
require.NoError(t, err)
memoList, err = s.getMemoList()
require.NoError(t, err)
require.Len(t, memoList, 1)
require.Len(t, memoList, 0)
}
func (s *TestingServer) getMemo(memoID int) (*api.MemoResponse, error) {

View file

@ -36,8 +36,8 @@ func TestMemoStore(t *testing.T) {
CreatorID: &user.ID,
})
require.NoError(t, err)
require.Equal(t, 2, len(memoList))
require.Equal(t, memo, memoList[1])
require.Equal(t, 1, len(memoList))
require.Equal(t, memo, memoList[0])
err = ts.DeleteMemo(ctx, &store.DeleteMemoMessage{
ID: memo.ID,
})