CVE-2024-29030: Insufficient Information (NVD-CWE-noinfo)
value: memos is a privacy-first, lightweight note-taking service. In memos 0.13.2, an SSRF vulnerability exists at the /api/resource that allows authenticated users to enumerate the internal network.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-19T15:15Z
Repository: https://github.com/usememos/memos
Language: Go
Files changed: internal/jobs/presign_link.go, server/integration/telegram.go, server/route/api/v2/storage_service.go, server/server.go
Commit date: 2024-04-13 11:01:16 +0800
Commit message: chore: retire legacy api

Diff: diff --git a/internal/jobs/presign_link.go b/internal/jobs/presign_link.go
index 187610069..a0853254b 100644
--- a/internal/jobs/presign_link.go
+++ b/internal/jobs/presign_link.go
@@ -1,18 +1,19 @@
 package jobs
 
 import (
 	"context"
-	"encoding/json"
 	"log/slog"
 	"strings"
 	"time"
 
 	"github.com/pkg/errors"
 
 	"github.com/usememos/memos/plugin/storage/s3"
-	apiv1 "github.com/usememos/memos/server/route/api/v1"
+	apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
+	storepb "github.com/usememos/memos/proto/gen/store"
+	apiv2 "github.com/usememos/memos/server/route/api/v2"
 	"github.com/usememos/memos/store"
 )
 
 // RunPreSignLinks is a background job that pre-signs external links stored in the database.
 // It uses S3 client to generate presigned URLs and updates the corresponding resources in the store.
@@ -93,46 +94,37 @@ func signExternalLinks(ctx context.Context, dataStore *store.Store) error {
 
 // findObjectStorage returns current default storage if it's S3-compatible or nil otherwise.
 // Returns error only in case of internal problems (ie: database or configuration issues).
 // May return nil client and nil error.
 func findObjectStorage(ctx context.Context, dataStore *store.Store) (*s3.Client, error) {
-	systemSettingStorageServiceID, err := dataStore.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: apiv1.SystemSettingStorageServiceIDName.String()})
+	workspaceStorageSetting, err := dataStore.GetWorkspaceStorageSetting(ctx)
 	if err != nil {
-		return nil, errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")
+		return nil, errors.Wrap(err, "Failed to find workspaceStorageSetting")
 	}
-
-	storageServiceID := apiv1.DefaultStorage
-	if systemSettingStorageServiceID != nil {
-		err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
-		if err != nil {
-			return nil, errors.Wrap(err, "Failed to unmarshal storage service id")
-		}
+	if workspaceStorageSetting.StorageType != storepb.WorkspaceStorageSetting_STORAGE_TYPE_EXTERNAL || workspaceStorageSetting.ActivedExternalStorageId == nil {
+		return nil, nil
 	}
-	storage, err := dataStore.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
+	storage, err := dataStore.GetStorageV1(ctx, &store.FindStorage{ID: workspaceStorageSetting.ActivedExternalStorageId})
 	if err != nil {
-		return nil, errors.Wrap(err, "Failed to find StorageServiceID")
+		return nil, errors.Wrap(err, "Failed to find storage")
 	}
-
 	if storage == nil {
-		return nil, nil // storage not configured - not an error, just return empty ref
+		return nil, nil
 	}
-	storageMessage, err := apiv1.ConvertStorageFromStore(storage)
 
-	if err != nil {
-		return nil, errors.Wrap(err, "Failed to ConvertStorageFromStore")
-	}
-	if storageMessage.Type != apiv1.StorageS3 {
+	storageMessage := apiv2.ConvertStorageFromStore(storage)
+	if storageMessage.Type != apiv2pb.Storage_S3 {
 		return nil, nil
 	}
 
-	s3Config := storageMessage.Config.S3Config
+	s3Config := storageMessage.Config.GetS3Config()
 	return s3.NewClient(ctx, &s3.Config{
 		AccessKey: s3Config.AccessKey,
 		SecretKey: s3Config.SecretKey,
 		EndPoint:  s3Config.EndPoint,
 		Region:    s3Config.Region,
 		Bucket:    s3Config.Bucket,
-		URLPrefix: s3Config.URLPrefix,
-		URLSuffix: s3Config.URLSuffix,
+		URLPrefix: s3Config.UrlPrefix,
+		URLSuffix: s3Config.UrlSuffix,
 		PreSign:   s3Config.PreSign,
 	})
 }
diff --git a/server/integration/telegram.go b/server/integration/telegram.go
index 7c5a05cb7..830eb0b13 100644
--- a/server/integration/telegram.go
+++ b/server/integration/telegram.go
@@ -1,9 +1,8 @@
 package integration
 
 import (
-	"bytes"
 	"context"
 	"fmt"
 	"path/filepath"
 	"slices"
 	"strconv"
@@ -17,11 +16,10 @@ import (
 	"github.com/yourselfhosted/gomark/parser/tokenizer"
 
 	"github.com/usememos/memos/plugin/telegram"
 	"github.com/usememos/memos/plugin/webhook"
 	storepb "github.com/usememos/memos/proto/gen/store"
-	apiv1 "github.com/usememos/memos/server/route/api/v1"
 	apiv2 "github.com/usememos/memos/server/route/api/v2"
 	"github.com/usememos/memos/store"
 )
 
 type TelegramHandler struct {
@@ -124,13 +122,14 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
 			CreatorID: creatorID,
 			Filename:  filepath.Base(attachment.FileName),
 			Type:      attachment.GetMimeType(),
 			Size:      attachment.FileSize,
 			MemoID:    &memoMessage.ID,
+			Blob:      attachment.Data,
 		}
 
-		err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data))
+		err := apiv2.SaveResourceBlob(ctx, t.store, &create)
 		if err != nil {
 			_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to SaveResourceBlob: %s", err), nil)
 			return err
 		}
 
diff --git a/server/route/api/v1/auth.go b/server/route/api/v1/auth.go
deleted file mode 100644
index c6dcf6d41..000000000
--- a/server/route/api/v1/auth.go
+++ /dev/null
@@ -1,253 +0,0 @@
-package v1
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/labstack/echo/v4"
-	"github.com/pkg/errors"
-	"golang.org/x/crypto/bcrypt"
-
-	"github.com/usememos/memos/internal/util"
-	storepb "github.com/usememos/memos/proto/gen/store"
-	"github.com/usememos/memos/server/route/api/auth"
-	"github.com/usememos/memos/store"
-)
-
-type SignIn struct {
-	Username string `json:"username"`
-	Password string `json:"password"`
-	Remember bool   `json:"remember"`
-}
-
-type SSOSignIn struct {
-	IdentityProviderID int32  `json:"identityProviderId"`
-	Code               string `json:"code"`
-	RedirectURI        string `json:"redirectUri"`
-}
-
-type SignUp struct {
-	Username string `json:"username"`
-	Password string `json:"password"`
-}
-
-func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
-	g.POST("/auth/signin", s.SignIn)
-	g.POST("/auth/signout", s.SignOut)
-	g.POST("/auth/signup", s.SignUp)
-}
-
-// SignIn godoc
-//
-//	@Summary	Sign-in to memos.
-//	@Tags		auth
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		SignIn		true	"Sign-in object"
-//	@Success	200		{object}	store.User	"User information"
-//	@Failure	400		{object}	nil			"Malformatted signin request"
-//	@Failure	401		{object}	nil			"Password login is deactivated | Incorrect login credentials, please try again"
-//	@Failure	403		{object}	nil			"User has been archived with username %s"
-//	@Failure	500		{object}	nil			"Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
-//	@Router		/api/v1/auth/signin [POST]
-func (s *APIV1Service) SignIn(c echo.Context) error {
-	ctx := c.Request().Context()
-	workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
-	}
-	if workspaceGeneralSetting.DisallowPasswordLogin {
-		return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
-	}
-
-	signin := &SignIn{}
-	if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		Username: &signin.Username,
-	})
-	if err != nil {
-		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 == store.Archived {
-		return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
-	}
-
-	// Compare the stored hashed password, with the hashed version of the password that was received.
-	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
-		// If the two passwords don't match, return a 401 status.
-		return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
-	}
-
-	var expireAt time.Time
-	// Set cookie expiration to 100 years to make it persistent.
-	cookieExp := time.Now().AddDate(100, 0, 0)
-	if !signin.Remember {
-		expireAt = time.Now().Add(auth.AccessTokenDuration)
-		cookieExp = time.Now().Add(auth.CookieExpDuration)
-	}
-
-	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
-	}
-	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
-	}
-	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
-	userMessage := convertUserFromStore(user)
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-// SignOut godoc
-//
-//	@Summary	Sign-out from memos.
-//	@Tags		auth
-//	@Produce	json
-//	@Success	200	{boolean}	true	"Sign-out success"
-//	@Router		/api/v1/auth/signout [POST]
-func (s *APIV1Service) SignOut(c echo.Context) error {
-	accessToken := findAccessToken(c)
-	userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)
-
-	err := removeAccessTokenAndCookies(c, s.Store, userID, accessToken)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to remove access token, err: %s", err)).SetInternal(err)
-	}
-
-	return c.JSON(http.StatusOK, true)
-}
-
-// SignUp godoc
-//
-//	@Summary	Sign-up to memos.
-//	@Tags		auth
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		SignUp		true	"Sign-up object"
-//	@Success	200		{object}	store.User	"User information"
-//	@Failure	400		{object}	nil			"Malformatted signup request | Failed to find users"
-//	@Failure	401		{object}	nil			"signup is disabled"
-//	@Failure	403		{object}	nil			"Forbidden"
-//	@Failure	404		{object}	nil			"Not found"
-//	@Failure	500		{object}	nil			"Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
-//	@Router		/api/v1/auth/signup [POST]
-func (s *APIV1Service) SignUp(c echo.Context) error {
-	ctx := c.Request().Context()
-	signup := &SignUp{}
-	if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
-	}
-
-	hostUserType := store.RoleHost
-	existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
-		Role: &hostUserType,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
-	}
-	if !util.UIDMatcher.MatchString(strings.ToLower(signup.Username)) {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
-	}
-
-	userCreate := &store.User{
-		Username: signup.Username,
-		// The new signup user should be normal user by default.
-		Role:     store.RoleUser,
-		Nickname: signup.Username,
-	}
-	if len(existedHostUsers) == 0 {
-		// Change the default role to host if there is no host user.
-		userCreate.Role = store.RoleHost
-	} else {
-		workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
-		}
-		if workspaceGeneralSetting.DisallowSignup {
-			return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
-		}
-		if workspaceGeneralSetting.DisallowPasswordLogin {
-			return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").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)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
-	}
-	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
-	}
-	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
-	}
-	cookieExp := time.Now().Add(auth.CookieExpDuration)
-	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
-	userMessage := convertUserFromStore(user)
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
-	userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
-	if err != nil {
-		return errors.Wrap(err, "failed to get user access tokens")
-	}
-	userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
-		AccessToken: accessToken,
-		Description: "Account sign in",
-	}
-	userAccessTokens = append(userAccessTokens, &userAccessToken)
-	if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
-		UserId: user.ID,
-		Key:    storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
-		Value: &storepb.UserSetting_AccessTokens{
-			AccessTokens: &storepb.AccessTokensUserSetting{
-				AccessTokens: userAccessTokens,
-			},
-		},
-	}); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
-	}
-	return nil
-}
-
-// removeAccessTokenAndCookies removes the jwt token from the cookies.
-func removeAccessTokenAndCookies(c echo.Context, s *store.Store, userID int32, token string) error {
-	err := s.RemoveUserAccessToken(c.Request().Context(), userID, token)
-	if err != nil {
-		return err
-	}
-
-	cookieExp := time.Now().Add(-1 * time.Hour)
-	setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
-	return nil
-}
-
-// setTokenCookie sets the token to the cookie.
-func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
-	cookie := new(http.Cookie)
-	cookie.Name = name
-	cookie.Value = token
-	cookie.Expires = expiration
-	cookie.Path = "/"
-	// Http-only helps mitigate the risk of client side script accessing the protected cookie.
-	cookie.HttpOnly = true
-	cookie.SameSite = http.SameSiteStrictMode
-	c.SetCookie(cookie)
-}
diff --git a/server/route/api/v1/common.go b/server/route/api/v1/common.go
deleted file mode 100644
index a0b49bbcf..000000000
--- a/server/route/api/v1/common.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package v1
-
-// RowStatus is the status for a row.
-type RowStatus string
-
-const (
-	// Normal is the status for a normal row.
-	Normal RowStatus = "NORMAL"
-	// Archived is the status for an archived row.
-	Archived RowStatus = "ARCHIVED"
-)
-
-func (r RowStatus) String() string {
-	return string(r)
-}
diff --git a/server/route/api/v1/docs.go b/server/route/api/v1/docs.go
deleted file mode 100644
index 6054a037a..000000000
--- a/server/route/api/v1/docs.go
+++ /dev/null
@@ -1,3393 +0,0 @@
-// Code generated by swaggo/swag. DO NOT EDIT.
-
-package v1
-
-import "github.com/swaggo/swag"
-
-const docTemplate = `{
-    "schemes": {{ marshal .Schemes }},
-    "swagger": "2.0",
-    "info": {
-        "description": "{{escape .Description}}",
-        "title": "{{.Title}}",
-        "contact": {
-            "name": "API Support",
-            "url": "https://github.com/orgs/usememos/discussions"
-        },
-        "license": {
-            "name": "MIT License",
-            "url": "https://github.com/usememos/memos/blob/main/LICENSE"
-        },
-        "version": "{{.Version}}"
-    },
-    "host": "{{.Host}}",
-    "basePath": "{{.BasePath}}",
-    "paths": {
-        "/api/v1/auth/signin": {
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "auth"
-                ],
-                "summary": "Sign-in to memos.",
-                "parameters": [
-                    {
-                        "description": "Sign-in object",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.SignIn"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "User information",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted signin request"
-                    },
-                    "401": {
-                        "description": "Password login is deactivated | Incorrect login credentials, please try again"
-                    },
-                    "403": {
-                        "description": "User has been archived with username %s"
-                    },
-                    "500": {
-                        "description": "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/auth/signin/sso": {
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "auth"
-                ],
-                "summary": "Sign-in to memos using SSO.",
-                "parameters": [
-                    {
-                        "description": "SSO sign-in object",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.SSOSignIn"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "User information",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted signin request"
-                    },
-                    "401": {
-                        "description": "Access denied, identifier does not match the filter."
-                    },
-                    "403": {
-                        "description": "User has been archived with username {username}"
-                    },
-                    "404": {
-                        "description": "Identity provider not found"
-                    },
-                    "500": {
-                        "description": "Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/auth/signout": {
-            "post": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "auth"
-                ],
-                "summary": "Sign-out from memos.",
-                "responses": {
-                    "200": {
-                        "description": "Sign-out success",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    }
-                }
-            }
-        },
-        "/api/v1/auth/signup": {
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "auth"
-                ],
-                "summary": "Sign-up to memos.",
-                "parameters": [
-                    {
-                        "description": "Sign-up object",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.SignUp"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "User information",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted signup request | Failed to find users"
-                    },
-                    "401": {
-                        "description": "signup is disabled"
-                    },
-                    "403": {
-                        "description": "Forbidden"
-                    },
-                    "404": {
-                        "description": "Not found"
-                    },
-                    "500": {
-                        "description": "Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/idp": {
-            "get": {
-                "description": "*clientSecret is only available for host user",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "idp"
-                ],
-                "summary": "Get a list of identity providers",
-                "responses": {
-                    "200": {
-                        "description": "List of available identity providers",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/api_v1.IdentityProvider"
-                            }
-                        }
-                    },
-                    "500": {
-                        "description": "Failed to find identity provider list | Failed to find user"
-                    }
-                }
-            },
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "idp"
-                ],
-                "summary": "Create Identity Provider",
-                "parameters": [
-                    {
-                        "description": "Identity provider information",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.CreateIdentityProviderRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Identity provider information",
-                        "schema": {
-                            "$ref": "#/definitions/store.IdentityProvider"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post identity provider request"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to create identity provider"
-                    }
-                }
-            }
-        },
-        "/api/v1/idp/{idpId}": {
-            "get": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "idp"
-                ],
-                "summary": "Get an identity provider by ID",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Identity provider ID",
-                        "name": "idpId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Requested identity provider",
-                        "schema": {
-                            "$ref": "#/definitions/store.IdentityProvider"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "404": {
-                        "description": "Identity provider not found"
-                    },
-                    "500": {
-                        "description": "Failed to find identity provider list | Failed to find user"
-                    }
-                }
-            },
-            "delete": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "idp"
-                ],
-                "summary": "Delete an identity provider by ID",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Identity Provider ID",
-                        "name": "idpId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Identity Provider deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted patch identity provider request"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to patch identity provider"
-                    }
-                }
-            },
-            "patch": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "idp"
-                ],
-                "summary": "Update an identity provider by ID",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Identity Provider ID",
-                        "name": "idpId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Patched identity provider information",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.UpdateIdentityProviderRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Patched identity provider",
-                        "schema": {
-                            "$ref": "#/definitions/store.IdentityProvider"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted patch identity provider request"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to patch identity provider"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Get a list of memos matching optional filters",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Creator ID",
-                        "name": "creatorId",
-                        "in": "query"
-                    },
-                    {
-                        "type": "string",
-                        "description": "Creator username",
-                        "name": "creatorUsername",
-                        "in": "query"
-                    },
-                    {
-                        "enum": [
-                            "NORMAL",
-                            "ARCHIVED"
-                        ],
-                        "type": "string",
-                        "description": "Row status",
-                        "name": "rowStatus",
-                        "in": "query"
-                    },
-                    {
-                        "type": "boolean",
-                        "description": "Pinned",
-                        "name": "pinned",
-                        "in": "query"
-                    },
-                    {
-                        "type": "string",
-                        "description": "Search for tag. Do not append #",
-                        "name": "tag",
-                        "in": "query"
-                    },
-                    {
-                        "type": "string",
-                        "description": "Search for content",
-                        "name": "content",
-                        "in": "query"
-                    },
-                    {
-                        "type": "integer",
-                        "description": "Limit",
-                        "name": "limit",
-                        "in": "query"
-                    },
-                    {
-                        "type": "integer",
-                        "description": "Offset",
-                        "name": "offset",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.Memo"
-                            }
-                        }
-                    },
-                    "400": {
-                        "description": "Missing user to find memo"
-                    },
-                    "500": {
-                        "description": "Failed to get memo display with updated ts setting value | Failed to fetch memo list | Failed to compose memo response"
-                    }
-                }
-            },
-            "post": {
-                "description": "Visibility can be PUBLIC, PROTECTED or PRIVATE\n*You should omit fields to use their default values",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Create a memo",
-                "parameters": [
-                    {
-                        "description": "Request object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateMemoRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Stored memo",
-                        "schema": {
-                            "$ref": "#/definitions/store.Memo"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post memo request | Content size overflow, up to 1MB"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "404": {
-                        "description": "User not found | Memo not found: %d"
-                    },
-                    "500": {
-                        "description": "Failed to find user setting | Failed to unmarshal user setting value | Failed to find system setting | Failed to unmarshal system setting | Failed to find user | Failed to create memo | Failed to create activity | Failed to upsert memo resource | Failed to upsert memo relation | Failed to compose memo | Failed to compose memo response"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo/all": {
-            "get": {
-                "description": "This should also list protected memos if the user is logged in\nAuthentication is optional",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Get a list of public memos matching optional filters",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Limit",
-                        "name": "limit",
-                        "in": "query"
-                    },
-                    {
-                        "type": "integer",
-                        "description": "Offset",
-                        "name": "offset",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.Memo"
-                            }
-                        }
-                    },
-                    "500": {
-                        "description": "Failed to get memo display with updated ts setting value | Failed to fetch all memo list | Failed to compose memo response"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo/stats": {
-            "get": {
-                "description": "Used to generate the heatmap",
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Get memo stats by creator ID or username",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Creator ID",
-                        "name": "creatorId",
-                        "in": "query"
-                    },
-                    {
-                        "type": "string",
-                        "description": "Creator username",
-                        "name": "creatorUsername",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo createdTs list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "type": "integer"
-                            }
-                        }
-                    },
-                    "400": {
-                        "description": "Missing user id to find memo"
-                    },
-                    "500": {
-                        "description": "Failed to get memo display with updated ts setting value | Failed to find memo list | Failed to compose memo response"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo/{memoId}": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Get memo by ID",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Memo ID",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.Memo"
-                            }
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "403": {
-                        "description": "this memo is private only | this memo is protected, missing user in session"
-                    },
-                    "404": {
-                        "description": "Memo not found: %d"
-                    },
-                    "500": {
-                        "description": "Failed to find memo by ID: %v | Failed to compose memo response"
-                    }
-                }
-            },
-            "delete": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Delete memo by ID",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Memo ID to delete",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "404": {
-                        "description": "Memo not found: %d"
-                    },
-                    "500": {
-                        "description": "Failed to find memo | Failed to delete memo ID: %v"
-                    }
-                }
-            },
-            "patch": {
-                "description": "Visibility can be PUBLIC, PROTECTED or PRIVATE\n*You should omit fields to use their default values",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo"
-                ],
-                "summary": "Update a memo",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "ID of memo to update",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Patched object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.PatchMemoRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Stored memo",
-                        "schema": {
-                            "$ref": "#/definitions/store.Memo"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted patch memo request | Content size overflow, up to 1MB"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "404": {
-                        "description": "Memo not found: %d"
-                    },
-                    "500": {
-                        "description": "Failed to find memo | Failed to patch memo | Failed to upsert memo resource | Failed to delete memo resource | Failed to compose memo response"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo/{memoId}/organizer": {
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo-organizer"
-                ],
-                "summary": "Organize memo (pin/unpin)",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "ID of memo to organize",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Memo organizer object",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo information",
-                        "schema": {
-                            "$ref": "#/definitions/store.Memo"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted post memo organizer request"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "404": {
-                        "description": "Memo not found: %v"
-                    },
-                    "500": {
-                        "description": "Failed to find memo | Failed to upsert memo organizer | Failed to find memo by ID: %v | Failed to compose memo response"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo/{memoId}/relation": {
-            "get": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo-relation"
-                ],
-                "summary": "Get a list of Memo Relations",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "ID of memo to find relations",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo relation information list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.MemoRelation"
-                            }
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s"
-                    },
-                    "500": {
-                        "description": "Failed to list memo relations"
-                    }
-                }
-            },
-            "post": {
-                "description": "Create a relation between two memos",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo-relation"
-                ],
-                "summary": "Create Memo Relation",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "ID of memo to relate",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Memo relation object",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo relation information",
-                        "schema": {
-                            "$ref": "#/definitions/store.MemoRelation"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted post memo relation request"
-                    },
-                    "500": {
-                        "description": "Failed to upsert memo relation"
-                    }
-                }
-            }
-        },
-        "/api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType}": {
-            "delete": {
-                "description": "Removes a relation between two memos",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "memo-relation"
-                ],
-                "summary": "Delete a Memo Relation",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "ID of memo to find relations",
-                        "name": "memoId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "integer",
-                        "description": "ID of memo to remove relation to",
-                        "name": "relatedMemoId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "enum": [
-                            "REFERENCE",
-                            "COMMENT"
-                        ],
-                        "type": "string",
-                        "description": "Type of relation to remove",
-                        "name": "relationType",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Memo relation deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "Memo ID is not a number: %s | Related memo ID is not a number: %s"
-                    },
-                    "500": {
-                        "description": "Failed to delete memo relation"
-                    }
-                }
-            }
-        },
-        "/api/v1/ping": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "system"
-                ],
-                "summary": "Ping the system",
-                "responses": {
-                    "200": {
-                        "description": "If succeed to ping the system",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    }
-                }
-            }
-        },
-        "/api/v1/resource": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "resource"
-                ],
-                "summary": "Get a list of resources",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Limit",
-                        "name": "limit",
-                        "in": "query"
-                    },
-                    {
-                        "type": "integer",
-                        "description": "Offset",
-                        "name": "offset",
-                        "in": "query"
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Resource list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.Resource"
-                            }
-                        }
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "500": {
-                        "description": "Failed to fetch resource list"
-                    }
-                }
-            },
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "resource"
-                ],
-                "summary": "Create resource",
-                "parameters": [
-                    {
-                        "description": "Request object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.CreateResourceRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Created resource",
-                        "schema": {
-                            "$ref": "#/definitions/store.Resource"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post resource request | Invalid external link | Invalid external link scheme | Failed to request %s | Failed to read %s | Failed to read mime from %s"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "500": {
-                        "description": "Failed to save resource | Failed to create resource | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/resource/blob": {
-            "post": {
-                "consumes": [
-                    "multipart/form-data"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "resource"
-                ],
-                "summary": "Upload resource",
-                "parameters": [
-                    {
-                        "type": "file",
-                        "description": "File to upload",
-                        "name": "file",
-                        "in": "formData",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Created resource",
-                        "schema": {
-                            "$ref": "#/definitions/store.Resource"
-                        }
-                    },
-                    "400": {
-                        "description": "Upload file not found | File size exceeds allowed limit of %d MiB | Failed to parse upload data"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "500": {
-                        "description": "Failed to get uploading file | Failed to open file | Failed to save resource | Failed to create resource | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/resource/{resourceId}": {
-            "delete": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "resource"
-                ],
-                "summary": "Delete a resource",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Resource ID",
-                        "name": "resourceId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Resource deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "404": {
-                        "description": "Resource not found: %d"
-                    },
-                    "500": {
-                        "description": "Failed to find resource | Failed to delete resource"
-                    }
-                }
-            },
-            "patch": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "resource"
-                ],
-                "summary": "Update a resource",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Resource ID",
-                        "name": "resourceId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Patch resource request",
-                        "name": "patch",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.UpdateResourceRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Updated resource",
-                        "schema": {
-                            "$ref": "#/definitions/store.Resource"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted patch resource request"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "404": {
-                        "description": "Resource not found: %d"
-                    },
-                    "500": {
-                        "description": "Failed to find resource | Failed to patch resource"
-                    }
-                }
-            }
-        },
-        "/api/v1/status": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "system"
-                ],
-                "summary": "Get system GetSystemStatus",
-                "responses": {
-                    "200": {
-                        "description": "System GetSystemStatus",
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.SystemStatus"
-                        }
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find host user | Failed to find system setting list | Failed to unmarshal system setting customized profile value"
-                    }
-                }
-            }
-        },
-        "/api/v1/storage": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "storage"
-                ],
-                "summary": "Get a list of storages",
-                "responses": {
-                    "200": {
-                        "description": "List of storages",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.Storage"
-                            }
-                        }
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to convert storage"
-                    }
-                }
-            },
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "storage"
-                ],
-                "summary": "Create storage",
-                "parameters": [
-                    {
-                        "description": "Request object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateStorageRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Created storage",
-                        "schema": {
-                            "$ref": "#/definitions/store.Storage"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post storage request"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to create storage | Failed to convert storage"
-                    }
-                }
-            }
-        },
-        "/api/v1/storage/{storageId}": {
-            "delete": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "storage"
-                ],
-                "summary": "Delete a storage",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Storage ID",
-                        "name": "storageId",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Storage deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Storage service %d is using"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
-                    }
-                }
-            },
-            "patch": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "storage"
-                ],
-                "summary": "Update a storage",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "Storage ID",
-                        "name": "storageId",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Patch request",
-                        "name": "patch",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpdateStorageRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Updated resource",
-                        "schema": {
-                            "$ref": "#/definitions/store.Storage"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to patch storage | Failed to convert storage"
-                    }
-                }
-            }
-        },
-        "/api/v1/system/setting": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "system-setting"
-                ],
-                "summary": "Get a list of system settings",
-                "responses": {
-                    "200": {
-                        "description": "System setting list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/api_v1.SystemSetting"
-                            }
-                        }
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to find system setting list"
-                    }
-                }
-            },
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "system-setting"
-                ],
-                "summary": "Create system setting",
-                "parameters": [
-                    {
-                        "description": "Request object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.UpsertSystemSettingRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "400": {
-                        "description": "Malformatted post system setting request | invalid system setting"
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "403": {
-                        "description": "Cannot disable passwords if no SSO identity provider is configured."
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to upsert system setting"
-                    }
-                }
-            }
-        },
-        "/api/v1/system/vacuum": {
-            "post": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "system"
-                ],
-                "summary": "Vacuum the database",
-                "responses": {
-                    "200": {
-                        "description": "Database vacuumed",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "401": {
-                        "description": "Missing user in session | Unauthorized"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to ExecVacuum database"
-                    }
-                }
-            }
-        },
-        "/api/v1/tag": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "tag"
-                ],
-                "summary": "Get a list of tags",
-                "responses": {
-                    "200": {
-                        "description": "Tag list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "type": "string"
-                            }
-                        }
-                    },
-                    "400": {
-                        "description": "Missing user id to find tag"
-                    },
-                    "500": {
-                        "description": "Failed to find tag list"
-                    }
-                }
-            },
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "tag"
-                ],
-                "summary": "Create a tag",
-                "parameters": [
-                    {
-                        "description": "Request object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertTagRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Created tag name",
-                        "schema": {
-                            "type": "string"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post tag request | Tag name shouldn't be empty"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "500": {
-                        "description": "Failed to upsert tag | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/tag/delete": {
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "tag"
-                ],
-                "summary": "Delete a tag",
-                "parameters": [
-                    {
-                        "description": "Request object.",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.DeleteTagRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Tag deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post tag request | Tag name shouldn't be empty"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "500": {
-                        "description": "Failed to delete tag name: %v"
-                    }
-                }
-            }
-        },
-        "/api/v1/tag/suggestion": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "tag"
-                ],
-                "summary": "Get a list of tags suggested from other memos contents",
-                "responses": {
-                    "200": {
-                        "description": "Tag list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "type": "string"
-                            }
-                        }
-                    },
-                    "400": {
-                        "description": "Missing user session"
-                    },
-                    "500": {
-                        "description": "Failed to find memo list | Failed to find tag list"
-                    }
-                }
-            }
-        },
-        "/api/v1/user": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Get a list of users",
-                "responses": {
-                    "200": {
-                        "description": "User list",
-                        "schema": {
-                            "type": "array",
-                            "items": {
-                                "$ref": "#/definitions/store.User"
-                            }
-                        }
-                    },
-                    "500": {
-                        "description": "Failed to fetch user list"
-                    }
-                }
-            },
-            "post": {
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Create a user",
-                "parameters": [
-                    {
-                        "description": "Request object",
-                        "name": "body",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.CreateUserRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Created user",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted post user request | Invalid user create format"
-                    },
-                    "401": {
-                        "description": "Missing auth session | Unauthorized to create user"
-                    },
-                    "403": {
-                        "description": "Could not create host user"
-                    },
-                    "500": {
-                        "description": "Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
-                    }
-                }
-            }
-        },
-        "/api/v1/user/me": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Get current user",
-                "responses": {
-                    "200": {
-                        "description": "Current user",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "401": {
-                        "description": "Missing auth session"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to find userSettingList"
-                    }
-                }
-            }
-        },
-        "/api/v1/user/name/{username}": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Get user by username",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Username",
-                        "name": "username",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Requested user",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "404": {
-                        "description": "User not found"
-                    },
-                    "500": {
-                        "description": "Failed to find user"
-                    }
-                }
-            }
-        },
-        "/api/v1/user/{id}": {
-            "get": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Get user by id",
-                "parameters": [
-                    {
-                        "type": "integer",
-                        "description": "User ID",
-                        "name": "id",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Requested user",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "400": {
-                        "description": "Malformatted user id"
-                    },
-                    "404": {
-                        "description": "User not found"
-                    },
-                    "500": {
-                        "description": "Failed to find user"
-                    }
-                }
-            },
-            "delete": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Delete a user",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "User ID",
-                        "name": "id",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "User deleted",
-                        "schema": {
-                            "type": "boolean"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Current session user not found with ID: %d"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "403": {
-                        "description": "Unauthorized to delete user"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to delete user"
-                    }
-                }
-            },
-            "patch": {
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "user"
-                ],
-                "summary": "Update a user",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "User ID",
-                        "name": "id",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "description": "Patch request",
-                        "name": "patch",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "$ref": "#/definitions/api_v1.UpdateUserRequest"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Updated user",
-                        "schema": {
-                            "$ref": "#/definitions/store.User"
-                        }
-                    },
-                    "400": {
-                        "description": "ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
-                    },
-                    "401": {
-                        "description": "Missing user in session"
-                    },
-                    "403": {
-                        "description": "Unauthorized to update user"
-                    },
-                    "500": {
-                        "description": "Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
-                    }
-                }
-            }
-        },
-        "/o/get/GetImage": {
-            "get": {
-                "produces": [
-                    "GetImage/*"
-                ],
-                "tags": [
-                    "image-url"
-                ],
-                "summary": "Get GetImage from URL",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "description": "Image url",
-                        "name": "url",
-                        "in": "query",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "Image"
-                    },
-                    "400": {
-                        "description": "Missing GetImage url | Wrong url | Failed to get GetImage url: %s"
-                    },
-                    "500": {
-                        "description": "Failed to write GetImage blob"
-                    }
-                }
-            }
-        }
-    },
-    "definitions": {
-        "api_v1.CreateIdentityProviderRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderConfig"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderType"
-                }
-            }
-        },
-        "api_v1.CreateMemoRequest": {
-            "type": "object",
-            "properties": {
-                "content": {
-                    "type": "string"
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "relationList": {
-                    "type": "array",
-                    "items": {
-                        "$ref": "#/definitions/api_v1.UpsertMemoRelationRequest"
-                    }
-                },
-                "resourceIdList": {
-                    "description": "Related fields",
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                },
-                "visibility": {
-                    "description": "Domain specific fields",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/api_v1.Visibility"
-                        }
-                    ]
-                }
-            }
-        },
-        "api_v1.CreateResourceRequest": {
-            "type": "object",
-            "properties": {
-                "externalLink": {
-                    "type": "string"
-                },
-                "filename": {
-                    "type": "string"
-                },
-                "type": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.CreateStorageRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/api_v1.StorageConfig"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/api_v1.StorageType"
-                }
-            }
-        },
-        "api_v1.CreateUserRequest": {
-            "type": "object",
-            "properties": {
-                "email": {
-                    "type": "string"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "password": {
-                    "type": "string"
-                },
-                "role": {
-                    "$ref": "#/definitions/api_v1.Role"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.CustomizedProfile": {
-            "type": "object",
-            "properties": {
-                "appearance": {
-                    "description": "Appearance is the server default appearance.",
-                    "type": "string"
-                },
-                "description": {
-                    "description": "Description is the server description.",
-                    "type": "string"
-                },
-                "locale": {
-                    "description": "Locale is the server default locale.",
-                    "type": "string"
-                },
-                "logoUrl": {
-                    "description": "LogoURL is the url of logo image.",
-                    "type": "string"
-                },
-                "name": {
-                    "description": "Name is the server name, default is ` + "`" + `memos` + "`" + `",
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.DeleteTagRequest": {
-            "type": "object",
-            "properties": {
-                "name": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.FieldMapping": {
-            "type": "object",
-            "properties": {
-                "displayName": {
-                    "type": "string"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "identifier": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.IdentityProvider": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderConfig"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderType"
-                }
-            }
-        },
-        "api_v1.IdentityProviderConfig": {
-            "type": "object",
-            "properties": {
-                "oauth2Config": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderOAuth2Config"
-                }
-            }
-        },
-        "api_v1.IdentityProviderOAuth2Config": {
-            "type": "object",
-            "properties": {
-                "authUrl": {
-                    "type": "string"
-                },
-                "clientId": {
-                    "type": "string"
-                },
-                "clientSecret": {
-                    "type": "string"
-                },
-                "fieldMapping": {
-                    "$ref": "#/definitions/api_v1.FieldMapping"
-                },
-                "scopes": {
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
-                "tokenUrl": {
-                    "type": "string"
-                },
-                "userInfoUrl": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.IdentityProviderType": {
-            "type": "string",
-            "enum": [
-                "OAUTH2"
-            ],
-            "x-enum-varnames": [
-                "IdentityProviderOAuth2Type"
-            ]
-        },
-        "api_v1.MemoRelationType": {
-            "type": "string",
-            "enum": [
-                "REFERENCE",
-                "COMMENT"
-            ],
-            "x-enum-varnames": [
-                "MemoRelationReference",
-                "MemoRelationComment"
-            ]
-        },
-        "api_v1.PatchMemoRequest": {
-            "type": "object",
-            "properties": {
-                "content": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                },
-                "createdTs": {
-                    "description": "Standard fields",
-                    "type": "integer"
-                },
-                "relationList": {
-                    "type": "array",
-                    "items": {
-                        "$ref": "#/definitions/api_v1.UpsertMemoRelationRequest"
-                    }
-                },
-                "resourceIdList": {
-                    "description": "Related fields",
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                },
-                "rowStatus": {
-                    "$ref": "#/definitions/api_v1.RowStatus"
-                },
-                "updatedTs": {
-                    "type": "integer"
-                },
-                "visibility": {
-                    "$ref": "#/definitions/api_v1.Visibility"
-                }
-            }
-        },
-        "api_v1.Role": {
-            "type": "string",
-            "enum": [
-                "HOST",
-                "ADMIN",
-                "USER"
-            ],
-            "x-enum-varnames": [
-                "RoleHost",
-                "RoleAdmin",
-                "RoleUser"
-            ]
-        },
-        "api_v1.RowStatus": {
-            "type": "string",
-            "enum": [
-                "NORMAL",
-                "ARCHIVED"
-            ],
-            "x-enum-varnames": [
-                "Normal",
-                "Archived"
-            ]
-        },
-        "api_v1.SSOSignIn": {
-            "type": "object",
-            "properties": {
-                "code": {
-                    "type": "string"
-                },
-                "identityProviderId": {
-                    "type": "integer"
-                },
-                "redirectUri": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.SignIn": {
-            "type": "object",
-            "properties": {
-                "password": {
-                    "type": "string"
-                },
-                "remember": {
-                    "type": "boolean"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.SignUp": {
-            "type": "object",
-            "properties": {
-                "password": {
-                    "type": "string"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.StorageConfig": {
-            "type": "object",
-            "properties": {
-                "s3Config": {
-                    "$ref": "#/definitions/api_v1.StorageS3Config"
-                }
-            }
-        },
-        "api_v1.StorageS3Config": {
-            "type": "object",
-            "properties": {
-                "accessKey": {
-                    "type": "string"
-                },
-                "bucket": {
-                    "type": "string"
-                },
-                "endPoint": {
-                    "type": "string"
-                },
-                "path": {
-                    "type": "string"
-                },
-                "presign": {
-                    "type": "boolean"
-                },
-                "region": {
-                    "type": "string"
-                },
-                "secretKey": {
-                    "type": "string"
-                },
-                "urlPrefix": {
-                    "type": "string"
-                },
-                "urlSuffix": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.StorageType": {
-            "type": "string",
-            "enum": [
-                "S3"
-            ],
-            "x-enum-varnames": [
-                "StorageS3"
-            ]
-        },
-        "api_v1.SystemSetting": {
-            "type": "object",
-            "properties": {
-                "description": {
-                    "type": "string"
-                },
-                "name": {
-                    "$ref": "#/definitions/api_v1.SystemSettingName"
-                },
-                "value": {
-                    "description": "Value is a JSON string with basic value.",
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.SystemSettingName": {
-            "type": "string",
-            "enum": [
-                "server-id",
-                "secret-session",
-                "disable-public-memos",
-                "max-upload-size-mib",
-                "customized-profile",
-                "storage-service-id",
-                "local-storage-path",
-                "telegram-bot-token",
-                "memo-display-with-updated-ts"
-            ],
-            "x-enum-varnames": [
-                "SystemSettingServerIDName",
-                "SystemSettingSecretSessionName",
-                "SystemSettingDisablePublicMemosName",
-                "SystemSettingMaxUploadSizeMiBName",
-                "SystemSettingCustomizedProfileName",
-                "SystemSettingStorageServiceIDName",
-                "SystemSettingLocalStoragePathName",
-                "SystemSettingTelegramBotTokenName",
-                "SystemSettingMemoDisplayWithUpdatedTsName"
-            ]
-        },
-        "api_v1.SystemStatus": {
-            "type": "object",
-            "properties": {
-                "additionalScript": {
-                    "description": "Additional script.",
-                    "type": "string"
-                },
-                "additionalStyle": {
-                    "description": "Additional style.",
-                    "type": "string"
-                },
-                "allowSignUp": {
-                    "description": "System settings\nAllow sign up.",
-                    "type": "boolean"
-                },
-                "customizedProfile": {
-                    "description": "Customized server profile, including server name and external url.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/api_v1.CustomizedProfile"
-                        }
-                    ]
-                },
-                "dbSize": {
-                    "type": "integer"
-                },
-                "disablePasswordLogin": {
-                    "description": "Disable password login.",
-                    "type": "boolean"
-                },
-                "disablePublicMemos": {
-                    "description": "Disable public memos.",
-                    "type": "boolean"
-                },
-                "host": {
-                    "$ref": "#/definitions/api_v1.User"
-                },
-                "localStoragePath": {
-                    "description": "Local storage path.",
-                    "type": "string"
-                },
-                "maxUploadSizeMiB": {
-                    "description": "Max upload size.",
-                    "type": "integer"
-                },
-                "memoDisplayWithUpdatedTs": {
-                    "description": "Memo display with updated timestamp.",
-                    "type": "boolean"
-                },
-                "profile": {
-                    "$ref": "#/definitions/profile.Profile"
-                },
-                "storageServiceId": {
-                    "description": "Storage service ID.",
-                    "type": "integer"
-                }
-            }
-        },
-        "api_v1.UpdateIdentityProviderRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderConfig"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/api_v1.IdentityProviderType"
-                }
-            }
-        },
-        "api_v1.UpdateResourceRequest": {
-            "type": "object",
-            "properties": {
-                "filename": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.UpdateStorageRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/api_v1.StorageConfig"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/api_v1.StorageType"
-                }
-            }
-        },
-        "api_v1.UpdateUserRequest": {
-            "type": "object",
-            "properties": {
-                "avatarUrl": {
-                    "type": "string"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "password": {
-                    "type": "string"
-                },
-                "rowStatus": {
-                    "$ref": "#/definitions/api_v1.RowStatus"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.UpsertMemoOrganizerRequest": {
-            "type": "object",
-            "properties": {
-                "pinned": {
-                    "type": "boolean"
-                }
-            }
-        },
-        "api_v1.UpsertMemoRelationRequest": {
-            "type": "object",
-            "properties": {
-                "relatedMemoId": {
-                    "type": "integer"
-                },
-                "type": {
-                    "$ref": "#/definitions/api_v1.MemoRelationType"
-                }
-            }
-        },
-        "api_v1.UpsertSystemSettingRequest": {
-            "type": "object",
-            "properties": {
-                "description": {
-                    "type": "string"
-                },
-                "name": {
-                    "$ref": "#/definitions/api_v1.SystemSettingName"
-                },
-                "value": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.UpsertTagRequest": {
-            "type": "object",
-            "properties": {
-                "name": {
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.User": {
-            "type": "object",
-            "properties": {
-                "avatarUrl": {
-                    "type": "string"
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "role": {
-                    "$ref": "#/definitions/api_v1.Role"
-                },
-                "rowStatus": {
-                    "description": "Standard fields",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/api_v1.RowStatus"
-                        }
-                    ]
-                },
-                "updatedTs": {
-                    "type": "integer"
-                },
-                "username": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                }
-            }
-        },
-        "api_v1.Visibility": {
-            "type": "string",
-            "enum": [
-                "PUBLIC",
-                "PROTECTED",
-                "PRIVATE"
-            ],
-            "x-enum-varnames": [
-                "Public",
-                "Protected",
-                "Private"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.CreateIdentityProviderRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.CreateMemoRequest": {
-            "type": "object",
-            "properties": {
-                "content": {
-                    "type": "string"
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "relationList": {
-                    "type": "array",
-                    "items": {
-                        "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
-                    }
-                },
-                "resourceIdList": {
-                    "description": "Related fields",
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                },
-                "visibility": {
-                    "description": "Domain specific fields",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.Visibility"
-                        }
-                    ]
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.CreateResourceRequest": {
-            "type": "object",
-            "properties": {
-                "externalLink": {
-                    "type": "string"
-                },
-                "filename": {
-                    "type": "string"
-                },
-                "type": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.CreateStorageRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageConfig"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageType"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.CreateUserRequest": {
-            "type": "object",
-            "properties": {
-                "email": {
-                    "type": "string"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "password": {
-                    "type": "string"
-                },
-                "role": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.Role"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.CustomizedProfile": {
-            "type": "object",
-            "properties": {
-                "appearance": {
-                    "description": "Appearance is the server default appearance.",
-                    "type": "string"
-                },
-                "description": {
-                    "description": "Description is the server description.",
-                    "type": "string"
-                },
-                "locale": {
-                    "description": "Locale is the server default locale.",
-                    "type": "string"
-                },
-                "logoUrl": {
-                    "description": "LogoURL is the url of logo image.",
-                    "type": "string"
-                },
-                "name": {
-                    "description": "Name is the server name, default is ` + "`" + `memos` + "`" + `",
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.DeleteTagRequest": {
-            "type": "object",
-            "properties": {
-                "name": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.FieldMapping": {
-            "type": "object",
-            "properties": {
-                "displayName": {
-                    "type": "string"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "identifier": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.IdentityProvider": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.IdentityProviderConfig": {
-            "type": "object",
-            "properties": {
-                "oauth2Config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config": {
-            "type": "object",
-            "properties": {
-                "authUrl": {
-                    "type": "string"
-                },
-                "clientId": {
-                    "type": "string"
-                },
-                "clientSecret": {
-                    "type": "string"
-                },
-                "fieldMapping": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.FieldMapping"
-                },
-                "scopes": {
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
-                "tokenUrl": {
-                    "type": "string"
-                },
-                "userInfoUrl": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.IdentityProviderType": {
-            "type": "string",
-            "enum": [
-                "OAUTH2"
-            ],
-            "x-enum-varnames": [
-                "IdentityProviderOAuth2Type"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.MemoRelationType": {
-            "type": "string",
-            "enum": [
-                "REFERENCE",
-                "COMMENT"
-            ],
-            "x-enum-varnames": [
-                "MemoRelationReference",
-                "MemoRelationComment"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.PatchMemoRequest": {
-            "type": "object",
-            "properties": {
-                "content": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                },
-                "createdTs": {
-                    "description": "Standard fields",
-                    "type": "integer"
-                },
-                "relationList": {
-                    "type": "array",
-                    "items": {
-                        "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
-                    }
-                },
-                "resourceIdList": {
-                    "description": "Related fields",
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                },
-                "rowStatus": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus"
-                },
-                "updatedTs": {
-                    "type": "integer"
-                },
-                "visibility": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.Visibility"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.Role": {
-            "type": "string",
-            "enum": [
-                "HOST",
-                "ADMIN",
-                "USER"
-            ],
-            "x-enum-varnames": [
-                "RoleHost",
-                "RoleAdmin",
-                "RoleUser"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.RowStatus": {
-            "type": "string",
-            "enum": [
-                "NORMAL",
-                "ARCHIVED"
-            ],
-            "x-enum-varnames": [
-                "Normal",
-                "Archived"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.SSOSignIn": {
-            "type": "object",
-            "properties": {
-                "code": {
-                    "type": "string"
-                },
-                "identityProviderId": {
-                    "type": "integer"
-                },
-                "redirectUri": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.SignIn": {
-            "type": "object",
-            "properties": {
-                "password": {
-                    "type": "string"
-                },
-                "remember": {
-                    "type": "boolean"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.SignUp": {
-            "type": "object",
-            "properties": {
-                "password": {
-                    "type": "string"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.StorageConfig": {
-            "type": "object",
-            "properties": {
-                "s3Config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageS3Config"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.StorageS3Config": {
-            "type": "object",
-            "properties": {
-                "accessKey": {
-                    "type": "string"
-                },
-                "bucket": {
-                    "type": "string"
-                },
-                "endPoint": {
-                    "type": "string"
-                },
-                "path": {
-                    "type": "string"
-                },
-                "presign": {
-                    "type": "boolean"
-                },
-                "region": {
-                    "type": "string"
-                },
-                "secretKey": {
-                    "type": "string"
-                },
-                "urlPrefix": {
-                    "type": "string"
-                },
-                "urlSuffix": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.StorageType": {
-            "type": "string",
-            "enum": [
-                "S3"
-            ],
-            "x-enum-varnames": [
-                "StorageS3"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.SystemSetting": {
-            "type": "object",
-            "properties": {
-                "description": {
-                    "type": "string"
-                },
-                "name": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemSettingName"
-                },
-                "value": {
-                    "description": "Value is a JSON string with basic value.",
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.SystemSettingName": {
-            "type": "string",
-            "enum": [
-                "server-id",
-                "secret-session",
-                "disable-public-memos",
-                "max-upload-size-mib",
-                "customized-profile",
-                "storage-service-id",
-                "local-storage-path",
-                "telegram-bot-token",
-                "memo-display-with-updated-ts"
-            ],
-            "x-enum-varnames": [
-                "SystemSettingServerIDName",
-                "SystemSettingSecretSessionName",
-                "SystemSettingDisablePublicMemosName",
-                "SystemSettingMaxUploadSizeMiBName",
-                "SystemSettingCustomizedProfileName",
-                "SystemSettingStorageServiceIDName",
-                "SystemSettingLocalStoragePathName",
-                "SystemSettingTelegramBotTokenName",
-                "SystemSettingMemoDisplayWithUpdatedTsName"
-            ]
-        },
-        "github_com_usememos_memos_api_v1.SystemStatus": {
-            "type": "object",
-            "properties": {
-                "additionalScript": {
-                    "description": "Additional script.",
-                    "type": "string"
-                },
-                "additionalStyle": {
-                    "description": "Additional style.",
-                    "type": "string"
-                },
-                "allowSignUp": {
-                    "description": "System settings\nAllow sign up.",
-                    "type": "boolean"
-                },
-                "customizedProfile": {
-                    "description": "Customized server profile, including server name and external url.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.CustomizedProfile"
-                        }
-                    ]
-                },
-                "dbSize": {
-                    "type": "integer"
-                },
-                "disablePasswordLogin": {
-                    "description": "Disable password login.",
-                    "type": "boolean"
-                },
-                "disablePublicMemos": {
-                    "description": "Disable public memos.",
-                    "type": "boolean"
-                },
-                "host": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.User"
-                },
-                "localStoragePath": {
-                    "description": "Local storage path.",
-                    "type": "string"
-                },
-                "maxUploadSizeMiB": {
-                    "description": "Max upload size.",
-                    "type": "integer"
-                },
-                "memoDisplayWithUpdatedTs": {
-                    "description": "Memo display with updated timestamp.",
-                    "type": "boolean"
-                },
-                "profile": {
-                    "$ref": "#/definitions/profile.Profile"
-                },
-                "storageServiceId": {
-                    "description": "Storage service ID.",
-                    "type": "integer"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpdateResourceRequest": {
-            "type": "object",
-            "properties": {
-                "filename": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpdateStorageRequest": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageConfig"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageType"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpdateUserRequest": {
-            "type": "object",
-            "properties": {
-                "avatarUrl": {
-                    "type": "string"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "password": {
-                    "type": "string"
-                },
-                "rowStatus": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus"
-                },
-                "username": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest": {
-            "type": "object",
-            "properties": {
-                "pinned": {
-                    "type": "boolean"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpsertMemoRelationRequest": {
-            "type": "object",
-            "properties": {
-                "relatedMemoId": {
-                    "type": "integer"
-                },
-                "type": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.MemoRelationType"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpsertSystemSettingRequest": {
-            "type": "object",
-            "properties": {
-                "description": {
-                    "type": "string"
-                },
-                "name": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemSettingName"
-                },
-                "value": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.UpsertTagRequest": {
-            "type": "object",
-            "properties": {
-                "name": {
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.User": {
-            "type": "object",
-            "properties": {
-                "avatarUrl": {
-                    "type": "string"
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "role": {
-                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.Role"
-                },
-                "rowStatus": {
-                    "description": "Standard fields",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus"
-                        }
-                    ]
-                },
-                "updatedTs": {
-                    "type": "integer"
-                },
-                "username": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                }
-            }
-        },
-        "github_com_usememos_memos_api_v1.Visibility": {
-            "type": "string",
-            "enum": [
-                "PUBLIC",
-                "PROTECTED",
-                "PRIVATE"
-            ],
-            "x-enum-varnames": [
-                "Public",
-                "Protected",
-                "Private"
-            ]
-        },
-        "profile.Profile": {
-            "type": "object",
-            "properties": {
-                "mode": {
-                    "description": "Mode can be \"prod\" or \"dev\" or \"demo\"",
-                    "type": "string"
-                },
-                "version": {
-                    "description": "Version is the current version of server",
-                    "type": "string"
-                }
-            }
-        },
-        "store.FieldMapping": {
-            "type": "object",
-            "properties": {
-                "displayName": {
-                    "type": "string"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "identifier": {
-                    "type": "string"
-                }
-            }
-        },
-        "store.IdentityProvider": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "$ref": "#/definitions/store.IdentityProviderConfig"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "identifierFilter": {
-                    "type": "string"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "$ref": "#/definitions/store.IdentityProviderType"
-                }
-            }
-        },
-        "store.IdentityProviderConfig": {
-            "type": "object",
-            "properties": {
-                "oauth2Config": {
-                    "$ref": "#/definitions/store.IdentityProviderOAuth2Config"
-                }
-            }
-        },
-        "store.IdentityProviderOAuth2Config": {
-            "type": "object",
-            "properties": {
-                "authUrl": {
-                    "type": "string"
-                },
-                "clientId": {
-                    "type": "string"
-                },
-                "clientSecret": {
-                    "type": "string"
-                },
-                "fieldMapping": {
-                    "$ref": "#/definitions/store.FieldMapping"
-                },
-                "scopes": {
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
-                "tokenUrl": {
-                    "type": "string"
-                },
-                "userInfoUrl": {
-                    "type": "string"
-                }
-            }
-        },
-        "store.IdentityProviderType": {
-            "type": "string",
-            "enum": [
-                "OAUTH2"
-            ],
-            "x-enum-varnames": [
-                "IdentityProviderOAuth2Type"
-            ]
-        },
-        "store.Memo": {
-            "type": "object",
-            "properties": {
-                "content": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "creatorID": {
-                    "type": "integer"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "parentID": {
-                    "type": "integer"
-                },
-                "pinned": {
-                    "description": "Composed fields",
-                    "type": "boolean"
-                },
-                "resourceName": {
-                    "type": "string"
-                },
-                "rowStatus": {
-                    "description": "Standard fields",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/store.RowStatus"
-                        }
-                    ]
-                },
-                "updatedTs": {
-                    "type": "integer"
-                },
-                "visibility": {
-                    "$ref": "#/definitions/store.Visibility"
-                }
-            }
-        },
-        "store.MemoRelation": {
-            "type": "object",
-            "properties": {
-                "memoID": {
-                    "type": "integer"
-                },
-                "relatedMemoID": {
-                    "type": "integer"
-                },
-                "type": {
-                    "$ref": "#/definitions/store.MemoRelationType"
-                }
-            }
-        },
-        "store.MemoRelationType": {
-            "type": "string",
-            "enum": [
-                "REFERENCE",
-                "COMMENT"
-            ],
-            "x-enum-varnames": [
-                "MemoRelationReference",
-                "MemoRelationComment"
-            ]
-        },
-        "store.Resource": {
-            "type": "object",
-            "properties": {
-                "blob": {
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "creatorID": {
-                    "description": "Standard fields",
-                    "type": "integer"
-                },
-                "externalLink": {
-                    "type": "string"
-                },
-                "filename": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "internalPath": {
-                    "type": "string"
-                },
-                "memoID": {
-                    "type": "integer"
-                },
-                "resourceName": {
-                    "type": "string"
-                },
-                "size": {
-                    "type": "integer"
-                },
-                "type": {
-                    "type": "string"
-                },
-                "updatedTs": {
-                    "type": "integer"
-                }
-            }
-        },
-        "store.Role": {
-            "type": "string",
-            "enum": [
-                "HOST",
-                "ADMIN",
-                "USER"
-            ],
-            "x-enum-varnames": [
-                "RoleHost",
-                "RoleAdmin",
-                "RoleUser"
-            ]
-        },
-        "store.RowStatus": {
-            "type": "string",
-            "enum": [
-                "NORMAL",
-                "ARCHIVED"
-            ],
-            "x-enum-varnames": [
-                "Normal",
-                "Archived"
-            ]
-        },
-        "store.Storage": {
-            "type": "object",
-            "properties": {
-                "config": {
-                    "type": "string"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "name": {
-                    "type": "string"
-                },
-                "type": {
-                    "type": "string"
-                }
-            }
-        },
-        "store.User": {
-            "type": "object",
-            "properties": {
-                "avatarURL": {
-                    "type": "string"
-                },
-                "createdTs": {
-                    "type": "integer"
-                },
-                "email": {
-                    "type": "string"
-                },
-                "id": {
-                    "type": "integer"
-                },
-                "nickname": {
-                    "type": "string"
-                },
-                "passwordHash": {
-                    "type": "string"
-                },
-                "role": {
-                    "$ref": "#/definitions/store.Role"
-                },
-                "rowStatus": {
-                    "description": "Standard fields",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/store.RowStatus"
-                        }
-                    ]
-                },
-                "updatedTs": {
-                    "type": "integer"
-                },
-                "username": {
-                    "description": "Domain specific fields",
-                    "type": "string"
-                }
-            }
-        },
-        "store.Visibility": {
-            "type": "string",
-            "enum": [
-                "PUBLIC",
-                "PROTECTED",
-                "PRIVATE"
-            ],
-            "x-enum-varnames": [
-                "Public",
-                "Protected",
-                "Private"
-            ]
-        }
-    },
-    "externalDocs": {
-        "description": "Find out more about Memos.",
-        "url": "https://usememos.com/"
-    }
-}`
-
-// SwaggerInfo holds exported Swagger Info so clients can modify it
-var SwaggerInfo = &swag.Spec{
-	Version:          "1.0",
-	Host:             "",
-	BasePath:         "/",
-	Schemes:          []string{},
-	Title:            "memos API",
-	Description:      "A privacy-first, lightweight note-taking service.",
-	InfoInstanceName: "swagger",
-	SwaggerTemplate:  docTemplate,
-	LeftDelim:        "{{",
-	RightDelim:       "}}",
-}
-
-func init() {
-	swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
-}
diff --git a/server/route/api/v1/http_getter.go b/server/route/api/v1/http_getter.go
deleted file mode 100644
index e63c5ab0d..000000000
--- a/server/route/api/v1/http_getter.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package v1
-
-import (
-	"fmt"
-	"net/http"
-	"net/url"
-
-	"github.com/labstack/echo/v4"
-
-	getter "github.com/usememos/memos/plugin/http-getter"
-)
-
-func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
-	// GET /get/image?url={url} - Get image.
-	g.GET("/get/image", GetImage)
-}
-
-// GetImage godoc
-//
-//	@Summary	Get GetImage from URL
-//	@Tags		image-url
-//	@Produce	GetImage/*
-//	@Param		url	query		string	true	"Image url"
-//	@Success	200	{object}	nil		"Image"
-//	@Failure	400	{object}	nil		"Missing GetImage url | Wrong url | Failed to get GetImage url: %s"
-//	@Failure	500	{object}	nil		"Failed to write GetImage blob"
-//	@Router		/o/get/GetImage [GET]
-func GetImage(c echo.Context) error {
-	urlStr := c.QueryParam("url")
-	if urlStr == "" {
-		return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
-	}
-	if _, err := url.Parse(urlStr); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
-	}
-
-	image, err := getter.GetImage(urlStr)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
-	}
-
-	c.Response().Writer.WriteHeader(http.StatusOK)
-	c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
-	c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
-	if _, err := c.Response().Writer.Write(image.Blob); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
-	}
-	return nil
-}
diff --git a/server/route/api/v1/jwt.go b/server/route/api/v1/jwt.go
deleted file mode 100644
index 3a60eedb7..000000000
--- a/server/route/api/v1/jwt.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package v1
-
-import (
-	"fmt"
-	"log/slog"
-	"net/http"
-	"strings"
-
-	"github.com/golang-jwt/jwt/v5"
-	"github.com/labstack/echo/v4"
-	"github.com/pkg/errors"
-
-	"github.com/usememos/memos/internal/util"
-	storepb "github.com/usememos/memos/proto/gen/store"
-	"github.com/usememos/memos/server/route/api/auth"
-	"github.com/usememos/memos/store"
-)
-
-const (
-	// The key name used to store user id in the context
-	// user id is extracted from the jwt token subject field.
-	userIDContextKey = "user-id"
-)
-
-func extractTokenFromHeader(c echo.Context) (string, error) {
-	authHeader := c.Request().Header.Get("Authorization")
-	if authHeader == "" {
-		return "", nil
-	}
-
-	authHeaderParts := strings.Fields(authHeader)
-	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
-		return "", errors.New("Authorization header format must be Bearer {token}")
-	}
-
-	return authHeaderParts[1], nil
-}
-
-func findAccessToken(c echo.Context) string {
-	// Check the HTTP request header first.
-	accessToken, _ := extractTokenFromHeader(c)
-	if accessToken == "" {
-		// Check the cookie.
-		cookie, _ := c.Cookie(auth.AccessTokenCookieName)
-		if cookie != nil {
-			accessToken = cookie.Value
-		}
-	}
-	return accessToken
-}
-
-// JWTMiddleware validates the access token.
-func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) echo.HandlerFunc {
-	return func(c echo.Context) error {
-		ctx := c.Request().Context()
-		path := c.Request().URL.Path
-		method := c.Request().Method
-
-		if server.defaultAuthSkipper(c) {
-			return next(c)
-		}
-
-		// Skip validation for server status endpoints.
-		if util.HasPrefixes(path, "/api/v1/ping", "/api/v1/status") && method == http.MethodGet {
-			return next(c)
-		}
-
-		accessToken := findAccessToken(c)
-		if accessToken == "" {
-			// Allow the user to access the public endpoints.
-			if util.HasPrefixes(path, "/o") {
-				return next(c)
-			}
-			// When the request is not authenticated, we allow the user to access the memo endpoints for those public memos.
-			if util.HasPrefixes(path, "/api/v1/idp", "/api/v1/memo", "/api/v1/user") && path != "/api/v1/user" && method == http.MethodGet {
-				return next(c)
-			}
-			return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
-		}
-
-		userID, err := getUserIDFromAccessToken(accessToken, secret)
-		if err != nil {
-			err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken)
-			if err != nil {
-				slog.Warn("fail to remove AccessToken and Cookies", err)
-			}
-			return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired access token")
-		}
-
-		accessTokens, err := server.Store.GetUserAccessTokens(ctx, userID)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user access tokens.").WithInternal(err)
-		}
-		if !validateAccessToken(accessToken, accessTokens) {
-			err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken)
-			if err != nil {
-				slog.Warn("fail to remove AccessToken and Cookies", err)
-			}
-			return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.")
-		}
-
-		// Even if there is no error, we still need to make sure the user still exists.
-		user, err := server.Store.GetUser(ctx, &store.FindUser{
-			ID: &userID,
-		})
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to find user ID: %d", userID)).SetInternal(err)
-		}
-		if user == nil {
-			return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Failed to find user ID: %d", userID))
-		}
-
-		// Stores userID into context.
-		c.Set(userIDContextKey, userID)
-		return next(c)
-	}
-}
-
-func getUserIDFromAccessToken(accessToken, secret string) (int32, error) {
-	claims := &auth.ClaimsMessage{}
-	_, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
-		if t.Method.Alg() != jwt.SigningMethodHS256.Name {
-			return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
-		}
-		if kid, ok := t.Header["kid"].(string); ok {
-			if kid == "v1" {
-				return []byte(secret), nil
-			}
-		}
-		return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
-	})
-	if err != nil {
-		return 0, errors.Wrap(err, "Invalid or expired access token")
-	}
-	// We either have a valid access token or we will attempt to generate new access token.
-	userID, err := util.ConvertStringToInt32(claims.Subject)
-	if err != nil {
-		return 0, errors.Wrap(err, "Malformed ID in the token")
-	}
-	return userID, nil
-}
-
-func (*APIV1Service) defaultAuthSkipper(c echo.Context) bool {
-	path := c.Path()
-	return util.HasPrefixes(path, "/api/v1/auth")
-}
-
-func validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
-	for _, userAccessToken := range userAccessTokens {
-		if accessTokenString == userAccessToken.AccessToken {
-			return true
-		}
-	}
-	return false
-}
diff --git a/server/route/api/v1/memo.go b/server/route/api/v1/memo.go
deleted file mode 100644
index 0b92121d0..000000000
--- a/server/route/api/v1/memo.go
+++ /dev/null
@@ -1,943 +0,0 @@
-package v1
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"log/slog"
-	"net/http"
-	"strconv"
-	"time"
-
-	"github.com/labstack/echo/v4"
-	"github.com/lithammer/shortuuid/v4"
-	"github.com/pkg/errors"
-
-	"github.com/usememos/memos/internal/util"
-	"github.com/usememos/memos/plugin/webhook"
-	storepb "github.com/usememos/memos/proto/gen/store"
-	"github.com/usememos/memos/store"
-)
-
-// 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"
-}
-
-type Memo struct {
-	ID   int32  `json:"id"`
-	Name string `json:"name"`
-
-	// Standard fields
-	RowStatus RowStatus `json:"rowStatus"`
-	CreatorID int32     `json:"creatorId"`
-	CreatedTs int64     `json:"createdTs"`
-	UpdatedTs int64     `json:"updatedTs"`
-
-	// Domain specific fields
-	DisplayTs  int64      `json:"displayTs"`
-	Content    string     `json:"content"`
-	Visibility Visibility `json:"visibility"`
-	Pinned     bool       `json:"pinned"`
-
-	// Related fields
-	CreatorName     string          `json:"creatorName"`
-	CreatorUsername string          `json:"creatorUsername"`
-	ResourceList    []*Resource     `json:"resourceList"`
-	RelationList    []*MemoRelation `json:"relationList"`
-}
-
-type CreateMemoRequest struct {
-	// Standard fields
-	CreatorID int32  `json:"-"`
-	CreatedTs *int64 `json:"createdTs"`
-
-	// Domain specific fields
-	Visibility Visibility `json:"visibility"`
-	Content    string     `json:"content"`
-
-	// Related fields
-	ResourceIDList []int32                      `json:"resourceIdList"`
-	RelationList   []*UpsertMemoRelationRequest `json:"relationList"`
-}
-
-type PatchMemoRequest struct {
-	ID int32 `json:"-"`
-
-	// Standard fields
-	CreatedTs *int64 `json:"createdTs"`
-	UpdatedTs *int64
-	RowStatus *RowStatus `json:"rowStatus"`
-
-	// Domain specific fields
-	Content    *string     `json:"content"`
-	Visibility *Visibility `json:"visibility"`
-
-	// Related fields
-	ResourceIDList []int32                      `json:"resourceIdList"`
-	RelationList   []*UpsertMemoRelationRequest `json:"relationList"`
-}
-
-type FindMemoRequest struct {
-	ID *int32
-
-	// Standard fields
-	RowStatus *RowStatus
-	CreatorID *int32
-
-	// Domain specific fields
-	Pinned         *bool
-	ContentSearch  []string
-	VisibilityList []Visibility
-
-	// Pagination
-	Limit  *int
-	Offset *int
-}
-
-// maxContentLength means the max memo content bytes is 1MB.
-const maxContentLength = 1 << 30
-
-func (s *APIV1Service) registerMemoRoutes(g *echo.Group) {
-	g.GET("/memo", s.GetMemoList)
-	g.POST("/memo", s.CreateMemo)
-	g.GET("/memo/all", s.GetAllMemos)
-	g.GET("/memo/stats", s.GetMemoStats)
-	g.GET("/memo/:memoId", s.GetMemo)
-	g.PATCH("/memo/:memoId", s.UpdateMemo)
-	g.DELETE("/memo/:memoId", s.DeleteMemo)
-}
-
-// GetMemoList godoc
-//
-//	@Summary	Get a list of memos matching optional filters
-//	@Tags		memo
-//	@Produce	json
-//	@Param		creatorId		query		int				false	"Creator ID"
-//	@Param		creatorUsername	query		string			false	"Creator username"
-//	@Param		rowStatus		query		store.RowStatus	false	"Row status"
-//	@Param		pinned			query		bool			false	"Pinned"
-//	@Param		tag				query		string			false	"Search for tag. Do not append #"
-//	@Param		content			query		string			false	"Search for content"
-//	@Param		limit			query		int				false	"Limit"
-//	@Param		offset			query		int				false	"Offset"
-//	@Success	200				{object}	[]store.Memo	"Memo list"
-//	@Failure	400				{object}	nil				"Missing user to find memo"
-//	@Failure	500				{object}	nil				"Failed to get memo display with updated ts setting value | Failed to fetch memo list | Failed to compose memo response"
-//	@Router		/api/v1/memo [GET]
-func (s *APIV1Service) GetMemoList(c echo.Context) error {
-	ctx := c.Request().Context()
-	find := &store.FindMemo{
-		OrderByPinned: true,
-	}
-	if userID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil {
-		find.CreatorID = &userID
-	}
-
-	if username := c.QueryParam("creatorUsername"); username != "" {
-		user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
-		if user != nil {
-			find.CreatorID = &user.ID
-		}
-	}
-
-	currentUserID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		// Anonymous use should only fetch PUBLIC memos with specified user
-		if find.CreatorID == nil {
-			return echo.NewHTTPError(http.StatusBadRequest, "Missing user to find memo")
-		}
-		find.VisibilityList = []store.Visibility{store.Public}
-	} else {
-		// Authorized user can fetch all PUBLIC/PROTECTED memo
-		visibilityList := []store.Visibility{store.Public, store.Protected}
-
-		// If Creator is authorized user (as default), PRIVATE memo is OK
-		if find.CreatorID == nil || *find.CreatorID == currentUserID {
-			find.CreatorID = &currentUserID
-			visibilityList = append(visibilityList, store.Private)
-		}
-		find.VisibilityList = visibilityList
-	}
-
-	rowStatus := store.RowStatus(c.QueryParam("rowStatus"))
-	if rowStatus != "" {
-		find.RowStatus = &rowStatus
-	}
-
-	contentSearch := []string{}
-	tag := c.QueryParam("tag")
-	if tag != "" {
-		contentSearch = append(contentSearch, "#"+tag)
-	}
-	content := c.QueryParam("content")
-	if content != "" {
-		contentSearch = append(contentSearch, content)
-	}
-	find.ContentSearch = contentSearch
-
-	if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
-		find.Limit = &limit
-	}
-	if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
-		find.Offset = &offset
-	}
-
-	list, err := s.Store.ListMemos(ctx, find)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
-	}
-	memoResponseList := []*Memo{}
-	for _, memo := range list {
-		memoResponse, err := s.convertMemoFromStore(ctx, memo)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
-		}
-		memoResponseList = append(memoResponseList, memoResponse)
-	}
-	return c.JSON(http.StatusOK, memoResponseList)
-}
-
-// CreateMemo godoc
-//
-//	@Summary		Create a memo
-//	@Description	Visibility can be PUBLIC, PROTECTED or PRIVATE
-//	@Description	*You should omit fields to use their default values
-//	@Tags			memo
-//	@Accept			json
-//	@Produce		json
-//	@Param			body	body		CreateMemoRequest	true	"Request object."
-//	@Success		200		{object}	store.Memo			"Stored memo"
-//	@Failure		400		{object}	nil					"Malformatted post memo request | Content size overflow, up to 1MB"
-//	@Failure		401		{object}	nil					"Missing user in session"
-//	@Failure		404		{object}	nil					"User not found | Memo not found: %d"
-//	@Failure		500		{object}	nil					"Failed to find user setting | Failed to unmarshal user setting value | Failed to find system setting | Failed to unmarshal system setting | Failed to find user | Failed to create memo | Failed to create activity | Failed to upsert memo resource | Failed to upsert memo relation | Failed to compose memo | Failed to compose memo response"
-//	@Router			/api/v1/memo [POST]
-//
-// NOTES:
-// - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
-func (s *APIV1Service) CreateMemo(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	createMemoRequest := &CreateMemoRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(createMemoRequest); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
-	}
-	if len(createMemoRequest.Content) > maxContentLength {
-		return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB")
-	}
-
-	if createMemoRequest.Visibility == "" {
-		userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
-			UserID: &userID,
-			Key:    storepb.UserSettingKey_USER_SETTING_MEMO_VISIBILITY,
-		})
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
-		}
-		if userMemoVisibilitySetting != nil {
-			createMemoRequest.Visibility = Visibility(userMemoVisibilitySetting.GetMemoVisibility())
-		} else {
-			// Private is the default memo visibility.
-			createMemoRequest.Visibility = Private
-		}
-	}
-
-	createMemoRequest.CreatorID = userID
-	memo, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
-	}
-
-	for _, resourceID := range createMemoRequest.ResourceIDList {
-		if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
-			ID:     resourceID,
-			MemoID: &memo.ID,
-		}); err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
-		}
-	}
-
-	for _, memoRelationUpsert := range createMemoRequest.RelationList {
-		if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
-			MemoID:        memo.ID,
-			RelatedMemoID: memoRelationUpsert.RelatedMemoID,
-			Type:          store.MemoRelationType(memoRelationUpsert.Type),
-		}); err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
-		}
-		if memo.Visibility != store.Private && memoRelationUpsert.Type == MemoRelationComment {
-			relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
-				ID: &memoRelationUpsert.RelatedMemoID,
-			})
-			if err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get related memo").SetInternal(err)
-			}
-			if relatedMemo.CreatorID != memo.CreatorID {
-				activity, err := s.Store.CreateActivity(ctx, &store.Activity{
-					CreatorID: memo.CreatorID,
-					Type:      store.ActivityTypeMemoComment,
-					Level:     store.ActivityLevelInfo,
-					Payload: &storepb.ActivityPayload{
-						MemoComment: &storepb.ActivityMemoCommentPayload{
-							MemoId:        memo.ID,
-							RelatedMemoId: memoRelationUpsert.RelatedMemoID,
-						},
-					},
-				})
-				if err != nil {
-					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
-				}
-				if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
-					SenderID:   memo.CreatorID,
-					ReceiverID: relatedMemo.CreatorID,
-					Status:     store.UNREAD,
-					Message: &storepb.InboxMessage{
-						Type:       storepb.InboxMessage_TYPE_MEMO_COMMENT,
-						ActivityId: &activity.ID,
-					},
-				}); err != nil {
-					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create inbox").SetInternal(err)
-				}
-			}
-		}
-	}
-
-	composedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
-		ID: &memo.ID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
-	}
-	if composedMemo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memo.ID))
-	}
-
-	memoResponse, err := s.convertMemoFromStore(ctx, composedMemo)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
-	}
-
-	// Send notification to telegram if memo is not private.
-	if memoResponse.Visibility != Private {
-		// fetch all telegram UserID
-		userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID})
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to ListUserSettings").SetInternal(err)
-		}
-		for _, userSetting := range userSettings {
-			tgUserID, err := strconv.ParseInt(userSetting.GetTelegramUserId(), 10, 64)
-			if err != nil {
-				continue
-			}
-
-			// send notification to telegram
-			content := memoResponse.CreatorName + " Says:\n\n" + memoResponse.Content
-			_, err = s.telegramBot.SendMessage(ctx, tgUserID, content)
-			if err != nil {
-				continue
-			}
-		}
-	}
-	// Try to dispatch webhook when memo is created.
-	if err := s.DispatchMemoCreatedWebhook(ctx, memoResponse); err != nil {
-		slog.Warn("Failed to dispatch memo created webhook", err)
-	}
-
-	return c.JSON(http.StatusOK, memoResponse)
-}
-
-// GetAllMemos godoc
-//
-//	@Summary		Get a list of public memos matching optional filters
-//	@Description	This should also list protected memos if the user is logged in
-//	@Description	Authentication is optional
-//	@Tags			memo
-//	@Produce		json
-//	@Param			limit	query		int				false	"Limit"
-//	@Param			offset	query		int				false	"Offset"
-//	@Success		200		{object}	[]store.Memo	"Memo list"
-//	@Failure		500		{object}	nil				"Failed to get memo display with updated ts setting value | Failed to fetch all memo list | Failed to compose memo response"
-//	@Router			/api/v1/memo/all [GET]
-//
-//	NOTES:
-//	- creatorUsername is listed at ./web/src/helpers/api.ts:82, but it's not present here
-func (s *APIV1Service) GetAllMemos(c echo.Context) error {
-	ctx := c.Request().Context()
-	memoFind := &store.FindMemo{}
-	_, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		memoFind.VisibilityList = []store.Visibility{store.Public}
-	} else {
-		memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
-	}
-
-	if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
-		memoFind.Limit = &limit
-	}
-	if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
-		memoFind.Offset = &offset
-	}
-
-	// Only fetch normal status memos.
-	normalStatus := store.Normal
-	memoFind.RowStatus = &normalStatus
-
-	list, err := s.Store.ListMemos(ctx, memoFind)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
-	}
-	memoResponseList := []*Memo{}
-	for _, memo := range list {
-		memoResponse, err := s.convertMemoFromStore(ctx, memo)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
-		}
-		memoResponseList = append(memoResponseList, memoResponse)
-	}
-	return c.JSON(http.StatusOK, memoResponseList)
-}
-
-// GetMemoStats godoc
-//
-//	@Summary		Get memo stats by creator ID or username
-//	@Description	Used to generate the heatmap
-//	@Tags			memo
-//	@Produce		json
-//	@Param			creatorId		query		int		false	"Creator ID"
-//	@Param			creatorUsername	query		string	false	"Creator username"
-//	@Success		200				{object}	[]int	"Memo createdTs list"
-//	@Failure		400				{object}	nil		"Missing user id to find memo"
-//	@Failure		500				{object}	nil		"Failed to get memo display with updated ts setting value | Failed to find memo list | Failed to compose memo response"
-//	@Router			/api/v1/memo/stats [GET]
-func (s *APIV1Service) GetMemoStats(c echo.Context) error {
-	ctx := c.Request().Context()
-	normalStatus := store.Normal
-	findMemoMessage := &store.FindMemo{
-		RowStatus:      &normalStatus,
-		ExcludeContent: true,
-	}
-	if creatorID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil {
-		findMemoMessage.CreatorID = &creatorID
-	}
-
-	if username := c.QueryParam("creatorUsername"); username != "" {
-		user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
-		if user != nil {
-			findMemoMessage.CreatorID = &user.ID
-		}
-	}
-
-	if findMemoMessage.CreatorID == nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
-	}
-
-	currentUserID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		findMemoMessage.VisibilityList = []store.Visibility{store.Public}
-	} else {
-		if *findMemoMessage.CreatorID != currentUserID {
-			findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
-		} else {
-			findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected, store.Private}
-		}
-	}
-
-	list, err := s.Store.ListMemos(ctx, findMemoMessage)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
-	}
-
-	displayTsList := []int64{}
-	for _, memo := range list {
-		displayTsList = append(displayTsList, memo.CreatedTs)
-	}
-	return c.JSON(http.StatusOK, displayTsList)
-}
-
-// GetMemo godoc
-//
-//	@Summary	Get memo by ID
-//	@Tags		memo
-//	@Produce	json
-//	@Param		memoId	path		int				true	"Memo ID"
-//	@Success	200		{object}	[]store.Memo	"Memo list"
-//	@Failure	400		{object}	nil				"ID is not a number: %s"
-//	@Failure	401		{object}	nil				"Missing user in session"
-//	@Failure	403		{object}	nil				"this memo is private only | this memo is protected, missing user in session
-//	@Failure	404		{object}	nil				"Memo not found: %d"
-//	@Failure	500		{object}	nil				"Failed to find memo by ID: %v | Failed to compose memo response"
-//	@Router		/api/v1/memo/{memoId} [GET]
-func (s *APIV1Service) GetMemo(c echo.Context) error {
-	ctx := c.Request().Context()
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-
-	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
-		ID: &memoID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
-	}
-
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if memo.Visibility == store.Private {
-		if !ok || memo.CreatorID != userID {
-			return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
-		}
-	} else if memo.Visibility == store.Protected {
-		if !ok {
-			return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
-		}
-	}
-	memoResponse, err := s.convertMemoFromStore(ctx, memo)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, memoResponse)
-}
-
-// DeleteMemo godoc
-//
-//	@Summary	Delete memo by ID
-//	@Tags		memo
-//	@Produce	json
-//	@Param		memoId	path		int		true	"Memo ID to delete"
-//	@Success	200		{boolean}	true	"Memo deleted"
-//	@Failure	400		{object}	nil		"ID is not a number: %s"
-//	@Failure	401		{object}	nil		"Missing user in session | Unauthorized"
-//	@Failure	404		{object}	nil		"Memo not found: %d"
-//	@Failure	500		{object}	nil		"Failed to find memo | Failed to delete memo ID: %v"
-//	@Router		/api/v1/memo/{memoId} [DELETE]
-func (s *APIV1Service) DeleteMemo(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-
-	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
-		ID: &memoID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
-	}
-	if memo.CreatorID != userID {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
-		// Try to dispatch webhook when memo is deleted.
-		if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
-			slog.Warn("Failed to dispatch memo deleted webhook", err)
-		}
-	}
-
-	if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{
-		ID: memoID,
-	}); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
-
-// UpdateMemo godoc
-//
-//	@Summary		Update a memo
-//	@Description	Visibility can be PUBLIC, PROTECTED or PRIVATE
-//	@Description	*You should omit fields to use their default values
-//	@Tags			memo
-//	@Accept			json
-//	@Produce		json
-//	@Param			memoId	path		int					true	"ID of memo to update"
-//	@Param			body	body		PatchMemoRequest	true	"Patched object."
-//	@Success		200		{object}	store.Memo			"Stored memo"
-//	@Failure		400		{object}	nil					"ID is not a number: %s | Malformatted patch memo request | Content size overflow, up to 1MB"
-//	@Failure		401		{object}	nil					"Missing user in session | Unauthorized"
-//	@Failure		404		{object}	nil					"Memo not found: %d"
-//	@Failure		500		{object}	nil					"Failed to find memo | Failed to patch memo | Failed to upsert memo resource | Failed to delete memo resource | Failed to compose memo response"
-//	@Router			/api/v1/memo/{memoId} [PATCH]
-//
-// NOTES:
-// - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
-// - Passing 0 to createdTs and updatedTs will set them to 0 in the database, which is probably unwanted.
-func (s *APIV1Service) UpdateMemo(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-
-	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
-		ID: &memoID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
-	}
-	if memo.CreatorID != userID {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	currentTs := time.Now().Unix()
-	patchMemoRequest := &PatchMemoRequest{
-		ID:        memoID,
-		UpdatedTs: &currentTs,
-	}
-	if err := json.NewDecoder(c.Request().Body).Decode(patchMemoRequest); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
-	}
-
-	if patchMemoRequest.Content != nil && len(*patchMemoRequest.Content) > maxContentLength {
-		return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
-	}
-
-	updateMemoMessage := &store.UpdateMemo{
-		ID:        memoID,
-		CreatedTs: patchMemoRequest.CreatedTs,
-		UpdatedTs: patchMemoRequest.UpdatedTs,
-		Content:   patchMemoRequest.Content,
-	}
-	if patchMemoRequest.RowStatus != nil {
-		rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String())
-		updateMemoMessage.RowStatus = &rowStatus
-	}
-
-	err = s.Store.UpdateMemo(ctx, updateMemoMessage)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
-	}
-	memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
-	}
-
-	memoMessage, err := s.convertMemoFromStore(ctx, memo)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
-	}
-	if patchMemoRequest.ResourceIDList != nil {
-		originResourceIDList := []int32{}
-		for _, resource := range memoMessage.ResourceList {
-			originResourceIDList = append(originResourceIDList, resource.ID)
-		}
-		addedResourceIDList, removedResourceIDList := getIDListDiff(originResourceIDList, patchMemoRequest.ResourceIDList)
-		for _, resourceID := range addedResourceIDList {
-			if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
-				ID:     resourceID,
-				MemoID: &memo.ID,
-			}); err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
-			}
-		}
-		for _, resourceID := range removedResourceIDList {
-			if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
-				ID: resourceID,
-			}); err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
-			}
-		}
-	}
-
-	if patchMemoRequest.RelationList != nil {
-		patchMemoRelationList := make([]*MemoRelation, 0)
-		for _, memoRelation := range patchMemoRequest.RelationList {
-			patchMemoRelationList = append(patchMemoRelationList, &MemoRelation{
-				MemoID:        memo.ID,
-				RelatedMemoID: memoRelation.RelatedMemoID,
-				Type:          memoRelation.Type,
-			})
-		}
-		addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memoMessage.RelationList, patchMemoRelationList)
-		for _, memoRelation := range addedMemoRelationList {
-			if _, err := s.Store.UpsertMemoRelation(ctx, memoRelation); err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
-			}
-		}
-		for _, memoRelation := range removedMemoRelationList {
-			if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
-				MemoID:        &memo.ID,
-				RelatedMemoID: &memoRelation.RelatedMemoID,
-				Type:          &memoRelation.Type,
-			}); err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
-			}
-		}
-	}
-
-	memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
-	}
-
-	memoResponse, err := s.convertMemoFromStore(ctx, memo)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
-	}
-	// Try to dispatch webhook when memo is updated.
-	if err := s.DispatchMemoUpdatedWebhook(ctx, memoResponse); err != nil {
-		slog.Error("Failed to dispatch memo updated webhook", err)
-	}
-
-	return c.JSON(http.StatusOK, memoResponse)
-}
-
-func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*Memo, error) {
-	memoMessage := &Memo{
-		ID:         memo.ID,
-		Name:       memo.UID,
-		RowStatus:  RowStatus(memo.RowStatus.String()),
-		CreatorID:  memo.CreatorID,
-		CreatedTs:  memo.CreatedTs,
-		UpdatedTs:  memo.UpdatedTs,
-		Content:    memo.Content,
-		Visibility: Visibility(memo.Visibility.String()),
-		Pinned:     memo.Pinned,
-	}
-
-	// Compose creator name.
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &memoMessage.CreatorID,
-	})
-	if err != nil {
-		return nil, err
-	}
-	if user.Nickname != "" {
-		memoMessage.CreatorName = user.Nickname
-	} else {
-		memoMessage.CreatorName = user.Username
-	}
-	memoMessage.CreatorUsername = user.Username
-
-	// Compose display ts.
-	memoMessage.DisplayTs = memoMessage.CreatedTs
-
-	// Compose related resources.
-	resourceList, err := s.Store.ListResources(ctx, &store.FindResource{
-		MemoID: &memo.ID,
-	})
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to list resources")
-	}
-	memoMessage.ResourceList = []*Resource{}
-	for _, resource := range resourceList {
-		memoMessage.ResourceList = append(memoMessage.ResourceList, convertResourceFromStore(resource))
-	}
-
-	// Compose related memo relations.
-	relationList := []*MemoRelation{}
-	tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
-		MemoID: &memo.ID,
-	})
-	if err != nil {
-		return nil, err
-	}
-	for _, relation := range tempList {
-		relationList = append(relationList, convertMemoRelationFromStore(relation))
-	}
-	tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
-		RelatedMemoID: &memo.ID,
-	})
-	if err != nil {
-		return nil, err
-	}
-	for _, relation := range tempList {
-		relationList = append(relationList, convertMemoRelationFromStore(relation))
-	}
-	memoMessage.RelationList = relationList
-	return memoMessage, nil
-}
-
-func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store.Memo {
-	createdTs := time.Now().Unix()
-	if memoCreate.CreatedTs != nil {
-		createdTs = *memoCreate.CreatedTs
-	}
-	return &store.Memo{
-		UID:        shortuuid.New(),
-		CreatorID:  memoCreate.CreatorID,
-		CreatedTs:  createdTs,
-		Content:    memoCreate.Content,
-		Visibility: store.Visibility(memoCreate.Visibility),
-	}
-}
-
-func getMemoRelationListDiff(oldList, newList []*MemoRelation) (addedList, removedList []*store.MemoRelation) {
-	oldMap := map[string]bool{}
-	for _, relation := range oldList {
-		oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
-	}
-	newMap := map[string]bool{}
-	for _, relation := range newList {
-		newMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
-	}
-	for _, relation := range oldList {
-		key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
-		if !newMap[key] {
-			removedList = append(removedList, &store.MemoRelation{
-				MemoID:        relation.MemoID,
-				RelatedMemoID: relation.RelatedMemoID,
-				Type:          store.MemoRelationType(relation.Type),
-			})
-		}
-	}
-	for _, relation := range newList {
-		key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
-		if !oldMap[key] {
-			addedList = append(addedList, &store.MemoRelation{
-				MemoID:        relation.MemoID,
-				RelatedMemoID: relation.RelatedMemoID,
-				Type:          store.MemoRelationType(relation.Type),
-			})
-		}
-	}
-	return addedList, removedList
-}
-
-func getIDListDiff(oldList, newList []int32) (addedList, removedList []int32) {
-	oldMap := map[int32]bool{}
-	for _, id := range oldList {
-		oldMap[id] = true
-	}
-	newMap := map[int32]bool{}
-	for _, id := range newList {
-		newMap[id] = true
-	}
-	for id := range oldMap {
-		if !newMap[id] {
-			removedList = append(removedList, id)
-		}
-	}
-	for id := range newMap {
-		if !oldMap[id] {
-			addedList = append(addedList, id)
-		}
-	}
-	return addedList, removedList
-}
-
-// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
-func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *Memo) error {
-	return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
-}
-
-// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
-func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *Memo) error {
-	return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
-}
-
-// DispatchMemoDeletedWebhook dispatches webhook when memo is deletedd.
-func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *Memo) error {
-	return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
-}
-
-func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Memo, activityType string) error {
-	webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
-		CreatorID: &memo.CreatorID,
-	})
-	if err != nil {
-		return err
-	}
-	for _, hook := range webhooks {
-		payload := convertMemoToWebhookPayload(memo)
-		payload.ActivityType = activityType
-		payload.URL = hook.Url
-		err := webhook.Post(*payload)
-		if err != nil {
-			return errors.Wrap(err, "failed to post webhook")
-		}
-	}
-	return nil
-}
-
-func convertMemoToWebhookPayload(memo *Memo) *webhook.WebhookPayload {
-	return &webhook.WebhookPayload{
-		CreatorID: memo.CreatorID,
-		CreatedTs: time.Now().Unix(),
-		Memo: &webhook.Memo{
-			ID:         memo.ID,
-			CreatorID:  memo.CreatorID,
-			CreatedTs:  memo.CreatedTs,
-			UpdatedTs:  memo.UpdatedTs,
-			Content:    memo.Content,
-			Visibility: memo.Visibility.String(),
-			Pinned:     memo.Pinned,
-			ResourceList: func() []*webhook.Resource {
-				resources := []*webhook.Resource{}
-				for _, resource := range memo.ResourceList {
-					resources = append(resources, &webhook.Resource{
-						ID:           resource.ID,
-						CreatorID:    resource.CreatorID,
-						CreatedTs:    resource.CreatedTs,
-						UpdatedTs:    resource.UpdatedTs,
-						Filename:     resource.Filename,
-						InternalPath: resource.InternalPath,
-						ExternalLink: resource.ExternalLink,
-						Type:         resource.Type,
-						Size:         resource.Size,
-					})
-				}
-				return resources
-			}(),
-			RelationList: func() []*webhook.MemoRelation {
-				relations := []*webhook.MemoRelation{}
-				for _, relation := range memo.RelationList {
-					relations = append(relations, &webhook.MemoRelation{
-						MemoID:        relation.MemoID,
-						RelatedMemoID: relation.RelatedMemoID,
-						Type:          relation.Type.String(),
-					})
-				}
-				return relations
-			}(),
-		},
-	}
-}
diff --git a/server/route/api/v1/memo_organizer.go b/server/route/api/v1/memo_organizer.go
deleted file mode 100644
index 2a125530b..000000000
--- a/server/route/api/v1/memo_organizer.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-
-	"github.com/usememos/memos/internal/util"
-	"github.com/usememos/memos/store"
-)
-
-type MemoOrganizer struct {
-	MemoID int32 `json:"memoId"`
-	UserID int32 `json:"userId"`
-	Pinned bool  `json:"pinned"`
-}
-
-type UpsertMemoOrganizerRequest struct {
-	Pinned bool `json:"pinned"`
-}
-
-func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) {
-	g.POST("/memo/:memoId/organizer", s.CreateMemoOrganizer)
-}
-
-// CreateMemoOrganizer godoc
-//
-//	@Summary	Organize memo (pin/unpin)
-//	@Tags		memo-organizer
-//	@Accept		json
-//	@Produce	json
-//	@Param		memoId	path		int							true	"ID of memo to organize"
-//	@Param		body	body		UpsertMemoOrganizerRequest	true	"Memo organizer object"
-//	@Success	200		{object}	store.Memo					"Memo information"
-//	@Failure	400		{object}	nil							"ID is not a number: %s | Malformatted post memo organizer request"
-//	@Failure	401		{object}	nil							"Missing user in session | Unauthorized"
-//	@Failure	404		{object}	nil							"Memo not found: %v"
-//	@Failure	500		{object}	nil							"Failed to find memo | Failed to upsert memo organizer | Failed to find memo by ID: %v | Failed to compose memo response"
-//	@Router		/api/v1/memo/{memoId}/organizer [POST]
-func (s *APIV1Service) CreateMemoOrganizer(c echo.Context) error {
-	ctx := c.Request().Context()
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
-		ID: &memoID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %v", memoID))
-	}
-	if memo.CreatorID != userID {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	request := &UpsertMemoOrganizerRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
-	}
-
-	upsert := &store.MemoOrganizer{
-		MemoID: memoID,
-		UserID: userID,
-		Pinned: request.Pinned,
-	}
-	_, err = s.Store.UpsertMemoOrganizer(ctx, upsert)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
-	}
-
-	memo, err = s.Store.GetMemo(ctx, &store.FindMemo{
-		ID: &memoID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
-	}
-	if memo == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %v", memoID))
-	}
-
-	memoResponse, err := s.convertMemoFromStore(ctx, memo)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, memoResponse)
-}
diff --git a/server/route/api/v1/memo_relation.go b/server/route/api/v1/memo_relation.go
deleted file mode 100644
index cd0e818dc..000000000
--- a/server/route/api/v1/memo_relation.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-
-	"github.com/usememos/memos/internal/util"
-	"github.com/usememos/memos/store"
-)
-
-type MemoRelationType string
-
-const (
-	MemoRelationReference MemoRelationType = "REFERENCE"
-	MemoRelationComment   MemoRelationType = "COMMENT"
-)
-
-func (t MemoRelationType) String() string {
-	return string(t)
-}
-
-type MemoRelation struct {
-	MemoID        int32            `json:"memoId"`
-	RelatedMemoID int32            `json:"relatedMemoId"`
-	Type          MemoRelationType `json:"type"`
-}
-
-type UpsertMemoRelationRequest struct {
-	RelatedMemoID int32            `json:"relatedMemoId"`
-	Type          MemoRelationType `json:"type"`
-}
-
-func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
-	g.GET("/memo/:memoId/relation", s.GetMemoRelationList)
-	g.POST("/memo/:memoId/relation", s.CreateMemoRelation)
-	g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", s.DeleteMemoRelation)
-}
-
-// GetMemoRelationList godoc
-//
-//	@Summary	Get a list of Memo Relations
-//	@Tags		memo-relation
-//	@Accept		json
-//	@Produce	json
-//	@Param		memoId	path		int						true	"ID of memo to find relations"
-//	@Success	200		{object}	[]store.MemoRelation	"Memo relation information list"
-//	@Failure	400		{object}	nil						"ID is not a number: %s"
-//	@Failure	500		{object}	nil						"Failed to list memo relations"
-//	@Router		/api/v1/memo/{memoId}/relation [GET]
-func (s *APIV1Service) GetMemoRelationList(c echo.Context) error {
-	ctx := c.Request().Context()
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-
-	memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
-		MemoID: &memoID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, memoRelationList)
-}
-
-// CreateMemoRelation godoc
-//
-//	@Summary		Create Memo Relation
-//	@Description	Create a relation between two memos
-//	@Tags			memo-relation
-//	@Accept			json
-//	@Produce		json
-//	@Param			memoId	path		int							true	"ID of memo to relate"
-//	@Param			body	body		UpsertMemoRelationRequest	true	"Memo relation object"
-//	@Success		200		{object}	store.MemoRelation			"Memo relation information"
-//	@Failure		400		{object}	nil							"ID is not a number: %s | Malformatted post memo relation request"
-//	@Failure		500		{object}	nil							"Failed to upsert memo relation"
-//	@Router			/api/v1/memo/{memoId}/relation [POST]
-//
-// NOTES:
-// - Currently not secured
-// - It's possible to create relations to memos that doesn't exist, which will trigger 404 errors when the frontend tries to load them.
-// - It's possible to create multiple relations, though the interface only shows first.
-func (s *APIV1Service) CreateMemoRelation(c echo.Context) error {
-	ctx := c.Request().Context()
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-
-	request := &UpsertMemoRelationRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo relation request").SetInternal(err)
-	}
-
-	memoRelation, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
-		MemoID:        memoID,
-		RelatedMemoID: request.RelatedMemoID,
-		Type:          store.MemoRelationType(request.Type),
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, memoRelation)
-}
-
-// DeleteMemoRelation godoc
-//
-//	@Summary		Delete a Memo Relation
-//	@Description	Removes a relation between two memos
-//	@Tags			memo-relation
-//	@Accept			json
-//	@Produce		json
-//	@Param			memoId			path		int					true	"ID of memo to find relations"
-//	@Param			relatedMemoId	path		int					true	"ID of memo to remove relation to"
-//	@Param			relationType	path		MemoRelationType	true	"Type of relation to remove"
-//	@Success		200				{boolean}	true				"Memo relation deleted"
-//	@Failure		400				{object}	nil					"Memo ID is not a number: %s | Related memo ID is not a number: %s"
-//	@Failure		500				{object}	nil					"Failed to delete memo relation"
-//	@Router			/api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType} [DELETE]
-//
-// NOTES:
-// - Currently not secured.
-// - Will always return true, even if the relation doesn't exist.
-func (s *APIV1Service) DeleteMemoRelation(c echo.Context) error {
-	ctx := c.Request().Context()
-	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
-	}
-	relatedMemoID, err := util.ConvertStringToInt32(c.Param("relatedMemoId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("relatedMemoId"))).SetInternal(err)
-	}
-	relationType := store.MemoRelationType(c.Param("relationType"))
-
-	if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
-		MemoID:        &memoID,
-		RelatedMemoID: &relatedMemoID,
-		Type:          &relationType,
-	}); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
-
-func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *MemoRelation {
-	return &MemoRelation{
-		MemoID:        memoRelation.MemoID,
-		RelatedMemoID: memoRelation.RelatedMemoID,
-		Type:          MemoRelationType(memoRelation.Type),
-	}
-}
diff --git a/server/route/api/v1/resource.go b/server/route/api/v1/resource.go
deleted file mode 100644
index eb1eeb4be..000000000
--- a/server/route/api/v1/resource.go
+++ /dev/null
@@ -1,505 +0,0 @@
-package v1
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"net/url"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/labstack/echo/v4"
-	"github.com/lithammer/shortuuid/v4"
-	"github.com/pkg/errors"
-
-	"github.com/usememos/memos/internal/util"
-	"github.com/usememos/memos/plugin/storage/s3"
-	"github.com/usememos/memos/store"
-)
-
-type Resource struct {
-	ID   int32  `json:"id"`
-	Name string `json:"name"`
-	UID  string `json:"uid"`
-
-	// Standard fields
-	CreatorID int32 `json:"creatorId"`
-	CreatedTs int64 `json:"createdTs"`
-	UpdatedTs int64 `json:"updatedTs"`
-
-	// Domain specific fields
-	Filename     string `json:"filename"`
-	Blob         []byte `json:"-"`
-	InternalPath string `json:"-"`
-	ExternalLink string `json:"externalLink"`
-	Type         string `json:"type"`
-	Size         int64  `json:"size"`
-}
-
-type CreateResourceRequest struct {
-	Filename     string `json:"filename"`
-	ExternalLink string `json:"externalLink"`
-	Type         string `json:"type"`
-}
-
-type FindResourceRequest struct {
-	ID        *int32  `json:"id"`
-	CreatorID *int32  `json:"creatorId"`
-	Filename  *string `json:"filename"`
-}
-
-type UpdateResourceRequest struct {
-	Filename *string `json:"filename"`
-}
-
-const (
-	// The upload memory buffer is 32 MiB.
-	// It should be kept low, so RAM usage doesn't get out of control.
-	// This is unrelated to maximum upload size limit, which is now set through system setting.
-	maxUploadBufferSizeBytes = 32 << 20
-	MebiByte                 = 1024 * 1024
-)
-
-var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
-
-func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
-	g.GET("/resource", s.GetResourceList)
-	g.POST("/resource", s.CreateResource)
-	g.POST("/resource/blob", s.UploadResource)
-	g.PATCH("/resource/:resourceId", s.UpdateResource)
-	g.DELETE("/resource/:resourceId", s.DeleteResource)
-}
-
-// GetResourceList godoc
-//
-//	@Summary	Get a list of resources
-//	@Tags		resource
-//	@Produce	json
-//	@Param		limit	query		int					false	"Limit"
-//	@Param		offset	query		int					false	"Offset"
-//	@Success	200		{object}	[]store.Resource	"Resource list"
-//	@Failure	401		{object}	nil					"Missing user in session"
-//	@Failure	500		{object}	nil					"Failed to fetch resource list"
-//	@Router		/api/v1/resource [GET]
-func (s *APIV1Service) GetResourceList(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-	find := &store.FindResource{
-		CreatorID: &userID,
-	}
-	if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
-		find.Limit = &limit
-	}
-	if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
-		find.Offset = &offset
-	}
-
-	list, err := s.Store.ListResources(ctx, find)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
-	}
-	resourceMessageList := []*Resource{}
-	for _, resource := range list {
-		resourceMessageList = append(resourceMessageList, convertResourceFromStore(resource))
-	}
-	return c.JSON(http.StatusOK, resourceMessageList)
-}
-
-// CreateResource godoc
-//
-//	@Summary	Create resource
-//	@Tags		resource
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		CreateResourceRequest	true	"Request object."
-//	@Success	200		{object}	store.Resource			"Created resource"
-//	@Failure	400		{object}	nil						"Malformatted post resource request | Invalid external link | Invalid external link scheme | Failed to request %s | Failed to read %s | Failed to read mime from %s"
-//	@Failure	401		{object}	nil						"Missing user in session"
-//	@Failure	500		{object}	nil						"Failed to save resource | Failed to create resource | Failed to create activity"
-//	@Router		/api/v1/resource [POST]
-func (s *APIV1Service) CreateResource(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	request := &CreateResourceRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
-	}
-
-	create := &store.Resource{
-		UID:          shortuuid.New(),
-		CreatorID:    userID,
-		Filename:     request.Filename,
-		ExternalLink: request.ExternalLink,
-		Type:         request.Type,
-	}
-	if request.ExternalLink != "" {
-		// Only allow those external links scheme with http/https
-		linkURL, err := url.Parse(request.ExternalLink)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err)
-		}
-		if linkURL.Scheme != "http" && linkURL.Scheme != "https" {
-			return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme")
-		}
-	}
-
-	resource, err := s.Store.CreateResource(ctx, create)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, convertResourceFromStore(resource))
-}
-
-// UploadResource godoc
-//
-//	@Summary	Upload resource
-//	@Tags		resource
-//	@Accept		multipart/form-data
-//	@Produce	json
-//	@Param		file	formData	file			true	"File to upload"
-//	@Success	200		{object}	store.Resource	"Created resource"
-//	@Failure	400		{object}	nil				"Upload file not found | File size exceeds allowed limit of %d MiB | Failed to parse upload data"
-//	@Failure	401		{object}	nil				"Missing user in session"
-//	@Failure	500		{object}	nil				"Failed to get uploading file | Failed to open file | Failed to save resource | Failed to create resource | Failed to create activity"
-//	@Router		/api/v1/resource/blob [POST]
-func (s *APIV1Service) UploadResource(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	maxUploadSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingMaxUploadSizeMiBName.String()})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get max upload size").SetInternal(err)
-	}
-	var settingMaxUploadSizeBytes int
-	if maxUploadSetting != nil {
-		if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting.Value); err == nil {
-			settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
-		} else {
-			settingMaxUploadSizeBytes = 0
-		}
-	} else {
-		// Default to 32 MiB.
-		settingMaxUploadSizeBytes = 32 * MebiByte
-	}
-
-	file, err := c.FormFile("file")
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
-	}
-	if file == nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
-	}
-
-	if file.Size > int64(settingMaxUploadSizeBytes) {
-		message := fmt.Sprintf("File size exceeds allowed limit of %d MiB", settingMaxUploadSizeBytes/MebiByte)
-		return echo.NewHTTPError(http.StatusBadRequest, message).SetInternal(err)
-	}
-	if err := c.Request().ParseMultipartForm(maxUploadBufferSizeBytes); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse upload data").SetInternal(err)
-	}
-
-	sourceFile, err := file.Open()
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to open file").SetInternal(err)
-	}
-	defer sourceFile.Close()
-
-	create := &store.Resource{
-		UID:       shortuuid.New(),
-		CreatorID: userID,
-		Filename:  file.Filename,
-		Type:      file.Header.Get("Content-Type"),
-		Size:      file.Size,
-	}
-	err = SaveResourceBlob(ctx, s.Store, create, sourceFile)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save resource").SetInternal(err)
-	}
-
-	resource, err := s.Store.CreateResource(ctx, create)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, convertResourceFromStore(resource))
-}
-
-// DeleteResource godoc
-//
-//	@Summary	Delete a resource
-//	@Tags		resource
-//	@Produce	json
-//	@Param		resourceId	path		int		true	"Resource ID"
-//	@Success	200			{boolean}	true	"Resource deleted"
-//	@Failure	400			{object}	nil		"ID is not a number: %s"
-//	@Failure	401			{object}	nil		"Missing user in session"
-//	@Failure	404			{object}	nil		"Resource not found: %d"
-//	@Failure	500			{object}	nil		"Failed to find resource | Failed to delete resource"
-//	@Router		/api/v1/resource/{resourceId} [DELETE]
-func (s *APIV1Service) DeleteResource(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
-	}
-
-	resource, err := s.Store.GetResource(ctx, &store.FindResource{
-		ID:        &resourceID,
-		CreatorID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
-	}
-	if resource == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
-	}
-
-	if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
-		ID: resourceID,
-	}); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
-
-// UpdateResource godoc
-//
-//	@Summary	Update a resource
-//	@Tags		resource
-//	@Produce	json
-//	@Param		resourceId	path		int						true	"Resource ID"
-//	@Param		patch		body		UpdateResourceRequest	true	"Patch resource request"
-//	@Success	200			{object}	store.Resource			"Updated resource"
-//	@Failure	400			{object}	nil						"ID is not a number: %s | Malformatted patch resource request"
-//	@Failure	401			{object}	nil						"Missing user in session | Unauthorized"
-//	@Failure	404			{object}	nil						"Resource not found: %d"
-//	@Failure	500			{object}	nil						"Failed to find resource | Failed to patch resource"
-//	@Router		/api/v1/resource/{resourceId} [PATCH]
-func (s *APIV1Service) UpdateResource(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
-	}
-
-	resource, err := s.Store.GetResource(ctx, &store.FindResource{
-		ID: &resourceID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
-	}
-	if resource == nil {
-		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
-	}
-	if resource.CreatorID != userID {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	request := &UpdateResourceRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
-	}
-
-	currentTs := time.Now().Unix()
-	update := &store.UpdateResource{
-		ID:        resourceID,
-		UpdatedTs: &currentTs,
-	}
-	if request.Filename != nil && *request.Filename != "" {
-		update.Filename = request.Filename
-	}
-
-	resource, err = s.Store.UpdateResource(ctx, update)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, convertResourceFromStore(resource))
-}
-
-func replacePathTemplate(path, filename string) string {
-	t := time.Now()
-	path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
-		switch s {
-		case "{filename}":
-			return filename
-		case "{timestamp}":
-			return fmt.Sprintf("%d", t.Unix())
-		case "{year}":
-			return fmt.Sprintf("%d", t.Year())
-		case "{month}":
-			return fmt.Sprintf("%02d", t.Month())
-		case "{day}":
-			return fmt.Sprintf("%02d", t.Day())
-		case "{hour}":
-			return fmt.Sprintf("%02d", t.Hour())
-		case "{minute}":
-			return fmt.Sprintf("%02d", t.Minute())
-		case "{second}":
-			return fmt.Sprintf("%02d", t.Second())
-		case "{uuid}":
-			return util.GenUUID()
-		}
-		return s
-	})
-	return path
-}
-
-func convertResourceFromStore(resource *store.Resource) *Resource {
-	return &Resource{
-		ID:           resource.ID,
-		Name:         fmt.Sprintf("resources/%d", resource.ID),
-		UID:          resource.UID,
-		CreatorID:    resource.CreatorID,
-		CreatedTs:    resource.CreatedTs,
-		UpdatedTs:    resource.UpdatedTs,
-		Filename:     resource.Filename,
-		Blob:         resource.Blob,
-		InternalPath: resource.InternalPath,
-		ExternalLink: resource.ExternalLink,
-		Type:         resource.Type,
-		Size:         resource.Size,
-	}
-}
-
-// SaveResourceBlob save the blob of resource based on the storage config
-//
-// Depend on the storage config, some fields of *store.ResourceCreate will be changed:
-// 1. *DatabaseStorage*: `create.Blob`.
-// 2. *LocalStorage*: `create.InternalPath`.
-// 3. Others( external service): `create.ExternalLink`.
-func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource, r io.Reader) error {
-	systemSettingStorageServiceID, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
-	if err != nil {
-		return errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")
-	}
-
-	storageServiceID := DefaultStorage
-	if systemSettingStorageServiceID != nil {
-		err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
-		if err != nil {
-			return errors.Wrap(err, "Failed to unmarshal storage service id")
-		}
-	}
-
-	// `DatabaseStorage` means store blob into database
-	if storageServiceID == DatabaseStorage {
-		fileBytes, err := io.ReadAll(r)
-		if err != nil {
-			return errors.Wrap(err, "Failed to read file")
-		}
-		create.Blob = fileBytes
-		return nil
-	} else if storageServiceID == LocalStorage {
-		// `LocalStorage` means save blob into local disk
-		systemSettingLocalStoragePath, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingLocalStoragePathName.String()})
-		if err != nil {
-			return errors.Wrap(err, "Failed to find SystemSettingLocalStoragePathName")
-		}
-		localStoragePath := "assets/{timestamp}_{filename}"
-		if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
-			err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
-			if err != nil {
-				return errors.Wrap(err, "Failed to unmarshal SystemSettingLocalStoragePathName")
-			}
-		}
-
-		internalPath := localStoragePath
-		if !strings.Contains(internalPath, "{filename}") {
-			internalPath = filepath.Join(internalPath, "{filename}")
-		}
-		internalPath = replacePathTemplate(internalPath, create.Filename)
-		internalPath = filepath.ToSlash(internalPath)
-		create.InternalPath = internalPath
-
-		osPath := filepath.FromSlash(internalPath)
-		if !filepath.IsAbs(osPath) {
-			osPath = filepath.Join(s.Profile.Data, osPath)
-		}
-		dir := filepath.Dir(osPath)
-		if err = os.MkdirAll(dir, os.ModePerm); err != nil {
-			return errors.Wrap(err, "Failed to create directory")
-		}
-		dst, err := os.Create(osPath)
-		if err != nil {
-			return errors.Wrap(err, "Failed to create file")
-		}
-		defer dst.Close()
-		_, err = io.Copy(dst, r)
-		if err != nil {
-			return errors.Wrap(err, "Failed to copy file")
-		}
-
-		return nil
-	}
-
-	// Others: store blob into external service, such as S3
-	storage, err := s.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
-	if err != nil {
-		return errors.Wrap(err, "Failed to find StorageServiceID")
-	}
-	if storage == nil {
-		return errors.Errorf("Storage %d not found", storageServiceID)
-	}
-	storageMessage, err := ConvertStorageFromStore(storage)
-	if err != nil {
-		return errors.Wrap(err, "Failed to ConvertStorageFromStore")
-	}
-
-	if storageMessage.Type != StorageS3 {
-		return errors.Errorf("Unsupported storage type: %s", storageMessage.Type)
-	}
-
-	s3Config := storageMessage.Config.S3Config
-	s3Client, err := s3.NewClient(ctx, &s3.Config{
-		AccessKey: s3Config.AccessKey,
-		SecretKey: s3Config.SecretKey,
-		EndPoint:  s3Config.EndPoint,
-		Region:    s3Config.Region,
-		Bucket:    s3Config.Bucket,
-		URLPrefix: s3Config.URLPrefix,
-		URLSuffix: s3Config.URLSuffix,
-		PreSign:   s3Config.PreSign,
-	})
-	if err != nil {
-		return errors.Wrap(err, "Failed to create s3 client")
-	}
-
-	filePath := s3Config.Path
-	if !strings.Contains(filePath, "{filename}") {
-		filePath = filepath.Join(filePath, "{filename}")
-	}
-	filePath = replacePathTemplate(filePath, create.Filename)
-
-	link, err := s3Client.UploadFile(ctx, filePath, create.Type, r)
-	if err != nil {
-		return errors.Wrap(err, "Failed to upload via s3 client")
-	}
-
-	create.ExternalLink = link
-	return nil
-}
diff --git a/server/route/api/v1/storage.go b/server/route/api/v1/storage.go
deleted file mode 100644
index a18cae5a5..000000000
--- a/server/route/api/v1/storage.go
+++ /dev/null
@@ -1,316 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-
-	"github.com/usememos/memos/internal/util"
-	"github.com/usememos/memos/store"
-)
-
-const (
-	// LocalStorage means the storage service is local file system.
-	LocalStorage int32 = -1
-	// DatabaseStorage means the storage service is database.
-	DatabaseStorage int32 = 0
-	// Default storage service is database.
-	DefaultStorage int32 = DatabaseStorage
-)
-
-type StorageType string
-
-const (
-	StorageS3 StorageType = "S3"
-)
-
-func (t StorageType) String() string {
-	return string(t)
-}
-
-type StorageConfig struct {
-	S3Config *StorageS3Config `json:"s3Config"`
-}
-
-type StorageS3Config struct {
-	EndPoint  string `json:"endPoint"`
-	Path      string `json:"path"`
-	Region    string `json:"region"`
-	AccessKey string `json:"accessKey"`
-	SecretKey string `json:"secretKey"`
-	Bucket    string `json:"bucket"`
-	URLPrefix string `json:"urlPrefix"`
-	URLSuffix string `json:"urlSuffix"`
-	PreSign   bool   `json:"presign"`
-}
-
-type Storage struct {
-	ID     int32          `json:"id"`
-	Name   string         `json:"name"`
-	Type   StorageType    `json:"type"`
-	Config *StorageConfig `json:"config"`
-}
-
-type CreateStorageRequest struct {
-	Name   string         `json:"name"`
-	Type   StorageType    `json:"type"`
-	Config *StorageConfig `json:"config"`
-}
-
-type UpdateStorageRequest struct {
-	Type   StorageType    `json:"type"`
-	Name   *string        `json:"name"`
-	Config *StorageConfig `json:"config"`
-}
-
-func (s *APIV1Service) registerStorageRoutes(g *echo.Group) {
-	g.GET("/storage", s.GetStorageList)
-	g.POST("/storage", s.CreateStorage)
-	g.PATCH("/storage/:storageId", s.UpdateStorage)
-	g.DELETE("/storage/:storageId", s.DeleteStorage)
-}
-
-// GetStorageList godoc
-//
-//	@Summary	Get a list of storages
-//	@Tags		storage
-//	@Produce	json
-//	@Success	200	{object}	[]store.Storage	"List of storages"
-//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
-//	@Failure	500	{object}	nil				"Failed to find user | Failed to convert storage"
-//	@Router		/api/v1/storage [GET]
-func (s *APIV1Service) GetStorageList(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	// We should only show storage list to host user.
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	list, err := s.Store.ListStorages(ctx, &store.FindStorage{})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
-	}
-
-	storageList := []*Storage{}
-	for _, storage := range list {
-		storageMessage, err := ConvertStorageFromStore(storage)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
-		}
-		storageList = append(storageList, storageMessage)
-	}
-	return c.JSON(http.StatusOK, storageList)
-}
-
-// CreateStorage godoc
-//
-//	@Summary	Create storage
-//	@Tags		storage
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		CreateStorageRequest	true	"Request object."
-//	@Success	200		{object}	store.Storage			"Created storage"
-//	@Failure	400		{object}	nil						"Malformatted post storage request"
-//	@Failure	401		{object}	nil						"Missing user in session"
-//	@Failure	500		{object}	nil						"Failed to find user | Failed to create storage | Failed to convert storage"
-//	@Router		/api/v1/storage [POST]
-func (s *APIV1Service) CreateStorage(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	create := &CreateStorageRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
-	}
-
-	configString := ""
-	if create.Type == StorageS3 && create.Config.S3Config != nil {
-		configBytes, err := json.Marshal(create.Config.S3Config)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
-		}
-		configString = string(configBytes)
-	}
-
-	storage, err := s.Store.CreateStorage(ctx, &store.Storage{
-		Name:   create.Name,
-		Type:   create.Type.String(),
-		Config: configString,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
-	}
-	storageMessage, err := ConvertStorageFromStore(storage)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, storageMessage)
-}
-
-// DeleteStorage godoc
-//
-//	@Summary	Delete a storage
-//	@Tags		storage
-//	@Produce	json
-//	@Param		storageId	path		int		true	"Storage ID"
-//	@Success	200			{boolean}	true	"Storage deleted"
-//	@Failure	400			{object}	nil		"ID is not a number: %s | Storage service %d is using"
-//	@Failure	401			{object}	nil		"Missing user in session | Unauthorized"
-//	@Failure	500			{object}	nil		"Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
-//	@Router		/api/v1/storage/{storageId} [DELETE]
-//
-// NOTES:
-// - error message "Storage service %d is using" probably should be "Storage service %d is in use".
-func (s *APIV1Service) DeleteStorage(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
-	}
-
-	systemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
-	}
-	if systemSetting != nil {
-		storageServiceID := DefaultStorage
-		err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
-		}
-		if storageServiceID == storageID {
-			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
-		}
-	}
-
-	if err = s.Store.DeleteStorage(ctx, &store.DeleteStorage{ID: storageID}); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
-
-// UpdateStorage godoc
-//
-//	@Summary	Update a storage
-//	@Tags		storage
-//	@Produce	json
-//	@Param		storageId	path		int						true	"Storage ID"
-//	@Param		patch		body		UpdateStorageRequest	true	"Patch request"
-//	@Success	200			{object}	store.Storage			"Updated resource"
-//	@Failure	400			{object}	nil						"ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
-//	@Failure	401			{object}	nil						"Missing user in session | Unauthorized"
-//	@Failure	500			{object}	nil						"Failed to find user | Failed to patch storage | Failed to convert storage"
-//	@Router		/api/v1/storage/{storageId} [PATCH]
-func (s *APIV1Service) UpdateStorage(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
-	}
-
-	update := &UpdateStorageRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(update); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
-	}
-	storageUpdate := &store.UpdateStorage{
-		ID: storageID,
-	}
-	if update.Name != nil {
-		storageUpdate.Name = update.Name
-	}
-	if update.Config != nil {
-		if update.Type == StorageS3 {
-			configBytes, err := json.Marshal(update.Config.S3Config)
-			if err != nil {
-				return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
-			}
-			configString := string(configBytes)
-			storageUpdate.Config = &configString
-		}
-	}
-
-	storage, err := s.Store.UpdateStorage(ctx, storageUpdate)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
-	}
-	storageMessage, err := ConvertStorageFromStore(storage)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, storageMessage)
-}
-
-func ConvertStorageFromStore(storage *store.Storage) (*Storage, error) {
-	storageMessage := &Storage{
-		ID:     storage.ID,
-		Name:   storage.Name,
-		Type:   StorageType(storage.Type),
-		Config: &StorageConfig{},
-	}
-	if storageMessage.Type == StorageS3 {
-		s3Config := &StorageS3Config{}
-		if err := json.Unmarshal([]byte(storage.Config), s3Config); err != nil {
-			return nil, err
-		}
-		storageMessage.Config = &StorageConfig{
-			S3Config: s3Config,
-		}
-	}
-	return storageMessage, nil
-}
diff --git a/server/route/api/v1/swagger.md b/server/route/api/v1/swagger.md
deleted file mode 100644
index 103cde334..000000000
--- a/server/route/api/v1/swagger.md
+++ /dev/null
@@ -1,1708 +0,0 @@
-# memos API
-A privacy-first, lightweight note-taking service.
-
-## Version: 1.0
-
-**Contact information:**  
-API Support  
-https://github.com/orgs/usememos/discussions  
-
-**License:** [MIT License](https://github.com/usememos/memos/blob/main/LICENSE)
-
-[Find out more about Memos.](https://usememos.com/)
-
----
-### /api/v1/auth/signin
-
-#### POST
-##### Summary
-
-Sign-in to memos.
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Sign-in object | Yes | [github_com_usememos_memos_api_v1.SignIn](#github_com_usememos_memos_api_v1signin) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | User information | [store.User](#storeuser) |
-| 400 | Malformatted signin request |  |
-| 401 | Password login is deactivated \| Incorrect login credentials, please try again |  |
-| 403 | User has been archived with username %s |  |
-| 500 | Failed to find system setting \| Failed to unmarshal system setting \| Incorrect login credentials, please try again \| Failed to generate tokens \| Failed to create activity |  |
-
-### /api/v1/auth/signin/sso
-
-#### POST
-##### Summary
-
-Sign-in to memos using SSO.
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | SSO sign-in object | Yes | [github_com_usememos_memos_api_v1.SSOSignIn](#github_com_usememos_memos_api_v1ssosignin) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | User information | [store.User](#storeuser) |
-| 400 | Malformatted signin request |  |
-| 401 | Access denied, identifier does not match the filter. |  |
-| 403 | User has been archived with username {username} |  |
-| 404 | Identity provider not found |  |
-| 500 | Failed to find identity provider \| Failed to create identity provider instance \| Failed to exchange token \| Failed to get user info \| Failed to compile identifier filter \| Incorrect login credentials, please try again \| Failed to generate random password \| Failed to generate password hash \| Failed to create user \| Failed to generate tokens \| Failed to create activity |  |
-
-### /api/v1/auth/signout
-
-#### POST
-##### Summary
-
-Sign-out from memos.
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Sign-out success | boolean |
-
-### /api/v1/auth/signup
-
-#### POST
-##### Summary
-
-Sign-up to memos.
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Sign-up object | Yes | [github_com_usememos_memos_api_v1.SignUp](#github_com_usememos_memos_api_v1signup) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | User information | [store.User](#storeuser) |
-| 400 | Malformatted signup request \| Failed to find users |  |
-| 401 | signup is disabled |  |
-| 403 | Forbidden |  |
-| 404 | Not found |  |
-| 500 | Failed to find system setting \| Failed to unmarshal system setting allow signup \| Failed to generate password hash \| Failed to create user \| Failed to generate tokens \| Failed to create activity |  |
-
----
-### /api/v1/idp
-
-#### GET
-##### Summary
-
-Get a list of identity providers
-
-##### Description
-
-*clientSecret is only available for host user
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | List of available identity providers | [ [api_v1.IdentityProvider](#api_v1identityprovider) ] |
-| 500 | Failed to find identity provider list \| Failed to find user |  |
-
-#### POST
-##### Summary
-
-Create Identity Provider
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Identity provider information | Yes | [api_v1.CreateIdentityProviderRequest](#api_v1createidentityproviderrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Identity provider information | [store.IdentityProvider](#storeidentityprovider) |
-| 400 | Malformatted post identity provider request |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to create identity provider |  |
-
-### /api/v1/idp/{idpId}
-
-#### DELETE
-##### Summary
-
-Delete an identity provider by ID
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| idpId | path | Identity Provider ID | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Identity Provider deleted | boolean |
-| 400 | ID is not a number: %s \| Malformatted patch identity provider request |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to patch identity provider |  |
-
-#### GET
-##### Summary
-
-Get an identity provider by ID
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| idpId | path | Identity provider ID | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Requested identity provider | [store.IdentityProvider](#storeidentityprovider) |
-| 400 | ID is not a number: %s |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 404 | Identity provider not found |  |
-| 500 | Failed to find identity provider list \| Failed to find user |  |
-
-#### PATCH
-##### Summary
-
-Update an identity provider by ID
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| idpId | path | Identity Provider ID | Yes | integer |
-| body | body | Patched identity provider information | Yes | [api_v1.UpdateIdentityProviderRequest](#api_v1updateidentityproviderrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Patched identity provider | [store.IdentityProvider](#storeidentityprovider) |
-| 400 | ID is not a number: %s \| Malformatted patch identity provider request |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to patch identity provider |  |
-
----
-### /api/v1/memo
-
-#### GET
-##### Summary
-
-Get a list of memos matching optional filters
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| creatorId | query | Creator ID | No | integer |
-| creatorUsername | query | Creator username | No | string |
-| rowStatus | query | Row status | No | string |
-| pinned | query | Pinned | No | boolean |
-| tag | query | Search for tag. Do not append # | No | string |
-| content | query | Search for content | No | string |
-| limit | query | Limit | No | integer |
-| offset | query | Offset | No | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo list | [ [store.Memo](#storememo) ] |
-| 400 | Missing user to find memo |  |
-| 500 | Failed to get memo display with updated ts setting value \| Failed to fetch memo list \| Failed to compose memo response |  |
-
-#### POST
-##### Summary
-
-Create a memo
-
-##### Description
-
-Visibility can be PUBLIC, PROTECTED or PRIVATE
-*You should omit fields to use their default values
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.CreateMemoRequest](#github_com_usememos_memos_api_v1creatememorequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Stored memo | [store.Memo](#storememo) |
-| 400 | Malformatted post memo request \| Content size overflow, up to 1MB |  |
-| 401 | Missing user in session |  |
-| 404 | User not found \| Memo not found: %d |  |
-| 500 | Failed to find user setting \| Failed to unmarshal user setting value \| Failed to find system setting \| Failed to unmarshal system setting \| Failed to find user \| Failed to create memo \| Failed to create activity \| Failed to upsert memo resource \| Failed to upsert memo relation \| Failed to compose memo \| Failed to compose memo response |  |
-
-### /api/v1/memo/{memoId}
-
-#### DELETE
-##### Summary
-
-Delete memo by ID
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | Memo ID to delete | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo deleted | boolean |
-| 400 | ID is not a number: %s |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 404 | Memo not found: %d |  |
-| 500 | Failed to find memo \| Failed to delete memo ID: %v |  |
-
-#### GET
-##### Summary
-
-Get memo by ID
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | Memo ID | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo list | [ [store.Memo](#storememo) ] |
-| 400 | ID is not a number: %s |  |
-| 401 | Missing user in session |  |
-| 403 | this memo is private only \| this memo is protected, missing user in session |  |
-| 404 | Memo not found: %d |  |
-| 500 | Failed to find memo by ID: %v \| Failed to compose memo response |  |
-
-#### PATCH
-##### Summary
-
-Update a memo
-
-##### Description
-
-Visibility can be PUBLIC, PROTECTED or PRIVATE
-*You should omit fields to use their default values
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | ID of memo to update | Yes | integer |
-| body | body | Patched object. | Yes | [github_com_usememos_memos_api_v1.PatchMemoRequest](#github_com_usememos_memos_api_v1patchmemorequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Stored memo | [store.Memo](#storememo) |
-| 400 | ID is not a number: %s \| Malformatted patch memo request \| Content size overflow, up to 1MB |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 404 | Memo not found: %d |  |
-| 500 | Failed to find memo \| Failed to patch memo \| Failed to upsert memo resource \| Failed to delete memo resource \| Failed to compose memo response |  |
-
-### /api/v1/memo/all
-
-#### GET
-##### Summary
-
-Get a list of public memos matching optional filters
-
-##### Description
-
-This should also list protected memos if the user is logged in
-Authentication is optional
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| limit | query | Limit | No | integer |
-| offset | query | Offset | No | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo list | [ [store.Memo](#storememo) ] |
-| 500 | Failed to get memo display with updated ts setting value \| Failed to fetch all memo list \| Failed to compose memo response |  |
-
-### /api/v1/memo/stats
-
-#### GET
-##### Summary
-
-Get memo stats by creator ID or username
-
-##### Description
-
-Used to generate the heatmap
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| creatorId | query | Creator ID | No | integer |
-| creatorUsername | query | Creator username | No | string |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo createdTs list | [ integer ] |
-| 400 | Missing user id to find memo |  |
-| 500 | Failed to get memo display with updated ts setting value \| Failed to find memo list \| Failed to compose memo response |  |
-
----
-### /api/v1/memo/{memoId}/organizer
-
-#### POST
-##### Summary
-
-Organize memo (pin/unpin)
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | ID of memo to organize | Yes | integer |
-| body | body | Memo organizer object | Yes | [github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest](#github_com_usememos_memos_api_v1upsertmemoorganizerrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo information | [store.Memo](#storememo) |
-| 400 | ID is not a number: %s \| Malformatted post memo organizer request |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 404 | Memo not found: %v |  |
-| 500 | Failed to find memo \| Failed to upsert memo organizer \| Failed to find memo by ID: %v \| Failed to compose memo response |  |
-
----
-### /api/v1/memo/{memoId}/relation
-
-#### GET
-##### Summary
-
-Get a list of Memo Relations
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | ID of memo to find relations | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo relation information list | [ [store.MemoRelation](#storememorelation) ] |
-| 400 | ID is not a number: %s |  |
-| 500 | Failed to list memo relations |  |
-
-#### POST
-##### Summary
-
-Create Memo Relation
-
-##### Description
-
-Create a relation between two memos
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | ID of memo to relate | Yes | integer |
-| body | body | Memo relation object | Yes | [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo relation information | [store.MemoRelation](#storememorelation) |
-| 400 | ID is not a number: %s \| Malformatted post memo relation request |  |
-| 500 | Failed to upsert memo relation |  |
-
-### /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType}
-
-#### DELETE
-##### Summary
-
-Delete a Memo Relation
-
-##### Description
-
-Removes a relation between two memos
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| memoId | path | ID of memo to find relations | Yes | integer |
-| relatedMemoId | path | ID of memo to remove relation to | Yes | integer |
-| relationType | path | Type of relation to remove | Yes | string |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Memo relation deleted | boolean |
-| 400 | Memo ID is not a number: %s \| Related memo ID is not a number: %s |  |
-| 500 | Failed to delete memo relation |  |
-
----
-### /api/v1/ping
-
-#### GET
-##### Summary
-
-Ping the system
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | If succeed to ping the system | boolean |
-
-### /api/v1/status
-
-#### GET
-##### Summary
-
-Get system GetSystemStatus
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | System GetSystemStatus | [api_v1.SystemStatus](#api_v1systemstatus) |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find host user \| Failed to find system setting list \| Failed to unmarshal system setting customized profile value |  |
-
-### /api/v1/system/vacuum
-
-#### POST
-##### Summary
-
-Vacuum the database
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Database vacuumed | boolean |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to ExecVacuum database |  |
-
----
-### /api/v1/resource
-
-#### GET
-##### Summary
-
-Get a list of resources
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| limit | query | Limit | No | integer |
-| offset | query | Offset | No | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Resource list | [ [store.Resource](#storeresource) ] |
-| 401 | Missing user in session |  |
-| 500 | Failed to fetch resource list |  |
-
-#### POST
-##### Summary
-
-Create resource
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object. | Yes | [api_v1.CreateResourceRequest](#api_v1createresourcerequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Created resource | [store.Resource](#storeresource) |
-| 400 | Malformatted post resource request \| Invalid external link \| Invalid external link scheme \| Failed to request %s \| Failed to read %s \| Failed to read mime from %s |  |
-| 401 | Missing user in session |  |
-| 500 | Failed to save resource \| Failed to create resource \| Failed to create activity |  |
-
-### /api/v1/resource/{resourceId}
-
-#### DELETE
-##### Summary
-
-Delete a resource
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| resourceId | path | Resource ID | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Resource deleted | boolean |
-| 400 | ID is not a number: %s |  |
-| 401 | Missing user in session |  |
-| 404 | Resource not found: %d |  |
-| 500 | Failed to find resource \| Failed to delete resource |  |
-
-#### PATCH
-##### Summary
-
-Update a resource
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| resourceId | path | Resource ID | Yes | integer |
-| patch | body | Patch resource request | Yes | [api_v1.UpdateResourceRequest](#api_v1updateresourcerequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Updated resource | [store.Resource](#storeresource) |
-| 400 | ID is not a number: %s \| Malformatted patch resource request |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 404 | Resource not found: %d |  |
-| 500 | Failed to find resource \| Failed to patch resource |  |
-
-### /api/v1/resource/blob
-
-#### POST
-##### Summary
-
-Upload resource
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| file | formData | File to upload | Yes | file |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Created resource | [store.Resource](#storeresource) |
-| 400 | Upload file not found \| File size exceeds allowed limit of %d MiB \| Failed to parse upload data |  |
-| 401 | Missing user in session |  |
-| 500 | Failed to get uploading file \| Failed to open file \| Failed to save resource \| Failed to create resource \| Failed to create activity |  |
-
----
-### /api/v1/storage
-
-#### GET
-##### Summary
-
-Get a list of storages
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | List of storages | [ [store.Storage](#storestorage) ] |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to convert storage |  |
-
-#### POST
-##### Summary
-
-Create storage
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.CreateStorageRequest](#github_com_usememos_memos_api_v1createstoragerequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Created storage | [store.Storage](#storestorage) |
-| 400 | Malformatted post storage request |  |
-| 401 | Missing user in session |  |
-| 500 | Failed to find user \| Failed to create storage \| Failed to convert storage |  |
-
-### /api/v1/storage/{storageId}
-
-#### DELETE
-##### Summary
-
-Delete a storage
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| storageId | path | Storage ID | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Storage deleted | boolean |
-| 400 | ID is not a number: %s \| Storage service %d is using |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to find storage \| Failed to unmarshal storage service id \| Failed to delete storage |  |
-
-#### PATCH
-##### Summary
-
-Update a storage
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| storageId | path | Storage ID | Yes | integer |
-| patch | body | Patch request | Yes | [github_com_usememos_memos_api_v1.UpdateStorageRequest](#github_com_usememos_memos_api_v1updatestoragerequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Updated resource | [store.Storage](#storestorage) |
-| 400 | ID is not a number: %s \| Malformatted patch storage request \| Malformatted post storage request |  |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to patch storage \| Failed to convert storage |  |
-
----
-### /api/v1/system/setting
-
-#### GET
-##### Summary
-
-Get a list of system settings
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | System setting list | [ [api_v1.SystemSetting](#api_v1systemsetting) ] |
-| 401 | Missing user in session \| Unauthorized |  |
-| 500 | Failed to find user \| Failed to find system setting list |  |
-
-#### POST
-##### Summary
-
-Create system setting
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object. | Yes | [api_v1.UpsertSystemSettingRequest](#api_v1upsertsystemsettingrequest) |
-
-##### Responses
-
-| Code | Description |
-| ---- | ----------- |
-| 400 | Malformatted post system setting request \| invalid system setting |
-| 401 | Missing user in session \| Unauthorized |
-| 403 | Cannot disable passwords if no SSO identity provider is configured. |
-| 500 | Failed to find user \| Failed to upsert system setting |
-
----
-### /api/v1/tag
-
-#### GET
-##### Summary
-
-Get a list of tags
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Tag list | [ string ] |
-| 400 | Missing user id to find tag |  |
-| 500 | Failed to find tag list |  |
-
-#### POST
-##### Summary
-
-Create a tag
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.UpsertTagRequest](#github_com_usememos_memos_api_v1upserttagrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Created tag name | string |
-| 400 | Malformatted post tag request \| Tag name shouldn't be empty |  |
-| 401 | Missing user in session |  |
-| 500 | Failed to upsert tag \| Failed to create activity |  |
-
-### /api/v1/tag/delete
-
-#### POST
-##### Summary
-
-Delete a tag
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.DeleteTagRequest](#github_com_usememos_memos_api_v1deletetagrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Tag deleted | boolean |
-| 400 | Malformatted post tag request \| Tag name shouldn't be empty |  |
-| 401 | Missing user in session |  |
-| 500 | Failed to delete tag name: %v |  |
-
-### /api/v1/tag/suggestion
-
-#### GET
-##### Summary
-
-Get a list of tags suggested from other memos contents
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Tag list | [ string ] |
-| 400 | Missing user session |  |
-| 500 | Failed to find memo list \| Failed to find tag list |  |
-
----
-### /api/v1/user
-
-#### GET
-##### Summary
-
-Get a list of users
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | User list | [ [store.User](#storeuser) ] |
-| 500 | Failed to fetch user list |  |
-
-#### POST
-##### Summary
-
-Create a user
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| body | body | Request object | Yes | [api_v1.CreateUserRequest](#api_v1createuserrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Created user | [store.User](#storeuser) |
-| 400 | Malformatted post user request \| Invalid user create format |  |
-| 401 | Missing auth session \| Unauthorized to create user |  |
-| 403 | Could not create host user |  |
-| 500 | Failed to find user by id \| Failed to generate password hash \| Failed to create user \| Failed to create activity |  |
-
-### /api/v1/user/{id}
-
-#### DELETE
-##### Summary
-
-Delete a user
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| id | path | User ID | Yes | string |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | User deleted | boolean |
-| 400 | ID is not a number: %s \| Current session user not found with ID: %d |  |
-| 401 | Missing user in session |  |
-| 403 | Unauthorized to delete user |  |
-| 500 | Failed to find user \| Failed to delete user |  |
-
-#### GET
-##### Summary
-
-Get user by id
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| id | path | User ID | Yes | integer |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Requested user | [store.User](#storeuser) |
-| 400 | Malformatted user id |  |
-| 404 | User not found |  |
-| 500 | Failed to find user |  |
-
-#### PATCH
-##### Summary
-
-Update a user
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| id | path | User ID | Yes | string |
-| patch | body | Patch request | Yes | [api_v1.UpdateUserRequest](#api_v1updateuserrequest) |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Updated user | [store.User](#storeuser) |
-| 400 | ID is not a number: %s \| Current session user not found with ID: %d \| Malformatted patch user request \| Invalid update user request |  |
-| 401 | Missing user in session |  |
-| 403 | Unauthorized to update user |  |
-| 500 | Failed to find user \| Failed to generate password hash \| Failed to patch user \| Failed to find userSettingList |  |
-
-### /api/v1/user/me
-
-#### GET
-##### Summary
-
-Get current user
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Current user | [store.User](#storeuser) |
-| 401 | Missing auth session |  |
-| 500 | Failed to find user \| Failed to find userSettingList |  |
-
-### /api/v1/user/name/{username}
-
-#### GET
-##### Summary
-
-Get user by username
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| username | path | Username | Yes | string |
-
-##### Responses
-
-| Code | Description | Schema |
-| ---- | ----------- | ------ |
-| 200 | Requested user | [store.User](#storeuser) |
-| 404 | User not found |  |
-| 500 | Failed to find user |  |
-
----
-### /o/get/GetImage
-
-#### GET
-##### Summary
-
-Get GetImage from URL
-
-##### Parameters
-
-| Name | Located in | Description | Required | Schema |
-| ---- | ---------- | ----------- | -------- | ------ |
-| url | query | Image url | Yes | string |
-
-##### Responses
-
-| Code | Description |
-| ---- | ----------- |
-| 200 | Image |
-| 400 | Missing GetImage url \| Wrong url \| Failed to get GetImage url: %s |
-| 500 | Failed to write GetImage blob |
-
----
-### Models
-
-#### api_v1.CreateIdentityProviderRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) |  | No |
-
-#### api_v1.CreateMemoRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| content | string |  | No |
-| createdTs | integer |  | No |
-| relationList | [ [api_v1.UpsertMemoRelationRequest](#api_v1upsertmemorelationrequest) ] |  | No |
-| resourceIdList | [ integer ] | Related fields | No |
-| visibility | [api_v1.Visibility](#api_v1visibility) | Domain specific fields | No |
-
-#### api_v1.CreateResourceRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| externalLink | string |  | No |
-| filename | string |  | No |
-| type | string |  | No |
-
-#### api_v1.CreateStorageRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [api_v1.StorageConfig](#api_v1storageconfig) |  | No |
-| name | string |  | No |
-| type | [api_v1.StorageType](#api_v1storagetype) |  | No |
-
-#### api_v1.CreateUserRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| email | string |  | No |
-| nickname | string |  | No |
-| password | string |  | No |
-| role | [api_v1.Role](#api_v1role) |  | No |
-| username | string |  | No |
-
-#### api_v1.CustomizedProfile
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| appearance | string | Appearance is the server default appearance. | No |
-| description | string | Description is the server description. | No |
-| locale | string | Locale is the server default locale. | No |
-| logoUrl | string | LogoURL is the url of logo image. | No |
-| name | string | Name is the server name, default is `memos` | No |
-
-#### api_v1.DeleteTagRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| name | string |  | No |
-
-#### api_v1.FieldMapping
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| displayName | string |  | No |
-| email | string |  | No |
-| identifier | string |  | No |
-
-#### api_v1.IdentityProvider
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) |  | No |
-| id | integer |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) |  | No |
-
-#### api_v1.IdentityProviderConfig
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| oauth2Config | [api_v1.IdentityProviderOAuth2Config](#api_v1identityprovideroauth2config) |  | No |
-
-#### api_v1.IdentityProviderOAuth2Config
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| authUrl | string |  | No |
-| clientId | string |  | No |
-| clientSecret | string |  | No |
-| fieldMapping | [api_v1.FieldMapping](#api_v1fieldmapping) |  | No |
-| scopes | [ string ] |  | No |
-| tokenUrl | string |  | No |
-| userInfoUrl | string |  | No |
-
-#### api_v1.IdentityProviderType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.IdentityProviderType | string |  |  |
-
-#### api_v1.MemoRelationType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.MemoRelationType | string |  |  |
-
-#### api_v1.PatchMemoRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| content | string | Domain specific fields | No |
-| createdTs | integer | Standard fields | No |
-| relationList | [ [api_v1.UpsertMemoRelationRequest](#api_v1upsertmemorelationrequest) ] |  | No |
-| resourceIdList | [ integer ] | Related fields | No |
-| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) |  | No |
-| updatedTs | integer |  | No |
-| visibility | [api_v1.Visibility](#api_v1visibility) |  | No |
-
-#### api_v1.Role
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.Role | string |  |  |
-
-#### api_v1.RowStatus
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.RowStatus | string |  |  |
-
-#### api_v1.SSOSignIn
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| code | string |  | No |
-| identityProviderId | integer |  | No |
-| redirectUri | string |  | No |
-
-#### api_v1.SignIn
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| password | string |  | No |
-| remember | boolean |  | No |
-| username | string |  | No |
-
-#### api_v1.SignUp
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| password | string |  | No |
-| username | string |  | No |
-
-#### api_v1.StorageConfig
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| s3Config | [api_v1.StorageS3Config](#api_v1storages3config) |  | No |
-
-#### api_v1.StorageS3Config
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| accessKey | string |  | No |
-| bucket | string |  | No |
-| endPoint | string |  | No |
-| path | string |  | No |
-| presign | boolean |  | No |
-| region | string |  | No |
-| secretKey | string |  | No |
-| urlPrefix | string |  | No |
-| urlSuffix | string |  | No |
-
-#### api_v1.StorageType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.StorageType | string |  |  |
-
-#### api_v1.SystemSetting
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| description | string |  | No |
-| name | [api_v1.SystemSettingName](#api_v1systemsettingname) |  | No |
-| value | string | Value is a JSON string with basic value. | No |
-
-#### api_v1.SystemSettingName
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.SystemSettingName | string |  |  |
-
-#### api_v1.SystemStatus
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| additionalScript | string | Additional script. | No |
-| additionalStyle | string | Additional style. | No |
-| allowSignUp | boolean | System settings Allow sign up. | No |
-| customizedProfile | [api_v1.CustomizedProfile](#api_v1customizedprofile) | Customized server profile, including server name and external url. | No |
-| dbSize | integer |  | No |
-| disablePasswordLogin | boolean | Disable password login. | No |
-| disablePublicMemos | boolean | Disable public memos. | No |
-| host | [api_v1.User](#api_v1user) |  | No |
-| localStoragePath | string | Local storage path. | No |
-| maxUploadSizeMiB | integer | Max upload size. | No |
-| memoDisplayWithUpdatedTs | boolean | Memo display with updated timestamp. | No |
-| profile | [profile.Profile](#profileprofile) |  | No |
-| storageServiceId | integer | Storage service ID. | No |
-
-#### api_v1.UpdateIdentityProviderRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) |  | No |
-
-#### api_v1.UpdateResourceRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| filename | string |  | No |
-
-#### api_v1.UpdateStorageRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [api_v1.StorageConfig](#api_v1storageconfig) |  | No |
-| name | string |  | No |
-| type | [api_v1.StorageType](#api_v1storagetype) |  | No |
-
-#### api_v1.UpdateUserRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| avatarUrl | string |  | No |
-| email | string |  | No |
-| nickname | string |  | No |
-| password | string |  | No |
-| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) |  | No |
-| username | string |  | No |
-
-#### api_v1.UpsertMemoOrganizerRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| pinned | boolean |  | No |
-
-#### api_v1.UpsertMemoRelationRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| relatedMemoId | integer |  | No |
-| type | [api_v1.MemoRelationType](#api_v1memorelationtype) |  | No |
-
-#### api_v1.UpsertSystemSettingRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| description | string |  | No |
-| name | [api_v1.SystemSettingName](#api_v1systemsettingname) |  | No |
-| value | string |  | No |
-
-#### api_v1.UpsertTagRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| name | string |  | No |
-
-#### api_v1.User
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| avatarUrl | string |  | No |
-| createdTs | integer |  | No |
-| email | string |  | No |
-| id | integer |  | No |
-| nickname | string |  | No |
-| role | [api_v1.Role](#api_v1role) |  | No |
-| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) | Standard fields | No |
-| updatedTs | integer |  | No |
-| username | string | Domain specific fields | No |
-
-#### api_v1.Visibility
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| api_v1.Visibility | string |  |  |
-
-#### github_com_usememos_memos_api_v1.CreateIdentityProviderRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) |  | No |
-
-#### github_com_usememos_memos_api_v1.CreateMemoRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| content | string |  | No |
-| createdTs | integer |  | No |
-| relationList | [ [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) ] |  | No |
-| resourceIdList | [ integer ] | Related fields | No |
-| visibility | [github_com_usememos_memos_api_v1.Visibility](#github_com_usememos_memos_api_v1visibility) | Domain specific fields | No |
-
-#### github_com_usememos_memos_api_v1.CreateResourceRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| externalLink | string |  | No |
-| filename | string |  | No |
-| type | string |  | No |
-
-#### github_com_usememos_memos_api_v1.CreateStorageRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [github_com_usememos_memos_api_v1.StorageConfig](#github_com_usememos_memos_api_v1storageconfig) |  | No |
-| name | string |  | No |
-| type | [github_com_usememos_memos_api_v1.StorageType](#github_com_usememos_memos_api_v1storagetype) |  | No |
-
-#### github_com_usememos_memos_api_v1.CreateUserRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| email | string |  | No |
-| nickname | string |  | No |
-| password | string |  | No |
-| role | [github_com_usememos_memos_api_v1.Role](#github_com_usememos_memos_api_v1role) |  | No |
-| username | string |  | No |
-
-#### github_com_usememos_memos_api_v1.CustomizedProfile
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| appearance | string | Appearance is the server default appearance. | No |
-| description | string | Description is the server description. | No |
-| locale | string | Locale is the server default locale. | No |
-| logoUrl | string | LogoURL is the url of logo image. | No |
-| name | string | Name is the server name, default is `memos` | No |
-
-#### github_com_usememos_memos_api_v1.DeleteTagRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| name | string |  | No |
-
-#### github_com_usememos_memos_api_v1.FieldMapping
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| displayName | string |  | No |
-| email | string |  | No |
-| identifier | string |  | No |
-
-#### github_com_usememos_memos_api_v1.IdentityProvider
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) |  | No |
-| id | integer |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) |  | No |
-
-#### github_com_usememos_memos_api_v1.IdentityProviderConfig
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| oauth2Config | [github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config](#github_com_usememos_memos_api_v1identityprovideroauth2config) |  | No |
-
-#### github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| authUrl | string |  | No |
-| clientId | string |  | No |
-| clientSecret | string |  | No |
-| fieldMapping | [github_com_usememos_memos_api_v1.FieldMapping](#github_com_usememos_memos_api_v1fieldmapping) |  | No |
-| scopes | [ string ] |  | No |
-| tokenUrl | string |  | No |
-| userInfoUrl | string |  | No |
-
-#### github_com_usememos_memos_api_v1.IdentityProviderType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.IdentityProviderType | string |  |  |
-
-#### github_com_usememos_memos_api_v1.MemoRelationType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.MemoRelationType | string |  |  |
-
-#### github_com_usememos_memos_api_v1.PatchMemoRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| content | string | Domain specific fields | No |
-| createdTs | integer | Standard fields | No |
-| relationList | [ [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) ] |  | No |
-| resourceIdList | [ integer ] | Related fields | No |
-| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) |  | No |
-| updatedTs | integer |  | No |
-| visibility | [github_com_usememos_memos_api_v1.Visibility](#github_com_usememos_memos_api_v1visibility) |  | No |
-
-#### github_com_usememos_memos_api_v1.Role
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.Role | string |  |  |
-
-#### github_com_usememos_memos_api_v1.RowStatus
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.RowStatus | string |  |  |
-
-#### github_com_usememos_memos_api_v1.SSOSignIn
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| code | string |  | No |
-| identityProviderId | integer |  | No |
-| redirectUri | string |  | No |
-
-#### github_com_usememos_memos_api_v1.SignIn
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| password | string |  | No |
-| remember | boolean |  | No |
-| username | string |  | No |
-
-#### github_com_usememos_memos_api_v1.SignUp
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| password | string |  | No |
-| username | string |  | No |
-
-#### github_com_usememos_memos_api_v1.StorageConfig
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| s3Config | [github_com_usememos_memos_api_v1.StorageS3Config](#github_com_usememos_memos_api_v1storages3config) |  | No |
-
-#### github_com_usememos_memos_api_v1.StorageS3Config
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| accessKey | string |  | No |
-| bucket | string |  | No |
-| endPoint | string |  | No |
-| path | string |  | No |
-| presign | boolean |  | No |
-| region | string |  | No |
-| secretKey | string |  | No |
-| urlPrefix | string |  | No |
-| urlSuffix | string |  | No |
-
-#### github_com_usememos_memos_api_v1.StorageType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.StorageType | string |  |  |
-
-#### github_com_usememos_memos_api_v1.SystemSetting
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| description | string |  | No |
-| name | [github_com_usememos_memos_api_v1.SystemSettingName](#github_com_usememos_memos_api_v1systemsettingname) |  | No |
-| value | string | Value is a JSON string with basic value. | No |
-
-#### github_com_usememos_memos_api_v1.SystemSettingName
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.SystemSettingName | string |  |  |
-
-#### github_com_usememos_memos_api_v1.SystemStatus
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| additionalScript | string | Additional script. | No |
-| additionalStyle | string | Additional style. | No |
-| allowSignUp | boolean | System settings Allow sign up. | No |
-| customizedProfile | [github_com_usememos_memos_api_v1.CustomizedProfile](#github_com_usememos_memos_api_v1customizedprofile) | Customized server profile, including server name and external url. | No |
-| dbSize | integer |  | No |
-| disablePasswordLogin | boolean | Disable password login. | No |
-| disablePublicMemos | boolean | Disable public memos. | No |
-| host | [github_com_usememos_memos_api_v1.User](#github_com_usememos_memos_api_v1user) |  | No |
-| localStoragePath | string | Local storage path. | No |
-| maxUploadSizeMiB | integer | Max upload size. | No |
-| memoDisplayWithUpdatedTs | boolean | Memo display with updated timestamp. | No |
-| profile | [profile.Profile](#profileprofile) |  | No |
-| storageServiceId | integer | Storage service ID. | No |
-
-#### github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) |  | No |
-
-#### github_com_usememos_memos_api_v1.UpdateResourceRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| filename | string |  | No |
-
-#### github_com_usememos_memos_api_v1.UpdateStorageRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [github_com_usememos_memos_api_v1.StorageConfig](#github_com_usememos_memos_api_v1storageconfig) |  | No |
-| name | string |  | No |
-| type | [github_com_usememos_memos_api_v1.StorageType](#github_com_usememos_memos_api_v1storagetype) |  | No |
-
-#### github_com_usememos_memos_api_v1.UpdateUserRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| avatarUrl | string |  | No |
-| email | string |  | No |
-| nickname | string |  | No |
-| password | string |  | No |
-| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) |  | No |
-| username | string |  | No |
-
-#### github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| pinned | boolean |  | No |
-
-#### github_com_usememos_memos_api_v1.UpsertMemoRelationRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| relatedMemoId | integer |  | No |
-| type | [github_com_usememos_memos_api_v1.MemoRelationType](#github_com_usememos_memos_api_v1memorelationtype) |  | No |
-
-#### github_com_usememos_memos_api_v1.UpsertSystemSettingRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| description | string |  | No |
-| name | [github_com_usememos_memos_api_v1.SystemSettingName](#github_com_usememos_memos_api_v1systemsettingname) |  | No |
-| value | string |  | No |
-
-#### github_com_usememos_memos_api_v1.UpsertTagRequest
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| name | string |  | No |
-
-#### github_com_usememos_memos_api_v1.User
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| avatarUrl | string |  | No |
-| createdTs | integer |  | No |
-| email | string |  | No |
-| id | integer |  | No |
-| nickname | string |  | No |
-| role | [github_com_usememos_memos_api_v1.Role](#github_com_usememos_memos_api_v1role) |  | No |
-| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) | Standard fields | No |
-| updatedTs | integer |  | No |
-| username | string | Domain specific fields | No |
-
-#### github_com_usememos_memos_api_v1.Visibility
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| github_com_usememos_memos_api_v1.Visibility | string |  |  |
-
-#### profile.Profile
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| mode | string | Mode can be "prod" or "dev" or "demo" | No |
-| version | string | Version is the current version of server | No |
-
-#### store.FieldMapping
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| displayName | string |  | No |
-| email | string |  | No |
-| identifier | string |  | No |
-
-#### store.IdentityProvider
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | [store.IdentityProviderConfig](#storeidentityproviderconfig) |  | No |
-| id | integer |  | No |
-| identifierFilter | string |  | No |
-| name | string |  | No |
-| type | [store.IdentityProviderType](#storeidentityprovidertype) |  | No |
-
-#### store.IdentityProviderConfig
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| oauth2Config | [store.IdentityProviderOAuth2Config](#storeidentityprovideroauth2config) |  | No |
-
-#### store.IdentityProviderOAuth2Config
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| authUrl | string |  | No |
-| clientId | string |  | No |
-| clientSecret | string |  | No |
-| fieldMapping | [store.FieldMapping](#storefieldmapping) |  | No |
-| scopes | [ string ] |  | No |
-| tokenUrl | string |  | No |
-| userInfoUrl | string |  | No |
-
-#### store.IdentityProviderType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| store.IdentityProviderType | string |  |  |
-
-#### store.Memo
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| content | string | Domain specific fields | No |
-| createdTs | integer |  | No |
-| creatorID | integer |  | No |
-| id | integer |  | No |
-| parentID | integer |  | No |
-| pinned | boolean | Composed fields | No |
-| resourceName | string |  | No |
-| rowStatus | [store.RowStatus](#storerowstatus) | Standard fields | No |
-| updatedTs | integer |  | No |
-| visibility | [store.Visibility](#storevisibility) |  | No |
-
-#### store.MemoRelation
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| memoID | integer |  | No |
-| relatedMemoID | integer |  | No |
-| type | [store.MemoRelationType](#storememorelationtype) |  | No |
-
-#### store.MemoRelationType
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| store.MemoRelationType | string |  |  |
-
-#### store.Resource
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| blob | [ integer ] |  | No |
-| createdTs | integer |  | No |
-| creatorID | integer | Standard fields | No |
-| externalLink | string |  | No |
-| filename | string | Domain specific fields | No |
-| id | integer |  | No |
-| internalPath | string |  | No |
-| memoID | integer |  | No |
-| resourceName | string |  | No |
-| size | integer |  | No |
-| type | string |  | No |
-| updatedTs | integer |  | No |
-
-#### store.Role
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| store.Role | string |  |  |
-
-#### store.RowStatus
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| store.RowStatus | string |  |  |
-
-#### store.Storage
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| config | string |  | No |
-| id | integer |  | No |
-| name | string |  | No |
-| type | string |  | No |
-
-#### store.User
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| avatarURL | string |  | No |
-| createdTs | integer |  | No |
-| email | string |  | No |
-| id | integer |  | No |
-| nickname | string |  | No |
-| passwordHash | string |  | No |
-| role | [store.Role](#storerole) |  | No |
-| rowStatus | [store.RowStatus](#storerowstatus) | Standard fields | No |
-| updatedTs | integer |  | No |
-| username | string | Domain specific fields | No |
-
-#### store.Visibility
-
-| Name | Type | Description | Required |
-| ---- | ---- | ----------- | -------- |
-| store.Visibility | string |  |  |
diff --git a/server/route/api/v1/swagger.yaml b/server/route/api/v1/swagger.yaml
deleted file mode 100644
index a4161efa3..000000000
--- a/server/route/api/v1/swagger.yaml
+++ /dev/null
@@ -1,2278 +0,0 @@
-basePath: /
-definitions:
-  api_v1.CreateIdentityProviderRequest:
-    properties:
-      config:
-        $ref: '#/definitions/api_v1.IdentityProviderConfig'
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/api_v1.IdentityProviderType'
-    type: object
-  api_v1.CreateMemoRequest:
-    properties:
-      content:
-        type: string
-      createdTs:
-        type: integer
-      relationList:
-        items:
-          $ref: '#/definitions/api_v1.UpsertMemoRelationRequest'
-        type: array
-      resourceIdList:
-        description: Related fields
-        items:
-          type: integer
-        type: array
-      visibility:
-        allOf:
-        - $ref: '#/definitions/api_v1.Visibility'
-        description: Domain specific fields
-    type: object
-  api_v1.CreateResourceRequest:
-    properties:
-      externalLink:
-        type: string
-      filename:
-        type: string
-      type:
-        type: string
-    type: object
-  api_v1.CreateStorageRequest:
-    properties:
-      config:
-        $ref: '#/definitions/api_v1.StorageConfig'
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/api_v1.StorageType'
-    type: object
-  api_v1.CreateUserRequest:
-    properties:
-      email:
-        type: string
-      nickname:
-        type: string
-      password:
-        type: string
-      role:
-        $ref: '#/definitions/api_v1.Role'
-      username:
-        type: string
-    type: object
-  api_v1.CustomizedProfile:
-    properties:
-      appearance:
-        description: Appearance is the server default appearance.
-        type: string
-      description:
-        description: Description is the server description.
-        type: string
-      locale:
-        description: Locale is the server default locale.
-        type: string
-      logoUrl:
-        description: LogoURL is the url of logo image.
-        type: string
-      name:
-        description: Name is the server name, default is `memos`
-        type: string
-    type: object
-  api_v1.DeleteTagRequest:
-    properties:
-      name:
-        type: string
-    type: object
-  api_v1.FieldMapping:
-    properties:
-      displayName:
-        type: string
-      email:
-        type: string
-      identifier:
-        type: string
-    type: object
-  api_v1.IdentityProvider:
-    properties:
-      config:
-        $ref: '#/definitions/api_v1.IdentityProviderConfig'
-      id:
-        type: integer
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/api_v1.IdentityProviderType'
-    type: object
-  api_v1.IdentityProviderConfig:
-    properties:
-      oauth2Config:
-        $ref: '#/definitions/api_v1.IdentityProviderOAuth2Config'
-    type: object
-  api_v1.IdentityProviderOAuth2Config:
-    properties:
-      authUrl:
-        type: string
-      clientId:
-        type: string
-      clientSecret:
-        type: string
-      fieldMapping:
-        $ref: '#/definitions/api_v1.FieldMapping'
-      scopes:
-        items:
-          type: string
-        type: array
-      tokenUrl:
-        type: string
-      userInfoUrl:
-        type: string
-    type: object
-  api_v1.IdentityProviderType:
-    enum:
-    - OAUTH2
-    type: string
-    x-enum-varnames:
-    - IdentityProviderOAuth2Type
-  api_v1.MemoRelationType:
-    enum:
-    - REFERENCE
-    - COMMENT
-    type: string
-    x-enum-varnames:
-    - MemoRelationReference
-    - MemoRelationComment
-  api_v1.PatchMemoRequest:
-    properties:
-      content:
-        description: Domain specific fields
-        type: string
-      createdTs:
-        description: Standard fields
-        type: integer
-      relationList:
-        items:
-          $ref: '#/definitions/api_v1.UpsertMemoRelationRequest'
-        type: array
-      resourceIdList:
-        description: Related fields
-        items:
-          type: integer
-        type: array
-      rowStatus:
-        $ref: '#/definitions/api_v1.RowStatus'
-      updatedTs:
-        type: integer
-      visibility:
-        $ref: '#/definitions/api_v1.Visibility'
-    type: object
-  api_v1.Role:
-    enum:
-    - HOST
-    - ADMIN
-    - USER
-    type: string
-    x-enum-varnames:
-    - RoleHost
-    - RoleAdmin
-    - RoleUser
-  api_v1.RowStatus:
-    enum:
-    - NORMAL
-    - ARCHIVED
-    type: string
-    x-enum-varnames:
-    - Normal
-    - Archived
-  api_v1.SSOSignIn:
-    properties:
-      code:
-        type: string
-      identityProviderId:
-        type: integer
-      redirectUri:
-        type: string
-    type: object
-  api_v1.SignIn:
-    properties:
-      password:
-        type: string
-      remember:
-        type: boolean
-      username:
-        type: string
-    type: object
-  api_v1.SignUp:
-    properties:
-      password:
-        type: string
-      username:
-        type: string
-    type: object
-  api_v1.StorageConfig:
-    properties:
-      s3Config:
-        $ref: '#/definitions/api_v1.StorageS3Config'
-    type: object
-  api_v1.StorageS3Config:
-    properties:
-      accessKey:
-        type: string
-      bucket:
-        type: string
-      endPoint:
-        type: string
-      path:
-        type: string
-      presign:
-        type: boolean
-      region:
-        type: string
-      secretKey:
-        type: string
-      urlPrefix:
-        type: string
-      urlSuffix:
-        type: string
-    type: object
-  api_v1.StorageType:
-    enum:
-    - S3
-    type: string
-    x-enum-varnames:
-    - StorageS3
-  api_v1.SystemSetting:
-    properties:
-      description:
-        type: string
-      name:
-        $ref: '#/definitions/api_v1.SystemSettingName'
-      value:
-        description: Value is a JSON string with basic value.
-        type: string
-    type: object
-  api_v1.SystemSettingName:
-    enum:
-    - server-id
-    - secret-session
-    - disable-public-memos
-    - max-upload-size-mib
-    - customized-profile
-    - storage-service-id
-    - local-storage-path
-    - telegram-bot-token
-    - memo-display-with-updated-ts
-    type: string
-    x-enum-varnames:
-    - SystemSettingServerIDName
-    - SystemSettingSecretSessionName
-    - SystemSettingDisablePublicMemosName
-    - SystemSettingMaxUploadSizeMiBName
-    - SystemSettingCustomizedProfileName
-    - SystemSettingStorageServiceIDName
-    - SystemSettingLocalStoragePathName
-    - SystemSettingTelegramBotTokenName
-    - SystemSettingMemoDisplayWithUpdatedTsName
-  api_v1.SystemStatus:
-    properties:
-      additionalScript:
-        description: Additional script.
-        type: string
-      additionalStyle:
-        description: Additional style.
-        type: string
-      allowSignUp:
-        description: |-
-          System settings
-          Allow sign up.
-        type: boolean
-      customizedProfile:
-        allOf:
-        - $ref: '#/definitions/api_v1.CustomizedProfile'
-        description: Customized server profile, including server name and external
-          url.
-      dbSize:
-        type: integer
-      disablePasswordLogin:
-        description: Disable password login.
-        type: boolean
-      disablePublicMemos:
-        description: Disable public memos.
-        type: boolean
-      host:
-        $ref: '#/definitions/api_v1.User'
-      localStoragePath:
-        description: Local storage path.
-        type: string
-      maxUploadSizeMiB:
-        description: Max upload size.
-        type: integer
-      memoDisplayWithUpdatedTs:
-        description: Memo display with updated timestamp.
-        type: boolean
-      profile:
-        $ref: '#/definitions/profile.Profile'
-      storageServiceId:
-        description: Storage service ID.
-        type: integer
-    type: object
-  api_v1.UpdateIdentityProviderRequest:
-    properties:
-      config:
-        $ref: '#/definitions/api_v1.IdentityProviderConfig'
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/api_v1.IdentityProviderType'
-    type: object
-  api_v1.UpdateResourceRequest:
-    properties:
-      filename:
-        type: string
-    type: object
-  api_v1.UpdateStorageRequest:
-    properties:
-      config:
-        $ref: '#/definitions/api_v1.StorageConfig'
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/api_v1.StorageType'
-    type: object
-  api_v1.UpdateUserRequest:
-    properties:
-      avatarUrl:
-        type: string
-      email:
-        type: string
-      nickname:
-        type: string
-      password:
-        type: string
-      rowStatus:
-        $ref: '#/definitions/api_v1.RowStatus'
-      username:
-        type: string
-    type: object
-  api_v1.UpsertMemoOrganizerRequest:
-    properties:
-      pinned:
-        type: boolean
-    type: object
-  api_v1.UpsertMemoRelationRequest:
-    properties:
-      relatedMemoId:
-        type: integer
-      type:
-        $ref: '#/definitions/api_v1.MemoRelationType'
-    type: object
-  api_v1.UpsertSystemSettingRequest:
-    properties:
-      description:
-        type: string
-      name:
-        $ref: '#/definitions/api_v1.SystemSettingName'
-      value:
-        type: string
-    type: object
-  api_v1.UpsertTagRequest:
-    properties:
-      name:
-        type: string
-    type: object
-  api_v1.User:
-    properties:
-      avatarUrl:
-        type: string
-      createdTs:
-        type: integer
-      email:
-        type: string
-      id:
-        type: integer
-      nickname:
-        type: string
-      role:
-        $ref: '#/definitions/api_v1.Role'
-      rowStatus:
-        allOf:
-        - $ref: '#/definitions/api_v1.RowStatus'
-        description: Standard fields
-      updatedTs:
-        type: integer
-      username:
-        description: Domain specific fields
-        type: string
-    type: object
-  api_v1.Visibility:
-    enum:
-    - PUBLIC
-    - PROTECTED
-    - PRIVATE
-    type: string
-    x-enum-varnames:
-    - Public
-    - Protected
-    - Private
-  github_com_usememos_memos_api_v1.CreateIdentityProviderRequest:
-    properties:
-      config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig'
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType'
-    type: object
-  github_com_usememos_memos_api_v1.CreateMemoRequest:
-    properties:
-      content:
-        type: string
-      createdTs:
-        type: integer
-      relationList:
-        items:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
-        type: array
-      resourceIdList:
-        description: Related fields
-        items:
-          type: integer
-        type: array
-      visibility:
-        allOf:
-        - $ref: '#/definitions/github_com_usememos_memos_api_v1.Visibility'
-        description: Domain specific fields
-    type: object
-  github_com_usememos_memos_api_v1.CreateResourceRequest:
-    properties:
-      externalLink:
-        type: string
-      filename:
-        type: string
-      type:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.CreateStorageRequest:
-    properties:
-      config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageConfig'
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageType'
-    type: object
-  github_com_usememos_memos_api_v1.CreateUserRequest:
-    properties:
-      email:
-        type: string
-      nickname:
-        type: string
-      password:
-        type: string
-      role:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.Role'
-      username:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.CustomizedProfile:
-    properties:
-      appearance:
-        description: Appearance is the server default appearance.
-        type: string
-      description:
-        description: Description is the server description.
-        type: string
-      locale:
-        description: Locale is the server default locale.
-        type: string
-      logoUrl:
-        description: LogoURL is the url of logo image.
-        type: string
-      name:
-        description: Name is the server name, default is `memos`
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.DeleteTagRequest:
-    properties:
-      name:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.FieldMapping:
-    properties:
-      displayName:
-        type: string
-      email:
-        type: string
-      identifier:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.IdentityProvider:
-    properties:
-      config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig'
-      id:
-        type: integer
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType'
-    type: object
-  github_com_usememos_memos_api_v1.IdentityProviderConfig:
-    properties:
-      oauth2Config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config'
-    type: object
-  github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config:
-    properties:
-      authUrl:
-        type: string
-      clientId:
-        type: string
-      clientSecret:
-        type: string
-      fieldMapping:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.FieldMapping'
-      scopes:
-        items:
-          type: string
-        type: array
-      tokenUrl:
-        type: string
-      userInfoUrl:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.IdentityProviderType:
-    enum:
-    - OAUTH2
-    type: string
-    x-enum-varnames:
-    - IdentityProviderOAuth2Type
-  github_com_usememos_memos_api_v1.MemoRelationType:
-    enum:
-    - REFERENCE
-    - COMMENT
-    type: string
-    x-enum-varnames:
-    - MemoRelationReference
-    - MemoRelationComment
-  github_com_usememos_memos_api_v1.PatchMemoRequest:
-    properties:
-      content:
-        description: Domain specific fields
-        type: string
-      createdTs:
-        description: Standard fields
-        type: integer
-      relationList:
-        items:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
-        type: array
-      resourceIdList:
-        description: Related fields
-        items:
-          type: integer
-        type: array
-      rowStatus:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus'
-      updatedTs:
-        type: integer
-      visibility:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.Visibility'
-    type: object
-  github_com_usememos_memos_api_v1.Role:
-    enum:
-    - HOST
-    - ADMIN
-    - USER
-    type: string
-    x-enum-varnames:
-    - RoleHost
-    - RoleAdmin
-    - RoleUser
-  github_com_usememos_memos_api_v1.RowStatus:
-    enum:
-    - NORMAL
-    - ARCHIVED
-    type: string
-    x-enum-varnames:
-    - Normal
-    - Archived
-  github_com_usememos_memos_api_v1.SSOSignIn:
-    properties:
-      code:
-        type: string
-      identityProviderId:
-        type: integer
-      redirectUri:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.SignIn:
-    properties:
-      password:
-        type: string
-      remember:
-        type: boolean
-      username:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.SignUp:
-    properties:
-      password:
-        type: string
-      username:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.StorageConfig:
-    properties:
-      s3Config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageS3Config'
-    type: object
-  github_com_usememos_memos_api_v1.StorageS3Config:
-    properties:
-      accessKey:
-        type: string
-      bucket:
-        type: string
-      endPoint:
-        type: string
-      path:
-        type: string
-      presign:
-        type: boolean
-      region:
-        type: string
-      secretKey:
-        type: string
-      urlPrefix:
-        type: string
-      urlSuffix:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.StorageType:
-    enum:
-    - S3
-    type: string
-    x-enum-varnames:
-    - StorageS3
-  github_com_usememos_memos_api_v1.SystemSetting:
-    properties:
-      description:
-        type: string
-      name:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.SystemSettingName'
-      value:
-        description: Value is a JSON string with basic value.
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.SystemSettingName:
-    enum:
-    - server-id
-    - secret-session
-    - disable-public-memos
-    - max-upload-size-mib
-    - customized-profile
-    - storage-service-id
-    - local-storage-path
-    - telegram-bot-token
-    - memo-display-with-updated-ts
-    type: string
-    x-enum-varnames:
-    - SystemSettingServerIDName
-    - SystemSettingSecretSessionName
-    - SystemSettingDisablePublicMemosName
-    - SystemSettingMaxUploadSizeMiBName
-    - SystemSettingCustomizedProfileName
-    - SystemSettingStorageServiceIDName
-    - SystemSettingLocalStoragePathName
-    - SystemSettingTelegramBotTokenName
-    - SystemSettingMemoDisplayWithUpdatedTsName
-  github_com_usememos_memos_api_v1.SystemStatus:
-    properties:
-      additionalScript:
-        description: Additional script.
-        type: string
-      additionalStyle:
-        description: Additional style.
-        type: string
-      allowSignUp:
-        description: |-
-          System settings
-          Allow sign up.
-        type: boolean
-      customizedProfile:
-        allOf:
-        - $ref: '#/definitions/github_com_usememos_memos_api_v1.CustomizedProfile'
-        description: Customized server profile, including server name and external
-          url.
-      dbSize:
-        type: integer
-      disablePasswordLogin:
-        description: Disable password login.
-        type: boolean
-      disablePublicMemos:
-        description: Disable public memos.
-        type: boolean
-      host:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.User'
-      localStoragePath:
-        description: Local storage path.
-        type: string
-      maxUploadSizeMiB:
-        description: Max upload size.
-        type: integer
-      memoDisplayWithUpdatedTs:
-        description: Memo display with updated timestamp.
-        type: boolean
-      profile:
-        $ref: '#/definitions/profile.Profile'
-      storageServiceId:
-        description: Storage service ID.
-        type: integer
-    type: object
-  github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest:
-    properties:
-      config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig'
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType'
-    type: object
-  github_com_usememos_memos_api_v1.UpdateResourceRequest:
-    properties:
-      filename:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.UpdateStorageRequest:
-    properties:
-      config:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageConfig'
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageType'
-    type: object
-  github_com_usememos_memos_api_v1.UpdateUserRequest:
-    properties:
-      avatarUrl:
-        type: string
-      email:
-        type: string
-      nickname:
-        type: string
-      password:
-        type: string
-      rowStatus:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus'
-      username:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest:
-    properties:
-      pinned:
-        type: boolean
-    type: object
-  github_com_usememos_memos_api_v1.UpsertMemoRelationRequest:
-    properties:
-      relatedMemoId:
-        type: integer
-      type:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.MemoRelationType'
-    type: object
-  github_com_usememos_memos_api_v1.UpsertSystemSettingRequest:
-    properties:
-      description:
-        type: string
-      name:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.SystemSettingName'
-      value:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.UpsertTagRequest:
-    properties:
-      name:
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.User:
-    properties:
-      avatarUrl:
-        type: string
-      createdTs:
-        type: integer
-      email:
-        type: string
-      id:
-        type: integer
-      nickname:
-        type: string
-      role:
-        $ref: '#/definitions/github_com_usememos_memos_api_v1.Role'
-      rowStatus:
-        allOf:
-        - $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus'
-        description: Standard fields
-      updatedTs:
-        type: integer
-      username:
-        description: Domain specific fields
-        type: string
-    type: object
-  github_com_usememos_memos_api_v1.Visibility:
-    enum:
-    - PUBLIC
-    - PROTECTED
-    - PRIVATE
-    type: string
-    x-enum-varnames:
-    - Public
-    - Protected
-    - Private
-  profile.Profile:
-    properties:
-      mode:
-        description: Mode can be "prod" or "dev" or "demo"
-        type: string
-      version:
-        description: Version is the current version of server
-        type: string
-    type: object
-  store.FieldMapping:
-    properties:
-      displayName:
-        type: string
-      email:
-        type: string
-      identifier:
-        type: string
-    type: object
-  store.IdentityProvider:
-    properties:
-      config:
-        $ref: '#/definitions/store.IdentityProviderConfig'
-      id:
-        type: integer
-      identifierFilter:
-        type: string
-      name:
-        type: string
-      type:
-        $ref: '#/definitions/store.IdentityProviderType'
-    type: object
-  store.IdentityProviderConfig:
-    properties:
-      oauth2Config:
-        $ref: '#/definitions/store.IdentityProviderOAuth2Config'
-    type: object
-  store.IdentityProviderOAuth2Config:
-    properties:
-      authUrl:
-        type: string
-      clientId:
-        type: string
-      clientSecret:
-        type: string
-      fieldMapping:
-        $ref: '#/definitions/store.FieldMapping'
-      scopes:
-        items:
-          type: string
-        type: array
-      tokenUrl:
-        type: string
-      userInfoUrl:
-        type: string
-    type: object
-  store.IdentityProviderType:
-    enum:
-    - OAUTH2
-    type: string
-    x-enum-varnames:
-    - IdentityProviderOAuth2Type
-  store.Memo:
-    properties:
-      content:
-        description: Domain specific fields
-        type: string
-      createdTs:
-        type: integer
-      creatorID:
-        type: integer
-      id:
-        type: integer
-      parentID:
-        type: integer
-      pinned:
-        description: Composed fields
-        type: boolean
-      resourceName:
-        type: string
-      rowStatus:
-        allOf:
-        - $ref: '#/definitions/store.RowStatus'
-        description: Standard fields
-      updatedTs:
-        type: integer
-      visibility:
-        $ref: '#/definitions/store.Visibility'
-    type: object
-  store.MemoRelation:
-    properties:
-      memoID:
-        type: integer
-      relatedMemoID:
-        type: integer
-      type:
-        $ref: '#/definitions/store.MemoRelationType'
-    type: object
-  store.MemoRelationType:
-    enum:
-    - REFERENCE
-    - COMMENT
-    type: string
-    x-enum-varnames:
-    - MemoRelationReference
-    - MemoRelationComment
-  store.Resource:
-    properties:
-      blob:
-        items:
-          type: integer
-        type: array
-      createdTs:
-        type: integer
-      creatorID:
-        description: Standard fields
-        type: integer
-      externalLink:
-        type: string
-      filename:
-        description: Domain specific fields
-        type: string
-      id:
-        type: integer
-      internalPath:
-        type: string
-      memoID:
-        type: integer
-      resourceName:
-        type: string
-      size:
-        type: integer
-      type:
-        type: string
-      updatedTs:
-        type: integer
-    type: object
-  store.Role:
-    enum:
-    - HOST
-    - ADMIN
-    - USER
-    type: string
-    x-enum-varnames:
-    - RoleHost
-    - RoleAdmin
-    - RoleUser
-  store.RowStatus:
-    enum:
-    - NORMAL
-    - ARCHIVED
-    type: string
-    x-enum-varnames:
-    - Normal
-    - Archived
-  store.Storage:
-    properties:
-      config:
-        type: string
-      id:
-        type: integer
-      name:
-        type: string
-      type:
-        type: string
-    type: object
-  store.User:
-    properties:
-      avatarURL:
-        type: string
-      createdTs:
-        type: integer
-      email:
-        type: string
-      id:
-        type: integer
-      nickname:
-        type: string
-      passwordHash:
-        type: string
-      role:
-        $ref: '#/definitions/store.Role'
-      rowStatus:
-        allOf:
-        - $ref: '#/definitions/store.RowStatus'
-        description: Standard fields
-      updatedTs:
-        type: integer
-      username:
-        description: Domain specific fields
-        type: string
-    type: object
-  store.Visibility:
-    enum:
-    - PUBLIC
-    - PROTECTED
-    - PRIVATE
-    type: string
-    x-enum-varnames:
-    - Public
-    - Protected
-    - Private
-externalDocs:
-  description: Find out more about Memos.
-  url: https://usememos.com/
-info:
-  contact:
-    name: API Support
-    url: https://github.com/orgs/usememos/discussions
-  description: A privacy-first, lightweight note-taking service.
-  license:
-    name: MIT License
-    url: https://github.com/usememos/memos/blob/main/LICENSE
-  title: memos API
-  version: "1.0"
-paths:
-  /api/v1/auth/signin:
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Sign-in object
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.SignIn'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: User information
-          schema:
-            $ref: '#/definitions/store.User'
-        "400":
-          description: Malformatted signin request
-        "401":
-          description: Password login is deactivated | Incorrect login credentials,
-            please try again
-        "403":
-          description: User has been archived with username %s
-        "500":
-          description: Failed to find system setting | Failed to unmarshal system
-            setting | Incorrect login credentials, please try again | Failed to generate
-            tokens | Failed to create activity
-      summary: Sign-in to memos.
-      tags:
-      - auth
-  /api/v1/auth/signin/sso:
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: SSO sign-in object
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.SSOSignIn'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: User information
-          schema:
-            $ref: '#/definitions/store.User'
-        "400":
-          description: Malformatted signin request
-        "401":
-          description: Access denied, identifier does not match the filter.
-        "403":
-          description: User has been archived with username {username}
-        "404":
-          description: Identity provider not found
-        "500":
-          description: Failed to find identity provider | Failed to create identity
-            provider instance | Failed to exchange token | Failed to get user info
-            | Failed to compile identifier filter | Incorrect login credentials, please
-            try again | Failed to generate random password | Failed to generate password
-            hash | Failed to create user | Failed to generate tokens | Failed to create
-            activity
-      summary: Sign-in to memos using SSO.
-      tags:
-      - auth
-  /api/v1/auth/signout:
-    post:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Sign-out success
-          schema:
-            type: boolean
-      summary: Sign-out from memos.
-      tags:
-      - auth
-  /api/v1/auth/signup:
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Sign-up object
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.SignUp'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: User information
-          schema:
-            $ref: '#/definitions/store.User'
-        "400":
-          description: Malformatted signup request | Failed to find users
-        "401":
-          description: signup is disabled
-        "403":
-          description: Forbidden
-        "404":
-          description: Not found
-        "500":
-          description: Failed to find system setting | Failed to unmarshal system
-            setting allow signup | Failed to generate password hash | Failed to create
-            user | Failed to generate tokens | Failed to create activity
-      summary: Sign-up to memos.
-      tags:
-      - auth
-  /api/v1/idp:
-    get:
-      description: '*clientSecret is only available for host user'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: List of available identity providers
-          schema:
-            items:
-              $ref: '#/definitions/api_v1.IdentityProvider'
-            type: array
-        "500":
-          description: Failed to find identity provider list | Failed to find user
-      summary: Get a list of identity providers
-      tags:
-      - idp
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Identity provider information
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.CreateIdentityProviderRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Identity provider information
-          schema:
-            $ref: '#/definitions/store.IdentityProvider'
-        "400":
-          description: Malformatted post identity provider request
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to create identity provider
-      summary: Create Identity Provider
-      tags:
-      - idp
-  /api/v1/idp/{idpId}:
-    delete:
-      consumes:
-      - application/json
-      parameters:
-      - description: Identity Provider ID
-        in: path
-        name: idpId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Identity Provider deleted
-          schema:
-            type: boolean
-        "400":
-          description: 'ID is not a number: %s | Malformatted patch identity provider
-            request'
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to patch identity provider
-      summary: Delete an identity provider by ID
-      tags:
-      - idp
-    get:
-      consumes:
-      - application/json
-      parameters:
-      - description: Identity provider ID
-        in: path
-        name: idpId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Requested identity provider
-          schema:
-            $ref: '#/definitions/store.IdentityProvider'
-        "400":
-          description: 'ID is not a number: %s'
-        "401":
-          description: Missing user in session | Unauthorized
-        "404":
-          description: Identity provider not found
-        "500":
-          description: Failed to find identity provider list | Failed to find user
-      summary: Get an identity provider by ID
-      tags:
-      - idp
-    patch:
-      consumes:
-      - application/json
-      parameters:
-      - description: Identity Provider ID
-        in: path
-        name: idpId
-        required: true
-        type: integer
-      - description: Patched identity provider information
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.UpdateIdentityProviderRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Patched identity provider
-          schema:
-            $ref: '#/definitions/store.IdentityProvider'
-        "400":
-          description: 'ID is not a number: %s | Malformatted patch identity provider
-            request'
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to patch identity provider
-      summary: Update an identity provider by ID
-      tags:
-      - idp
-  /api/v1/memo:
-    get:
-      parameters:
-      - description: Creator ID
-        in: query
-        name: creatorId
-        type: integer
-      - description: Creator username
-        in: query
-        name: creatorUsername
-        type: string
-      - description: Row status
-        enum:
-        - NORMAL
-        - ARCHIVED
-        in: query
-        name: rowStatus
-        type: string
-      - description: Pinned
-        in: query
-        name: pinned
-        type: boolean
-      - description: 'Search for tag. Do not append #'
-        in: query
-        name: tag
-        type: string
-      - description: Search for content
-        in: query
-        name: content
-        type: string
-      - description: Limit
-        in: query
-        name: limit
-        type: integer
-      - description: Offset
-        in: query
-        name: offset
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo list
-          schema:
-            items:
-              $ref: '#/definitions/store.Memo'
-            type: array
-        "400":
-          description: Missing user to find memo
-        "500":
-          description: Failed to get memo display with updated ts setting value |
-            Failed to fetch memo list | Failed to compose memo response
-      summary: Get a list of memos matching optional filters
-      tags:
-      - memo
-    post:
-      consumes:
-      - application/json
-      description: |-
-        Visibility can be PUBLIC, PROTECTED or PRIVATE
-        *You should omit fields to use their default values
-      parameters:
-      - description: Request object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.CreateMemoRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Stored memo
-          schema:
-            $ref: '#/definitions/store.Memo'
-        "400":
-          description: Malformatted post memo request | Content size overflow, up
-            to 1MB
-        "401":
-          description: Missing user in session
-        "404":
-          description: 'User not found | Memo not found: %d'
-        "500":
-          description: Failed to find user setting | Failed to unmarshal user setting
-            value | Failed to find system setting | Failed to unmarshal system setting
-            | Failed to find user | Failed to create memo | Failed to create activity
-            | Failed to upsert memo resource | Failed to upsert memo relation | Failed
-            to compose memo | Failed to compose memo response
-      summary: Create a memo
-      tags:
-      - memo
-  /api/v1/memo/{memoId}:
-    delete:
-      parameters:
-      - description: Memo ID to delete
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo deleted
-          schema:
-            type: boolean
-        "400":
-          description: 'ID is not a number: %s'
-        "401":
-          description: Missing user in session | Unauthorized
-        "404":
-          description: 'Memo not found: %d'
-        "500":
-          description: 'Failed to find memo | Failed to delete memo ID: %v'
-      summary: Delete memo by ID
-      tags:
-      - memo
-    get:
-      parameters:
-      - description: Memo ID
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo list
-          schema:
-            items:
-              $ref: '#/definitions/store.Memo'
-            type: array
-        "400":
-          description: 'ID is not a number: %s'
-        "401":
-          description: Missing user in session
-        "403":
-          description: this memo is private only | this memo is protected, missing
-            user in session
-        "404":
-          description: 'Memo not found: %d'
-        "500":
-          description: 'Failed to find memo by ID: %v | Failed to compose memo response'
-      summary: Get memo by ID
-      tags:
-      - memo
-    patch:
-      consumes:
-      - application/json
-      description: |-
-        Visibility can be PUBLIC, PROTECTED or PRIVATE
-        *You should omit fields to use their default values
-      parameters:
-      - description: ID of memo to update
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      - description: Patched object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.PatchMemoRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Stored memo
-          schema:
-            $ref: '#/definitions/store.Memo'
-        "400":
-          description: 'ID is not a number: %s | Malformatted patch memo request |
-            Content size overflow, up to 1MB'
-        "401":
-          description: Missing user in session | Unauthorized
-        "404":
-          description: 'Memo not found: %d'
-        "500":
-          description: Failed to find memo | Failed to patch memo | Failed to upsert
-            memo resource | Failed to delete memo resource | Failed to compose memo
-            response
-      summary: Update a memo
-      tags:
-      - memo
-  /api/v1/memo/{memoId}/organizer:
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: ID of memo to organize
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      - description: Memo organizer object
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo information
-          schema:
-            $ref: '#/definitions/store.Memo'
-        "400":
-          description: 'ID is not a number: %s | Malformatted post memo organizer
-            request'
-        "401":
-          description: Missing user in session | Unauthorized
-        "404":
-          description: 'Memo not found: %v'
-        "500":
-          description: 'Failed to find memo | Failed to upsert memo organizer | Failed
-            to find memo by ID: %v | Failed to compose memo response'
-      summary: Organize memo (pin/unpin)
-      tags:
-      - memo-organizer
-  /api/v1/memo/{memoId}/relation:
-    get:
-      consumes:
-      - application/json
-      parameters:
-      - description: ID of memo to find relations
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo relation information list
-          schema:
-            items:
-              $ref: '#/definitions/store.MemoRelation'
-            type: array
-        "400":
-          description: 'ID is not a number: %s'
-        "500":
-          description: Failed to list memo relations
-      summary: Get a list of Memo Relations
-      tags:
-      - memo-relation
-    post:
-      consumes:
-      - application/json
-      description: Create a relation between two memos
-      parameters:
-      - description: ID of memo to relate
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      - description: Memo relation object
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo relation information
-          schema:
-            $ref: '#/definitions/store.MemoRelation'
-        "400":
-          description: 'ID is not a number: %s | Malformatted post memo relation request'
-        "500":
-          description: Failed to upsert memo relation
-      summary: Create Memo Relation
-      tags:
-      - memo-relation
-  /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType}:
-    delete:
-      consumes:
-      - application/json
-      description: Removes a relation between two memos
-      parameters:
-      - description: ID of memo to find relations
-        in: path
-        name: memoId
-        required: true
-        type: integer
-      - description: ID of memo to remove relation to
-        in: path
-        name: relatedMemoId
-        required: true
-        type: integer
-      - description: Type of relation to remove
-        enum:
-        - REFERENCE
-        - COMMENT
-        in: path
-        name: relationType
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo relation deleted
-          schema:
-            type: boolean
-        "400":
-          description: 'Memo ID is not a number: %s | Related memo ID is not a number:
-            %s'
-        "500":
-          description: Failed to delete memo relation
-      summary: Delete a Memo Relation
-      tags:
-      - memo-relation
-  /api/v1/memo/all:
-    get:
-      description: |-
-        This should also list protected memos if the user is logged in
-        Authentication is optional
-      parameters:
-      - description: Limit
-        in: query
-        name: limit
-        type: integer
-      - description: Offset
-        in: query
-        name: offset
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo list
-          schema:
-            items:
-              $ref: '#/definitions/store.Memo'
-            type: array
-        "500":
-          description: Failed to get memo display with updated ts setting value |
-            Failed to fetch all memo list | Failed to compose memo response
-      summary: Get a list of public memos matching optional filters
-      tags:
-      - memo
-  /api/v1/memo/stats:
-    get:
-      description: Used to generate the heatmap
-      parameters:
-      - description: Creator ID
-        in: query
-        name: creatorId
-        type: integer
-      - description: Creator username
-        in: query
-        name: creatorUsername
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Memo createdTs list
-          schema:
-            items:
-              type: integer
-            type: array
-        "400":
-          description: Missing user id to find memo
-        "500":
-          description: Failed to get memo display with updated ts setting value |
-            Failed to find memo list | Failed to compose memo response
-      summary: Get memo stats by creator ID or username
-      tags:
-      - memo
-  /api/v1/ping:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: If succeed to ping the system
-          schema:
-            type: boolean
-      summary: Ping the system
-      tags:
-      - system
-  /api/v1/resource:
-    get:
-      parameters:
-      - description: Limit
-        in: query
-        name: limit
-        type: integer
-      - description: Offset
-        in: query
-        name: offset
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Resource list
-          schema:
-            items:
-              $ref: '#/definitions/store.Resource'
-            type: array
-        "401":
-          description: Missing user in session
-        "500":
-          description: Failed to fetch resource list
-      summary: Get a list of resources
-      tags:
-      - resource
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Request object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.CreateResourceRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Created resource
-          schema:
-            $ref: '#/definitions/store.Resource'
-        "400":
-          description: Malformatted post resource request | Invalid external link
-            | Invalid external link scheme | Failed to request %s | Failed to read
-            %s | Failed to read mime from %s
-        "401":
-          description: Missing user in session
-        "500":
-          description: Failed to save resource | Failed to create resource | Failed
-            to create activity
-      summary: Create resource
-      tags:
-      - resource
-  /api/v1/resource/{resourceId}:
-    delete:
-      parameters:
-      - description: Resource ID
-        in: path
-        name: resourceId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Resource deleted
-          schema:
-            type: boolean
-        "400":
-          description: 'ID is not a number: %s'
-        "401":
-          description: Missing user in session
-        "404":
-          description: 'Resource not found: %d'
-        "500":
-          description: Failed to find resource | Failed to delete resource
-      summary: Delete a resource
-      tags:
-      - resource
-    patch:
-      parameters:
-      - description: Resource ID
-        in: path
-        name: resourceId
-        required: true
-        type: integer
-      - description: Patch resource request
-        in: body
-        name: patch
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.UpdateResourceRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Updated resource
-          schema:
-            $ref: '#/definitions/store.Resource'
-        "400":
-          description: 'ID is not a number: %s | Malformatted patch resource request'
-        "401":
-          description: Missing user in session | Unauthorized
-        "404":
-          description: 'Resource not found: %d'
-        "500":
-          description: Failed to find resource | Failed to patch resource
-      summary: Update a resource
-      tags:
-      - resource
-  /api/v1/resource/blob:
-    post:
-      consumes:
-      - multipart/form-data
-      parameters:
-      - description: File to upload
-        in: formData
-        name: file
-        required: true
-        type: file
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Created resource
-          schema:
-            $ref: '#/definitions/store.Resource'
-        "400":
-          description: Upload file not found | File size exceeds allowed limit of
-            %d MiB | Failed to parse upload data
-        "401":
-          description: Missing user in session
-        "500":
-          description: Failed to get uploading file | Failed to open file | Failed
-            to save resource | Failed to create resource | Failed to create activity
-      summary: Upload resource
-      tags:
-      - resource
-  /api/v1/status:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: System GetSystemStatus
-          schema:
-            $ref: '#/definitions/api_v1.SystemStatus'
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find host user | Failed to find system setting list
-            | Failed to unmarshal system setting customized profile value
-      summary: Get system GetSystemStatus
-      tags:
-      - system
-  /api/v1/storage:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: List of storages
-          schema:
-            items:
-              $ref: '#/definitions/store.Storage'
-            type: array
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to convert storage
-      summary: Get a list of storages
-      tags:
-      - storage
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Request object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.CreateStorageRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Created storage
-          schema:
-            $ref: '#/definitions/store.Storage'
-        "400":
-          description: Malformatted post storage request
-        "401":
-          description: Missing user in session
-        "500":
-          description: Failed to find user | Failed to create storage | Failed to
-            convert storage
-      summary: Create storage
-      tags:
-      - storage
-  /api/v1/storage/{storageId}:
-    delete:
-      parameters:
-      - description: Storage ID
-        in: path
-        name: storageId
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Storage deleted
-          schema:
-            type: boolean
-        "400":
-          description: 'ID is not a number: %s | Storage service %d is using'
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to find storage | Failed to unmarshal
-            storage service id | Failed to delete storage
-      summary: Delete a storage
-      tags:
-      - storage
-    patch:
-      parameters:
-      - description: Storage ID
-        in: path
-        name: storageId
-        required: true
-        type: integer
-      - description: Patch request
-        in: body
-        name: patch
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpdateStorageRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Updated resource
-          schema:
-            $ref: '#/definitions/store.Storage'
-        "400":
-          description: 'ID is not a number: %s | Malformatted patch storage request
-            | Malformatted post storage request'
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to patch storage | Failed to convert
-            storage
-      summary: Update a storage
-      tags:
-      - storage
-  /api/v1/system/setting:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: System setting list
-          schema:
-            items:
-              $ref: '#/definitions/api_v1.SystemSetting'
-            type: array
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to find system setting list
-      summary: Get a list of system settings
-      tags:
-      - system-setting
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Request object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.UpsertSystemSettingRequest'
-      produces:
-      - application/json
-      responses:
-        "400":
-          description: Malformatted post system setting request | invalid system setting
-        "401":
-          description: Missing user in session | Unauthorized
-        "403":
-          description: Cannot disable passwords if no SSO identity provider is configured.
-        "500":
-          description: Failed to find user | Failed to upsert system setting
-      summary: Create system setting
-      tags:
-      - system-setting
-  /api/v1/system/vacuum:
-    post:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Database vacuumed
-          schema:
-            type: boolean
-        "401":
-          description: Missing user in session | Unauthorized
-        "500":
-          description: Failed to find user | Failed to ExecVacuum database
-      summary: Vacuum the database
-      tags:
-      - system
-  /api/v1/tag:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Tag list
-          schema:
-            items:
-              type: string
-            type: array
-        "400":
-          description: Missing user id to find tag
-        "500":
-          description: Failed to find tag list
-      summary: Get a list of tags
-      tags:
-      - tag
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Request object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertTagRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Created tag name
-          schema:
-            type: string
-        "400":
-          description: Malformatted post tag request | Tag name shouldn't be empty
-        "401":
-          description: Missing user in session
-        "500":
-          description: Failed to upsert tag | Failed to create activity
-      summary: Create a tag
-      tags:
-      - tag
-  /api/v1/tag/delete:
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Request object.
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/github_com_usememos_memos_api_v1.DeleteTagRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Tag deleted
-          schema:
-            type: boolean
-        "400":
-          description: Malformatted post tag request | Tag name shouldn't be empty
-        "401":
-          description: Missing user in session
-        "500":
-          description: 'Failed to delete tag name: %v'
-      summary: Delete a tag
-      tags:
-      - tag
-  /api/v1/tag/suggestion:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Tag list
-          schema:
-            items:
-              type: string
-            type: array
-        "400":
-          description: Missing user session
-        "500":
-          description: Failed to find memo list | Failed to find tag list
-      summary: Get a list of tags suggested from other memos contents
-      tags:
-      - tag
-  /api/v1/user:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: User list
-          schema:
-            items:
-              $ref: '#/definitions/store.User'
-            type: array
-        "500":
-          description: Failed to fetch user list
-      summary: Get a list of users
-      tags:
-      - user
-    post:
-      consumes:
-      - application/json
-      parameters:
-      - description: Request object
-        in: body
-        name: body
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.CreateUserRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Created user
-          schema:
-            $ref: '#/definitions/store.User'
-        "400":
-          description: Malformatted post user request | Invalid user create format
-        "401":
-          description: Missing auth session | Unauthorized to create user
-        "403":
-          description: Could not create host user
-        "500":
-          description: Failed to find user by id | Failed to generate password hash
-            | Failed to create user | Failed to create activity
-      summary: Create a user
-      tags:
-      - user
-  /api/v1/user/{id}:
-    delete:
-      parameters:
-      - description: User ID
-        in: path
-        name: id
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: User deleted
-          schema:
-            type: boolean
-        "400":
-          description: 'ID is not a number: %s | Current session user not found with
-            ID: %d'
-        "401":
-          description: Missing user in session
-        "403":
-          description: Unauthorized to delete user
-        "500":
-          description: Failed to find user | Failed to delete user
-      summary: Delete a user
-      tags:
-      - user
-    get:
-      parameters:
-      - description: User ID
-        in: path
-        name: id
-        required: true
-        type: integer
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Requested user
-          schema:
-            $ref: '#/definitions/store.User'
-        "400":
-          description: Malformatted user id
-        "404":
-          description: User not found
-        "500":
-          description: Failed to find user
-      summary: Get user by id
-      tags:
-      - user
-    patch:
-      parameters:
-      - description: User ID
-        in: path
-        name: id
-        required: true
-        type: string
-      - description: Patch request
-        in: body
-        name: patch
-        required: true
-        schema:
-          $ref: '#/definitions/api_v1.UpdateUserRequest'
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Updated user
-          schema:
-            $ref: '#/definitions/store.User'
-        "400":
-          description: 'ID is not a number: %s | Current session user not found with
-            ID: %d | Malformatted patch user request | Invalid update user request'
-        "401":
-          description: Missing user in session
-        "403":
-          description: Unauthorized to update user
-        "500":
-          description: Failed to find user | Failed to generate password hash | Failed
-            to patch user | Failed to find userSettingList
-      summary: Update a user
-      tags:
-      - user
-  /api/v1/user/me:
-    get:
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Current user
-          schema:
-            $ref: '#/definitions/store.User'
-        "401":
-          description: Missing auth session
-        "500":
-          description: Failed to find user | Failed to find userSettingList
-      summary: Get current user
-      tags:
-      - user
-  /api/v1/user/name/{username}:
-    get:
-      parameters:
-      - description: Username
-        in: path
-        name: username
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: Requested user
-          schema:
-            $ref: '#/definitions/store.User'
-        "404":
-          description: User not found
-        "500":
-          description: Failed to find user
-      summary: Get user by username
-      tags:
-      - user
-  /o/get/GetImage:
-    get:
-      parameters:
-      - description: Image url
-        in: query
-        name: url
-        required: true
-        type: string
-      produces:
-      - GetImage/*
-      responses:
-        "200":
-          description: Image
-        "400":
-          description: 'Missing GetImage url | Wrong url | Failed to get GetImage
-            url: %s'
-        "500":
-          description: Failed to write GetImage blob
-      summary: Get GetImage from URL
-      tags:
-      - image-url
-swagger: "2.0"
diff --git a/server/route/api/v1/system.go b/server/route/api/v1/system.go
deleted file mode 100644
index 412f936f3..000000000
--- a/server/route/api/v1/system.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-
-	"github.com/usememos/memos/server/profile"
-	"github.com/usememos/memos/store"
-)
-
-type SystemStatus struct {
-	Host    *User           `json:"host"`
-	Profile profile.Profile `json:"profile"`
-	DBSize  int64           `json:"dbSize"`
-
-	// System settings
-	// Disable password login.
-	DisablePasswordLogin bool `json:"disablePasswordLogin"`
-	// Disable public memos.
-	DisablePublicMemos bool `json:"disablePublicMemos"`
-	// Max upload size.
-	MaxUploadSizeMiB int `json:"maxUploadSizeMiB"`
-	// Customized server profile, including server name and external url.
-	CustomizedProfile CustomizedProfile `json:"customizedProfile"`
-	// Storage service ID.
-	StorageServiceID int32 `json:"storageServiceId"`
-	// Local storage path.
-	LocalStoragePath string `json:"localStoragePath"`
-	// Memo display with updated timestamp.
-	MemoDisplayWithUpdatedTs bool `json:"memoDisplayWithUpdatedTs"`
-}
-
-func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
-	g.GET("/ping", s.PingSystem)
-	g.GET("/status", s.GetSystemStatus)
-	g.POST("/system/vacuum", s.ExecVacuum)
-}
-
-// PingSystem godoc
-//
-//	@Summary	Ping the system
-//	@Tags		system
-//	@Produce	json
-//	@Success	200	{boolean}	true	"If succeed to ping the system"
-//	@Router		/api/v1/ping [GET]
-func (*APIV1Service) PingSystem(c echo.Context) error {
-	return c.JSON(http.StatusOK, true)
-}
-
-// GetSystemStatus godoc
-//
-//	@Summary	Get system GetSystemStatus
-//	@Tags		system
-//	@Produce	json
-//	@Success	200	{object}	SystemStatus	"System GetSystemStatus"
-//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
-//	@Failure	500	{object}	nil				"Failed to find host user | Failed to find system setting list | Failed to unmarshal system setting customized profile value"
-//	@Router		/api/v1/status [GET]
-func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
-	ctx := c.Request().Context()
-
-	systemStatus := SystemStatus{
-		Profile: profile.Profile{
-			Mode:    s.Profile.Mode,
-			Version: s.Profile.Version,
-		},
-		MaxUploadSizeMiB: 32,
-		CustomizedProfile: CustomizedProfile{
-			Name:       "Memos",
-			Locale:     "en",
-			Appearance: "system",
-		},
-		StorageServiceID: DefaultStorage,
-		LocalStoragePath: "assets/{timestamp}_{filename}",
-	}
-
-	hostUserType := store.RoleHost
-	hostUser, err := s.Store.GetUser(ctx, &store.FindUser{
-		Role: &hostUserType,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
-	}
-	if hostUser != nil {
-		systemStatus.Host = &User{ID: hostUser.ID}
-	}
-
-	workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace general setting").SetInternal(err)
-	}
-	systemStatus.DisablePasswordLogin = workspaceGeneralSetting.DisallowPasswordLogin
-
-	systemSettingList, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
-	}
-	for _, systemSetting := range systemSettingList {
-		var baseValue any
-		err := json.Unmarshal([]byte(systemSetting.Value), &baseValue)
-		if err != nil {
-			// Skip invalid value.
-			continue
-		}
-
-		switch systemSetting.Name {
-		case SystemSettingMaxUploadSizeMiBName.String():
-			systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
-		case SystemSettingCustomizedProfileName.String():
-			customizedProfile := CustomizedProfile{}
-			if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting customized profile value").SetInternal(err)
-			}
-			systemStatus.CustomizedProfile = customizedProfile
-		case SystemSettingStorageServiceIDName.String():
-			systemStatus.StorageServiceID = int32(baseValue.(float64))
-		default:
-			// Skip unknown system setting.
-		}
-	}
-
-	return c.JSON(http.StatusOK, systemStatus)
-}
-
-// ExecVacuum godoc
-//
-//	@Summary	Vacuum the database
-//	@Tags		system
-//	@Produce	json
-//	@Success	200	{boolean}	true	"Database vacuumed"
-//	@Failure	401	{object}	nil		"Missing user in session | Unauthorized"
-//	@Failure	500	{object}	nil		"Failed to find user | Failed to ExecVacuum database"
-//	@Router		/api/v1/system/vacuum [POST]
-func (s *APIV1Service) ExecVacuum(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	if err := s.Store.Vacuum(ctx); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
diff --git a/server/route/api/v1/system_setting.go b/server/route/api/v1/system_setting.go
deleted file mode 100644
index 13fbcb90b..000000000
--- a/server/route/api/v1/system_setting.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"net/http"
-	"path/filepath"
-	"strings"
-
-	"github.com/labstack/echo/v4"
-	"github.com/pkg/errors"
-
-	"github.com/usememos/memos/store"
-)
-
-type SystemSettingName string
-
-const (
-	// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
-	SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
-	// 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"
-)
-const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
-
-// 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"`
-}
-
-func (key SystemSettingName) String() string {
-	return string(key)
-}
-
-type SystemSetting struct {
-	Name SystemSettingName `json:"name"`
-	// Value is a JSON string with basic value.
-	Value       string `json:"value"`
-	Description string `json:"description"`
-}
-
-type UpsertSystemSettingRequest struct {
-	Name        SystemSettingName `json:"name"`
-	Value       string            `json:"value"`
-	Description string            `json:"description"`
-}
-
-func (s *APIV1Service) registerSystemSettingRoutes(g *echo.Group) {
-	g.GET("/system/setting", s.GetSystemSettingList)
-	g.POST("/system/setting", s.CreateSystemSetting)
-}
-
-// GetSystemSettingList godoc
-//
-//	@Summary	Get a list of system settings
-//	@Tags		system-setting
-//	@Produce	json
-//	@Success	200	{object}	[]SystemSetting	"System setting list"
-//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
-//	@Failure	500	{object}	nil				"Failed to find user | Failed to find system setting list"
-//	@Router		/api/v1/system/setting [GET]
-func (s *APIV1Service) GetSystemSettingList(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	list, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
-	}
-
-	systemSettingList := make([]*SystemSetting, 0, len(list))
-	for _, systemSetting := range list {
-		systemSettingList = append(systemSettingList, convertSystemSettingFromStore(systemSetting))
-	}
-	return c.JSON(http.StatusOK, systemSettingList)
-}
-
-// CreateSystemSetting godoc
-//
-//	@Summary	Create system setting
-//	@Tags		system-setting
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		UpsertSystemSettingRequest	true	"Request object."
-//	@Failure	400		{object}	nil							"Malformatted post system setting request | invalid system setting"
-//	@Failure	401		{object}	nil							"Missing user in session | Unauthorized"
-//	@Failure	403		{object}	nil							"Cannot disable passwords if no SSO identity provider is configured."
-//	@Failure	500		{object}	nil							"Failed to find user | Failed to upsert system setting"
-//	@Router		/api/v1/system/setting [POST]
-func (s *APIV1Service) CreateSystemSetting(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil || user.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
-	}
-
-	systemSettingUpsert := &UpsertSystemSettingRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
-	}
-	if err := systemSettingUpsert.Validate(); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
-	}
-
-	systemSetting, err := s.Store.UpsertWorkspaceSetting(ctx, &store.WorkspaceSetting{
-		Name:        systemSettingUpsert.Name.String(),
-		Value:       systemSettingUpsert.Value,
-		Description: systemSettingUpsert.Description,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, convertSystemSettingFromStore(systemSetting))
-}
-
-func (upsert UpsertSystemSettingRequest) Validate() error {
-	switch settingName := upsert.Name; settingName {
-	case SystemSettingMaxUploadSizeMiBName:
-		var value int
-		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
-			return errors.Errorf(systemSettingUnmarshalError, settingName)
-		}
-	case SystemSettingCustomizedProfileName:
-		customizedProfile := CustomizedProfile{
-			Name:        "Memos",
-			LogoURL:     "",
-			Description: "",
-			Locale:      "en",
-			Appearance:  "system",
-		}
-		if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
-			return errors.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 errors.Errorf(systemSettingUnmarshalError, settingName)
-		}
-		return nil
-	case SystemSettingLocalStoragePathName:
-		value := ""
-		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
-			return errors.Errorf(systemSettingUnmarshalError, settingName)
-		}
-
-		trimmedValue := strings.TrimSpace(value)
-		switch {
-		case trimmedValue != value:
-			return errors.New("local storage path must not contain leading or trailing whitespace")
-		case trimmedValue == "":
-			return errors.New("local storage path can't be empty")
-		case strings.Contains(trimmedValue, "\\"):
-			return errors.New("local storage path must use forward slashes `/`")
-		case strings.Contains(trimmedValue, "../"):
-			return errors.New("local storage path is not allowed to contain `../`")
-		case strings.HasPrefix(trimmedValue, "./"):
-			return errors.New("local storage path is not allowed to start with `./`")
-		case filepath.IsAbs(trimmedValue) || trimmedValue[0] == '/':
-			return errors.New("local storage path must be a relative path")
-		case !strings.Contains(trimmedValue, "{filename}"):
-			return errors.New("local storage path must contain `{filename}`")
-		}
-	default:
-		return errors.New("invalid system setting name")
-	}
-	return nil
-}
-
-func convertSystemSettingFromStore(systemSetting *store.WorkspaceSetting) *SystemSetting {
-	return &SystemSetting{
-		Name:        SystemSettingName(systemSetting.Name),
-		Value:       systemSetting.Value,
-		Description: systemSetting.Description,
-	}
-}
diff --git a/server/route/api/v1/tag.go b/server/route/api/v1/tag.go
deleted file mode 100644
index 72da0ccc0..000000000
--- a/server/route/api/v1/tag.go
+++ /dev/null
@@ -1,218 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"regexp"
-	"slices"
-	"sort"
-
-	"github.com/labstack/echo/v4"
-
-	"github.com/usememos/memos/store"
-)
-
-type Tag struct {
-	Name      string
-	CreatorID int32
-}
-
-type UpsertTagRequest struct {
-	Name string `json:"name"`
-}
-
-type DeleteTagRequest struct {
-	Name string `json:"name"`
-}
-
-func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
-	g.GET("/tag", s.GetTagList)
-	g.POST("/tag", s.CreateTag)
-	g.GET("/tag/suggestion", s.GetTagSuggestion)
-	g.POST("/tag/delete", s.DeleteTag)
-}
-
-// GetTagList godoc
-//
-//	@Summary	Get a list of tags
-//	@Tags		tag
-//	@Produce	json
-//	@Success	200	{object}	[]string	"Tag list"
-//	@Failure	400	{object}	nil			"Missing user id to find tag"
-//	@Failure	500	{object}	nil			"Failed to find tag list"
-//	@Router		/api/v1/tag [GET]
-func (s *APIV1Service) GetTagList(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
-	}
-
-	list, err := s.Store.ListTags(ctx, &store.FindTag{
-		CreatorID: userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
-	}
-
-	tagNameList := []string{}
-	for _, tag := range list {
-		tagNameList = append(tagNameList, tag.Name)
-	}
-	return c.JSON(http.StatusOK, tagNameList)
-}
-
-// CreateTag godoc
-//
-//	@Summary	Create a tag
-//	@Tags		tag
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		UpsertTagRequest	true	"Request object."
-//	@Success	200		{object}	string				"Created tag name"
-//	@Failure	400		{object}	nil					"Malformatted post tag request | Tag name shouldn't be empty"
-//	@Failure	401		{object}	nil					"Missing user in session"
-//	@Failure	500		{object}	nil					"Failed to upsert tag | Failed to create activity"
-//	@Router		/api/v1/tag [POST]
-func (s *APIV1Service) CreateTag(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	tagUpsert := &UpsertTagRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
-	}
-	if tagUpsert.Name == "" {
-		return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
-	}
-
-	tag, err := s.Store.UpsertTag(ctx, &store.Tag{
-		Name:      tagUpsert.Name,
-		CreatorID: userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
-	}
-	tagMessage := convertTagFromStore(tag)
-	return c.JSON(http.StatusOK, tagMessage.Name)
-}
-
-// DeleteTag godoc
-//
-//	@Summary	Delete a tag
-//	@Tags		tag
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		DeleteTagRequest	true	"Request object."
-//	@Success	200		{boolean}	true				"Tag deleted"
-//	@Failure	400		{object}	nil					"Malformatted post tag request | Tag name shouldn't be empty"
-//	@Failure	401		{object}	nil					"Missing user in session"
-//	@Failure	500		{object}	nil					"Failed to delete tag name: %v"
-//	@Router		/api/v1/tag/delete [POST]
-func (s *APIV1Service) DeleteTag(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-
-	tagDelete := &DeleteTagRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
-	}
-	if tagDelete.Name == "" {
-		return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
-	}
-
-	err := s.Store.DeleteTag(ctx, &store.DeleteTag{
-		Name:      tagDelete.Name,
-		CreatorID: userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
-
-// GetTagSuggestion godoc
-//
-//	@Summary	Get a list of tags suggested from other memos contents
-//	@Tags		tag
-//	@Produce	json
-//	@Success	200	{object}	[]string	"Tag list"
-//	@Failure	400	{object}	nil			"Missing user session"
-//	@Failure	500	{object}	nil			"Failed to find memo list | Failed to find tag list"
-//	@Router		/api/v1/tag/suggestion [GET]
-func (s *APIV1Service) GetTagSuggestion(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
-	}
-	normalRowStatus := store.Normal
-	memoFind := &store.FindMemo{
-		CreatorID:     &userID,
-		ContentSearch: []string{"#"},
-		RowStatus:     &normalRowStatus,
-	}
-
-	memoMessageList, err := s.Store.ListMemos(ctx, memoFind)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
-	}
-
-	list, err := s.Store.ListTags(ctx, &store.FindTag{
-		CreatorID: userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
-	}
-	tagNameList := []string{}
-	for _, tag := range list {
-		tagNameList = append(tagNameList, tag.Name)
-	}
-
-	tagMapSet := make(map[string]bool)
-	for _, memo := range memoMessageList {
-		for _, tag := range findTagListFromMemoContent(memo.Content) {
-			if !slices.Contains(tagNameList, tag) {
-				tagMapSet[tag] = true
-			}
-		}
-	}
-	tagList := []string{}
-	for tag := range tagMapSet {
-		tagList = append(tagList, tag)
-	}
-	sort.Strings(tagList)
-	return c.JSON(http.StatusOK, tagList)
-}
-
-func convertTagFromStore(tag *store.Tag) *Tag {
-	return &Tag{
-		Name:      tag.Name,
-		CreatorID: tag.CreatorID,
-	}
-}
-
-var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)
-
-func findTagListFromMemoContent(memoContent string) []string {
-	tagMapSet := make(map[string]bool)
-	matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
-	for _, v := range matches {
-		tagName := v[1]
-		tagMapSet[tagName] = true
-	}
-
-	tagList := []string{}
-	for tag := range tagMapSet {
-		tagList = append(tagList, tag)
-	}
-	sort.Strings(tagList)
-	return tagList
-}
diff --git a/server/route/api/v1/tag_test.go b/server/route/api/v1/tag_test.go
deleted file mode 100644
index 10578aab3..000000000
--- a/server/route/api/v1/tag_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package v1
-
-import (
-	"testing"
-)
-
-func TestFindTagListFromMemoContent(t *testing.T) {
-	tests := []struct {
-		memoContent string
-		want        []string
-	}{
-		{
-			memoContent: "#tag1 ",
-			want:        []string{"tag1"},
-		},
-		{
-			memoContent: "#tag1 #tag2 ",
-			want:        []string{"tag1", "tag2"},
-		},
-		{
-			memoContent: "#tag1 #tag2 \n#tag3 ",
-			want:        []string{"tag1", "tag2", "tag3"},
-		},
-		{
-			memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
-			want:        []string{"tag1", "tag2", "tag3", "tag4"},
-		},
-		{
-			memoContent: "#tag1 #tag2 \n#tag3  #tag4 ",
-			want:        []string{"tag1", "tag2", "tag3", "tag4"},
-		},
-		{
-			memoContent: "#tag1 123123#tag2 \n#tag3  #tag4 ",
-			want:        []string{"tag1", "tag2", "tag3", "tag4"},
-		},
-		{
-			memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3  #tag4 http://123123.com?123123#tag2) ",
-			want:        []string{"tag1", "tag2", "tag2)", "tag3", "tag4"},
-		},
-	}
-	for _, test := range tests {
-		result := findTagListFromMemoContent(test.memoContent)
-		if len(result) != len(test.want) {
-			t.Errorf("Find tag list %s: got result %v, want %v.", test.memoContent, result, test.want)
-		}
-	}
-}
diff --git a/server/route/api/v1/user.go b/server/route/api/v1/user.go
deleted file mode 100644
index f613203cd..000000000
--- a/server/route/api/v1/user.go
+++ /dev/null
@@ -1,487 +0,0 @@
-package v1
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/labstack/echo/v4"
-	"github.com/pkg/errors"
-	"golang.org/x/crypto/bcrypt"
-
-	"github.com/usememos/memos/internal/util"
-	"github.com/usememos/memos/store"
-)
-
-// Role is the type of a role.
-type Role string
-
-const (
-	// RoleHost is the HOST role.
-	RoleHost Role = "HOST"
-	// RoleAdmin is the ADMIN role.
-	RoleAdmin Role = "ADMIN"
-	// RoleUser is the USER role.
-	RoleUser Role = "USER"
-)
-
-func (role Role) String() string {
-	return string(role)
-}
-
-type User struct {
-	ID int32 `json:"id"`
-
-	// Standard fields
-	RowStatus RowStatus `json:"rowStatus"`
-	CreatedTs int64     `json:"createdTs"`
-	UpdatedTs int64     `json:"updatedTs"`
-
-	// Domain specific fields
-	Username     string `json:"username"`
-	Role         Role   `json:"role"`
-	Email        string `json:"email"`
-	Nickname     string `json:"nickname"`
-	PasswordHash string `json:"-"`
-	AvatarURL    string `json:"avatarUrl"`
-}
-
-type CreateUserRequest struct {
-	Username string `json:"username"`
-	Role     Role   `json:"role"`
-	Email    string `json:"email"`
-	Nickname string `json:"nickname"`
-	Password string `json:"password"`
-}
-
-type UpdateUserRequest struct {
-	RowStatus *RowStatus `json:"rowStatus"`
-	Username  *string    `json:"username"`
-	Email     *string    `json:"email"`
-	Nickname  *string    `json:"nickname"`
-	Password  *string    `json:"password"`
-	AvatarURL *string    `json:"avatarUrl"`
-}
-
-func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
-	g.GET("/user", s.GetUserList)
-	g.POST("/user", s.CreateUser)
-	g.GET("/user/me", s.GetCurrentUser)
-	// NOTE: This should be moved to /api/v2/user/:username
-	g.GET("/user/name/:username", s.GetUserByUsername)
-	g.GET("/user/:id", s.GetUserByID)
-	g.PATCH("/user/:id", s.UpdateUser)
-	g.DELETE("/user/:id", s.DeleteUser)
-}
-
-// GetUserList godoc
-//
-//	@Summary	Get a list of users
-//	@Tags		user
-//	@Produce	json
-//	@Success	200	{object}	[]store.User	"User list"
-//	@Failure	500	{object}	nil				"Failed to fetch user list"
-//	@Router		/api/v1/user [GET]
-func (s *APIV1Service) GetUserList(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
-	}
-	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
-	}
-	if currentUser == nil {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
-	}
-	if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to list users")
-	}
-
-	list, err := s.Store.ListUsers(ctx, &store.FindUser{})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
-	}
-
-	userMessageList := make([]*User, 0, len(list))
-	for _, user := range list {
-		userMessage := convertUserFromStore(user)
-		// data desensitize
-		userMessage.Email = ""
-		userMessageList = append(userMessageList, userMessage)
-	}
-	return c.JSON(http.StatusOK, userMessageList)
-}
-
-// CreateUser godoc
-//
-//	@Summary	Create a user
-//	@Tags		user
-//	@Accept		json
-//	@Produce	json
-//	@Param		body	body		CreateUserRequest	true	"Request object"
-//	@Success	200		{object}	store.User			"Created user"
-//	@Failure	400		{object}	nil					"Malformatted post user request | Invalid user create format"
-//	@Failure	401		{object}	nil					"Missing auth session | Unauthorized to create user"
-//	@Failure	403		{object}	nil					"Could not create host user"
-//	@Failure	500		{object}	nil					"Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
-//	@Router		/api/v1/user [POST]
-func (s *APIV1Service) CreateUser(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
-	}
-	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &userID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
-	}
-	if currentUser == nil {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
-	}
-	if currentUser.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to create user")
-	}
-
-	userCreate := &CreateUserRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
-	}
-	if err := userCreate.Validate(); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
-	}
-	if !util.UIDMatcher.MatchString(strings.ToLower(userCreate.Username)) {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err)
-	}
-	// Disallow host user to be created.
-	if userCreate.Role == RoleHost {
-		return echo.NewHTTPError(http.StatusForbidden, "Could not create host user")
-	}
-
-	passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
-	}
-
-	user, err := s.Store.CreateUser(ctx, &store.User{
-		Username:     userCreate.Username,
-		Role:         store.Role(userCreate.Role),
-		Email:        userCreate.Email,
-		Nickname:     userCreate.Nickname,
-		PasswordHash: string(passwordHash),
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
-	}
-
-	userMessage := convertUserFromStore(user)
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-// GetCurrentUser godoc
-//
-//	@Summary	Get current user
-//	@Tags		user
-//	@Produce	json
-//	@Success	200	{object}	store.User	"Current user"
-//	@Failure	401	{object}	nil			"Missing auth session"
-//	@Failure	500	{object}	nil			"Failed to find user | Failed to find userSettingList"
-//	@Router		/api/v1/user/me [GET]
-func (s *APIV1Service) GetCurrentUser(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
-	}
-
-	userMessage := convertUserFromStore(user)
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-// GetUserByUsername godoc
-//
-//	@Summary	Get user by username
-//	@Tags		user
-//	@Produce	json
-//	@Param		username	path		string		true	"Username"
-//	@Success	200			{object}	store.User	"Requested user"
-//	@Failure	404			{object}	nil			"User not found"
-//	@Failure	500			{object}	nil			"Failed to find user"
-//	@Router		/api/v1/user/name/{username} [GET]
-func (s *APIV1Service) GetUserByUsername(c echo.Context) error {
-	ctx := c.Request().Context()
-	username := c.Param("username")
-	user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil {
-		return echo.NewHTTPError(http.StatusNotFound, "User not found")
-	}
-
-	userMessage := convertUserFromStore(user)
-	// data desensitize
-	userMessage.Email = ""
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-// GetUserByID godoc
-//
-//	@Summary	Get user by id
-//	@Tags		user
-//	@Produce	json
-//	@Param		id	path		int			true	"User ID"
-//	@Success	200	{object}	store.User	"Requested user"
-//	@Failure	400	{object}	nil			"Malformatted user id"
-//	@Failure	404	{object}	nil			"User not found"
-//	@Failure	500	{object}	nil			"Failed to find user"
-//	@Router		/api/v1/user/{id} [GET]
-func (s *APIV1Service) GetUserByID(c echo.Context) error {
-	ctx := c.Request().Context()
-	id, err := util.ConvertStringToInt32(c.Param("id"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
-	}
-
-	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &id})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if user == nil {
-		return echo.NewHTTPError(http.StatusNotFound, "User not found")
-	}
-
-	userMessage := convertUserFromStore(user)
-	userID, ok := c.Get(userIDContextKey).(int32)
-	if !ok || userID != user.ID {
-		// Data desensitize.
-		userMessage.Email = ""
-	}
-
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-// DeleteUser godoc
-//
-//	@Summary	Delete a user
-//	@Tags		user
-//	@Produce	json
-//	@Param		id	path		string	true	"User ID"
-//	@Success	200	{boolean}	true	"User deleted"
-//	@Failure	400	{object}	nil		"ID is not a number: %s | Current session user not found with ID: %d"
-//	@Failure	401	{object}	nil		"Missing user in session"
-//	@Failure	403	{object}	nil		"Unauthorized to delete user"
-//	@Failure	500	{object}	nil		"Failed to find user | Failed to delete user"
-//	@Router		/api/v1/user/{id} [DELETE]
-func (s *APIV1Service) DeleteUser(c echo.Context) error {
-	ctx := c.Request().Context()
-	currentUserID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
-		ID: &currentUserID,
-	})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if currentUser == nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
-	} else if currentUser.Role != store.RoleHost {
-		return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete user").SetInternal(err)
-	}
-
-	userID, err := util.ConvertStringToInt32(c.Param("id"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
-	}
-	if currentUserID == userID {
-		return echo.NewHTTPError(http.StatusBadRequest, "Cannot delete current user")
-	}
-
-	if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
-		ID: userID,
-	}); err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
-	}
-	return c.JSON(http.StatusOK, true)
-}
-
-// UpdateUser godoc
-//
-//	@Summary	Update a user
-//	@Tags		user
-//	@Produce	json
-//	@Param		id		path		string				true	"User ID"
-//	@Param		patch	body		UpdateUserRequest	true	"Patch request"
-//	@Success	200		{object}	store.User			"Updated user"
-//	@Failure	400		{object}	nil					"ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
-//	@Failure	401		{object}	nil					"Missing user in session"
-//	@Failure	403		{object}	nil					"Unauthorized to update user"
-//	@Failure	500		{object}	nil					"Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
-//	@Router		/api/v1/user/{id} [PATCH]
-func (s *APIV1Service) UpdateUser(c echo.Context) error {
-	ctx := c.Request().Context()
-	userID, err := util.ConvertStringToInt32(c.Param("id"))
-	if err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
-	}
-
-	currentUserID, ok := c.Get(userIDContextKey).(int32)
-	if !ok {
-		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
-	}
-	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &currentUserID})
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
-	}
-	if currentUser == nil {
-		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
-	} else if currentUser.Role != store.RoleHost && currentUserID != userID {
-		return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to update user").SetInternal(err)
-	}
-
-	request := &UpdateUserRequest{}
-	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
-	}
-	if err := request.Validate(); err != nil {
-		return echo.NewHTTPError(http.StatusBadRequest, "Invalid update user request").SetInternal(err)
-	}
-
-	currentTs := time.Now().Unix()
-	userUpdate := &store.UpdateUser{
-		ID:        userID,
-		UpdatedTs: &currentTs,
-	}
-	if request.RowStatus != nil {
-		rowStatus := store.RowStatus(request.RowStatus.String())
-		userUpdate.RowStatus = &rowStatus
-		if rowStatus == store.Archived && currentUserID == userID {
-			return echo.NewHTTPError(http.StatusBadRequest, "Cannot archive current user")
-		}
-	}
-	if request.Username != nil {
-		if !util.UIDMatcher.MatchString(strings.ToLower(*request.Username)) {
-			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err)
-		}
-		userUpdate.Username = request.Username
-	}
-	if request.Email != nil {
-		userUpdate.Email = request.Email
-	}
-	if request.Nickname != nil {
-		userUpdate.Nickname = request.Nickname
-	}
-	if request.Password != nil {
-		passwordHash, err := bcrypt.GenerateFromPassword([]byte(*request.Password), bcrypt.DefaultCost)
-		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
-		}
-
-		passwordHashStr := string(passwordHash)
-		userUpdate.PasswordHash = &passwordHashStr
-	}
-	if request.AvatarURL != nil {
-		userUpdate.AvatarURL = request.AvatarURL
-	}
-
-	user, err := s.Store.UpdateUser(ctx, userUpdate)
-	if err != nil {
-		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
-	}
-
-	userMessage := convertUserFromStore(user)
-	return c.JSON(http.StatusOK, userMessage)
-}
-
-func (create CreateUserRequest) Validate() error {
-	if len(create.Username) < 3 {
-		return errors.New("username is too short, minimum length is 3")
-	}
-	if len(create.Username) > 32 {
-		return errors.New("username is too long, maximum length is 32")
-	}
-	if len(create.Password) < 3 {
-		return errors.New("password is too short, minimum length is 3")
-	}
-	if len(create.Password) > 512 {
-		return errors.New("password is too long, maximum length is 512")
-	}
-	if len(create.Nickname) > 64 {
-		return errors.New("nickname is too long, maximum length is 64")
-	}
-	if create.Email != "" {
-		if len(create.Email) > 256 {
-			return errors.New("email is too long, maximum length is 256")
-		}
-		if !util.ValidateEmail(create.Email) {
-			return errors.New("invalid email format")
-		}
-	}
-
-	return nil
-}
-
-func (update UpdateUserRequest) Validate() error {
-	if update.Username != nil && len(*update.Username) < 3 {
-		return errors.New("username is too short, minimum length is 3")
-	}
-	if update.Username != nil && len(*update.Username) > 32 {
-		return errors.New("username is too long, maximum length is 32")
-	}
-	if update.Password != nil && len(*update.Password) < 3 {
-		return errors.New("password is too short, minimum length is 3")
-	}
-	if update.Password != nil && len(*update.Password) > 512 {
-		return errors.New("password is too long, maximum length is 512")
-	}
-	if update.Nickname != nil && len(*update.Nickname) > 64 {
-		return errors.New("nickname is too long, maximum length is 64")
-	}
-	if update.AvatarURL != nil {
-		if len(*update.AvatarURL) > 2<<20 {
-			return errors.New("avatar is too large, maximum is 2MB")
-		}
-	}
-	if update.Email != nil && *update.Email != "" {
-		if len(*update.Email) > 256 {
-			return errors.New("email is too long, maximum length is 256")
-		}
-		if !util.ValidateEmail(*update.Email) {
-			return errors.New("invalid email format")
-		}
-	}
-
-	return nil
-}
-
-func convertUserFromStore(user *store.User) *User {
-	return &User{
-		ID:           user.ID,
-		RowStatus:    RowStatus(user.RowStatus),
-		CreatedTs:    user.CreatedTs,
-		UpdatedTs:    user.UpdatedTs,
-		Username:     user.Username,
-		Role:         Role(user.Role),
-		Email:        user.Email,
-		Nickname:     user.Nickname,
-		PasswordHash: user.PasswordHash,
-		AvatarURL:    user.AvatarURL,
-	}
-}
diff --git a/server/route/api/v1/v1.go b/server/route/api/v1/v1.go
deleted file mode 100644
index cb2b1b75e..000000000
--- a/server/route/api/v1/v1.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package v1
-
-import (
-	"net/http"
-	"time"
-
-	"github.com/labstack/echo/v4"
-	"github.com/labstack/echo/v4/middleware"
-
-	"github.com/usememos/memos/plugin/telegram"
-	"github.com/usememos/memos/server/profile"
-	"github.com/usememos/memos/server/route/resource"
-	"github.com/usememos/memos/server/route/rss"
-	"github.com/usememos/memos/store"
-)
-
-type APIV1Service struct {
-	Secret      string
-	Profile     *profile.Profile
-	Store       *store.Store
-	telegramBot *telegram.Bot
-}
-
-// @title						memos API
-// @version					1.0
-// @description				A privacy-first, lightweight note-taking service.
-//
-// @contact.name				API Support
-// @contact.url				https://github.com/orgs/usememos/discussions
-//
-// @license.name				MIT License
-// @license.url				https://github.com/usememos/memos/blob/main/LICENSE
-//
-// @BasePath					/
-//
-// @externalDocs.url			https://usememos.com/
-// @externalDocs.description	Find out more about Memos.
-func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store, telegramBot *telegram.Bot) *APIV1Service {
-	return &APIV1Service{
-		Secret:      secret,
-		Profile:     profile,
-		Store:       store,
-		telegramBot: telegramBot,
-	}
-}
-
-func (s *APIV1Service) Register(rootGroup *echo.Group) {
-	// Register API v1 routes.
-	apiV1Group := rootGroup.Group("/api/v1")
-	apiV1Group.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
-		Store: middleware.NewRateLimiterMemoryStoreWithConfig(
-			middleware.RateLimiterMemoryStoreConfig{Rate: 30, Burst: 100, ExpiresIn: 3 * time.Minute},
-		),
-		IdentifierExtractor: func(ctx echo.Context) (string, error) {
-			id := ctx.RealIP()
-			return id, nil
-		},
-		ErrorHandler: func(context echo.Context, err error) error {
-			return context.JSON(http.StatusForbidden, nil)
-		},
-		DenyHandler: func(context echo.Context, identifier string, err error) error {
-			return context.JSON(http.StatusTooManyRequests, nil)
-		},
-	}))
-	apiV1Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
-		return JWTMiddleware(s, next, s.Secret)
-	})
-	s.registerSystemRoutes(apiV1Group)
-	s.registerSystemSettingRoutes(apiV1Group)
-	s.registerAuthRoutes(apiV1Group)
-	s.registerUserRoutes(apiV1Group)
-	s.registerTagRoutes(apiV1Group)
-	s.registerStorageRoutes(apiV1Group)
-	s.registerResourceRoutes(apiV1Group)
-	s.registerMemoRoutes(apiV1Group)
-	s.registerMemoOrganizerRoutes(apiV1Group)
-	s.registerMemoRelationRoutes(apiV1Group)
-
-	// Register public routes.
-	publicGroup := rootGroup.Group("/o")
-	publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
-		return JWTMiddleware(s, next, s.Secret)
-	})
-	s.registerGetterPublicRoutes(publicGroup)
-
-	// Create and register resource public routes.
-	resource.NewResourceService(s.Profile, s.Store).RegisterRoutes(publicGroup)
-
-	// Create and register rss public routes.
-	rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup)
-
-	// programmatically set API version same as the server version
-	SwaggerInfo.Version = s.Profile.Version
-}
diff --git a/server/route/api/v2/storage_service.go b/server/route/api/v2/storage_service.go
index 0f74330f2..894f83fb4 100644
--- a/server/route/api/v2/storage_service.go
+++ b/server/route/api/v2/storage_service.go
@@ -23,11 +23,11 @@ func (s *APIV2Service) CreateStorage(ctx context.Context, request *apiv2pb.Creat
 	storage, err := s.Store.CreateStorageV1(ctx, convertStorageToStore(request.Storage))
 	if err != nil {
 		return nil, status.Errorf(codes.Internal, "failed to create storage, error: %+v", err)
 	}
 	return &apiv2pb.CreateStorageResponse{
-		Storage: convertStorageFromStore(storage),
+		Storage: ConvertStorageFromStore(storage),
 	}, nil
 }
 
 func (s *APIV2Service) ListStorages(ctx context.Context, _ *apiv2pb.ListStoragesRequest) (*apiv2pb.ListStoragesResponse, error) {
 	storages, err := s.Store.ListStoragesV1(ctx, &store.FindStorage{})
@@ -37,11 +37,11 @@ func (s *APIV2Service) ListStorages(ctx context.Context, _ *apiv2pb.ListStorages
 
 	response := &apiv2pb.ListStoragesResponse{
 		Storages: []*apiv2pb.Storage{},
 	}
 	for _, storage := range storages {
-		response.Storages = append(response.Storages, convertStorageFromStore(storage))
+		response.Storages = append(response.Storages, ConvertStorageFromStore(storage))
 	}
 	return response, nil
 }
 
 func (s *APIV2Service) GetStorage(ctx context.Context, request *apiv2pb.GetStorageRequest) (*apiv2pb.GetStorageResponse, error) {
@@ -53,11 +53,11 @@ func (s *APIV2Service) GetStorage(ctx context.Context, request *apiv2pb.GetStora
 	}
 	if storage == nil {
 		return nil, status.Errorf(codes.NotFound, "storage not found")
 	}
 	return &apiv2pb.GetStorageResponse{
-		Storage: convertStorageFromStore(storage),
+		Storage: ConvertStorageFromStore(storage),
 	}, nil
 }
 
 func (s *APIV2Service) UpdateStorage(ctx context.Context, request *apiv2pb.UpdateStorageRequest) (*apiv2pb.UpdateStorageResponse, error) {
 	if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
@@ -80,11 +80,11 @@ func (s *APIV2Service) UpdateStorage(ctx context.Context, request *apiv2pb.Updat
 	storage, err := s.Store.UpdateStorageV1(ctx, update)
 	if err != nil {
 		return nil, status.Errorf(codes.Internal, "failed to update storage, error: %+v", err)
 	}
 	return &apiv2pb.UpdateStorageResponse{
-		Storage: convertStorageFromStore(storage),
+		Storage: ConvertStorageFromStore(storage),
 	}, nil
 }
 
 func (s *APIV2Service) DeleteStorage(ctx context.Context, request *apiv2pb.DeleteStorageRequest) (*apiv2pb.DeleteStorageResponse, error) {
 	err := s.Store.DeleteStorage(ctx, &store.DeleteStorage{
@@ -94,11 +94,11 @@ func (s *APIV2Service) DeleteStorage(ctx context.Context, request *apiv2pb.Delet
 		return nil, status.Errorf(codes.Internal, "failed to delete storage, error: %+v", err)
 	}
 	return &apiv2pb.DeleteStorageResponse{}, nil
 }
 
-func convertStorageFromStore(storage *storepb.Storage) *apiv2pb.Storage {
+func ConvertStorageFromStore(storage *storepb.Storage) *apiv2pb.Storage {
 	temp := &apiv2pb.Storage{
 		Id:    storage.Id,
 		Title: storage.Name,
 		Type:  apiv2pb.Storage_Type(apiv2pb.Storage_Type_value[storage.Type.String()]),
 	}
diff --git a/server/server.go b/server/server.go
index ff84028ef..3b0b941cd 100644
--- a/server/server.go
+++ b/server/server.go
@@ -13,11 +13,10 @@ import (
 
 	"github.com/usememos/memos/plugin/telegram"
 	storepb "github.com/usememos/memos/proto/gen/store"
 	"github.com/usememos/memos/server/integration"
 	"github.com/usememos/memos/server/profile"
-	apiv1 "github.com/usememos/memos/server/route/api/v1"
 	apiv2 "github.com/usememos/memos/server/route/api/v2"
 	"github.com/usememos/memos/server/route/frontend"
 	versionchecker "github.com/usememos/memos/server/service/version_checker"
 	"github.com/usememos/memos/store"
 )
@@ -73,15 +72,10 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
 	if profile.Frontend {
 		frontendService := frontend.NewFrontendService(profile, store)
 		frontendService.Serve(ctx, e)
 	}
 
-	// Register API v1 endpoints.
-	rootGroup := e.Group("")
-	apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store, s.telegramBot)
-	apiV1Service.Register(rootGroup)
-
 	apiV2Service := apiv2.NewAPIV2Service(s.Secret, profile, store, s.Profile.Port+1)
 	// Register gRPC gateway as api v2.
 	if err := apiV2Service.RegisterGateway(ctx, e); err != nil {
 		return nil, errors.Wrap(err, "failed to register gRPC gateway")
 	}

Vulnerable code: 	"encoding/json"
	apiv1 "github.com/usememos/memos/server/route/api/v1"
	systemSettingStorageServiceID, err := dataStore.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: apiv1.SystemSettingStorageServiceIDName.String()})
		return nil, errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")

	storageServiceID := apiv1.DefaultStorage
	if systemSettingStorageServiceID != nil {
		err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
		if err != nil {
			return nil, errors.Wrap(err, "Failed to unmarshal storage service id")
		}
	storage, err := dataStore.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
		return nil, errors.Wrap(err, "Failed to find StorageServiceID")

		return nil, nil // storage not configured - not an error, just return empty ref
	storageMessage, err := apiv1.ConvertStorageFromStore(storage)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to ConvertStorageFromStore")
	}
	if storageMessage.Type != apiv1.StorageS3 {
	s3Config := storageMessage.Config.S3Config
		URLPrefix: s3Config.URLPrefix,
		URLSuffix: s3Config.URLSuffix,
	"bytes"
	apiv1 "github.com/usememos/memos/server/route/api/v1"
		err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data))
package v1

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/pkg/errors"
	"golang.org/x/crypto/bcrypt"

	"github.com/usememos/memos/internal/util"
	storepb "github.com/usememos/memos/proto/gen/store"
	"github.com/usememos/memos/server/route/api/auth"
	"github.com/usememos/memos/store"
)

type SignIn struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Remember bool   `json:"remember"`
}

type SSOSignIn struct {
	IdentityProviderID int32  `json:"identityProviderId"`
	Code               string `json:"code"`
	RedirectURI        string `json:"redirectUri"`
}

type SignUp struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
	g.POST("/auth/signin", s.SignIn)
	g.POST("/auth/signout", s.SignOut)
	g.POST("/auth/signup", s.SignUp)
}

// SignIn godoc
//
//	@Summary	Sign-in to memos.
//	@Tags		auth
//	@Accept		json
//	@Produce	json
//	@Param		body	body		SignIn		true	"Sign-in object"
//	@Success	200		{object}	store.User	"User information"
//	@Failure	400		{object}	nil			"Malformatted signin request"
//	@Failure	401		{object}	nil			"Password login is deactivated | Incorrect login credentials, please try again"
//	@Failure	403		{object}	nil			"User has been archived with username %s"
//	@Failure	500		{object}	nil			"Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
//	@Router		/api/v1/auth/signin [POST]
func (s *APIV1Service) SignIn(c echo.Context) error {
	ctx := c.Request().Context()
	workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
	}
	if workspaceGeneralSetting.DisallowPasswordLogin {
		return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
	}

	signin := &SignIn{}
	if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		Username: &signin.Username,
	})
	if err != nil {
		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 == store.Archived {
		return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
	}

	// Compare the stored hashed password, with the hashed version of the password that was received.
	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
		// If the two passwords don't match, return a 401 status.
		return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
	}

	var expireAt time.Time
	// Set cookie expiration to 100 years to make it persistent.
	cookieExp := time.Now().AddDate(100, 0, 0)
	if !signin.Remember {
		expireAt = time.Now().Add(auth.AccessTokenDuration)
		cookieExp = time.Now().Add(auth.CookieExpDuration)
	}

	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
	}
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
	}
	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
	userMessage := convertUserFromStore(user)
	return c.JSON(http.StatusOK, userMessage)
}

// SignOut godoc
//
//	@Summary	Sign-out from memos.
//	@Tags		auth
//	@Produce	json
//	@Success	200	{boolean}	true	"Sign-out success"
//	@Router		/api/v1/auth/signout [POST]
func (s *APIV1Service) SignOut(c echo.Context) error {
	accessToken := findAccessToken(c)
	userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)

	err := removeAccessTokenAndCookies(c, s.Store, userID, accessToken)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to remove access token, err: %s", err)).SetInternal(err)
	}

	return c.JSON(http.StatusOK, true)
}

// SignUp godoc
//
//	@Summary	Sign-up to memos.
//	@Tags		auth
//	@Accept		json
//	@Produce	json
//	@Param		body	body		SignUp		true	"Sign-up object"
//	@Success	200		{object}	store.User	"User information"
//	@Failure	400		{object}	nil			"Malformatted signup request | Failed to find users"
//	@Failure	401		{object}	nil			"signup is disabled"
//	@Failure	403		{object}	nil			"Forbidden"
//	@Failure	404		{object}	nil			"Not found"
//	@Failure	500		{object}	nil			"Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
//	@Router		/api/v1/auth/signup [POST]
func (s *APIV1Service) SignUp(c echo.Context) error {
	ctx := c.Request().Context()
	signup := &SignUp{}
	if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
	}

	hostUserType := store.RoleHost
	existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
		Role: &hostUserType,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
	}
	if !util.UIDMatcher.MatchString(strings.ToLower(signup.Username)) {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
	}

	userCreate := &store.User{
		Username: signup.Username,
		// The new signup user should be normal user by default.
		Role:     store.RoleUser,
		Nickname: signup.Username,
	}
	if len(existedHostUsers) == 0 {
		// Change the default role to host if there is no host user.
		userCreate.Role = store.RoleHost
	} else {
		workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
		}
		if workspaceGeneralSetting.DisallowSignup {
			return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
		}
		if workspaceGeneralSetting.DisallowPasswordLogin {
			return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").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)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
	}
	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
	}
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
	}
	cookieExp := time.Now().Add(auth.CookieExpDuration)
	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
	userMessage := convertUserFromStore(user)
	return c.JSON(http.StatusOK, userMessage)
}

func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
	userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
	if err != nil {
		return errors.Wrap(err, "failed to get user access tokens")
	}
	userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
		AccessToken: accessToken,
		Description: "Account sign in",
	}
	userAccessTokens = append(userAccessTokens, &userAccessToken)
	if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
		UserId: user.ID,
		Key:    storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
		Value: &storepb.UserSetting_AccessTokens{
			AccessTokens: &storepb.AccessTokensUserSetting{
				AccessTokens: userAccessTokens,
			},
		},
	}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
	}
	return nil
}

// removeAccessTokenAndCookies removes the jwt token from the cookies.
func removeAccessTokenAndCookies(c echo.Context, s *store.Store, userID int32, token string) error {
	err := s.RemoveUserAccessToken(c.Request().Context(), userID, token)
	if err != nil {
		return err
	}

	cookieExp := time.Now().Add(-1 * time.Hour)
	setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
	return nil
}

// setTokenCookie sets the token to the cookie.
func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
	cookie := new(http.Cookie)
	cookie.Name = name
	cookie.Value = token
	cookie.Expires = expiration
	cookie.Path = "/"
	// Http-only helps mitigate the risk of client side script accessing the protected cookie.
	cookie.HttpOnly = true
	cookie.SameSite = http.SameSiteStrictMode
	c.SetCookie(cookie)
}
package v1

// RowStatus is the status for a row.
type RowStatus string

const (
	// Normal is the status for a normal row.
	Normal RowStatus = "NORMAL"
	// Archived is the status for an archived row.
	Archived RowStatus = "ARCHIVED"
)

func (r RowStatus) String() string {
	return string(r)
}
// Code generated by swaggo/swag. DO NOT EDIT.

package v1

import "github.com/swaggo/swag"

const docTemplate = `{
    "schemes": {{ marshal .Schemes }},
    "swagger": "2.0",
    "info": {
        "description": "{{escape .Description}}",
        "title": "{{.Title}}",
        "contact": {
            "name": "API Support",
            "url": "https://github.com/orgs/usememos/discussions"
        },
        "license": {
            "name": "MIT License",
            "url": "https://github.com/usememos/memos/blob/main/LICENSE"
        },
        "version": "{{.Version}}"
    },
    "host": "{{.Host}}",
    "basePath": "{{.BasePath}}",
    "paths": {
        "/api/v1/auth/signin": {
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "auth"
                ],
                "summary": "Sign-in to memos.",
                "parameters": [
                    {
                        "description": "Sign-in object",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.SignIn"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "User information",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "400": {
                        "description": "Malformatted signin request"
                    },
                    "401": {
                        "description": "Password login is deactivated | Incorrect login credentials, please try again"
                    },
                    "403": {
                        "description": "User has been archived with username %s"
                    },
                    "500": {
                        "description": "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/auth/signin/sso": {
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "auth"
                ],
                "summary": "Sign-in to memos using SSO.",
                "parameters": [
                    {
                        "description": "SSO sign-in object",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.SSOSignIn"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "User information",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "400": {
                        "description": "Malformatted signin request"
                    },
                    "401": {
                        "description": "Access denied, identifier does not match the filter."
                    },
                    "403": {
                        "description": "User has been archived with username {username}"
                    },
                    "404": {
                        "description": "Identity provider not found"
                    },
                    "500": {
                        "description": "Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/auth/signout": {
            "post": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "auth"
                ],
                "summary": "Sign-out from memos.",
                "responses": {
                    "200": {
                        "description": "Sign-out success",
                        "schema": {
                            "type": "boolean"
                        }
                    }
                }
            }
        },
        "/api/v1/auth/signup": {
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "auth"
                ],
                "summary": "Sign-up to memos.",
                "parameters": [
                    {
                        "description": "Sign-up object",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.SignUp"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "User information",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "400": {
                        "description": "Malformatted signup request | Failed to find users"
                    },
                    "401": {
                        "description": "signup is disabled"
                    },
                    "403": {
                        "description": "Forbidden"
                    },
                    "404": {
                        "description": "Not found"
                    },
                    "500": {
                        "description": "Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/idp": {
            "get": {
                "description": "*clientSecret is only available for host user",
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "idp"
                ],
                "summary": "Get a list of identity providers",
                "responses": {
                    "200": {
                        "description": "List of available identity providers",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/api_v1.IdentityProvider"
                            }
                        }
                    },
                    "500": {
                        "description": "Failed to find identity provider list | Failed to find user"
                    }
                }
            },
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "idp"
                ],
                "summary": "Create Identity Provider",
                "parameters": [
                    {
                        "description": "Identity provider information",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.CreateIdentityProviderRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Identity provider information",
                        "schema": {
                            "$ref": "#/definitions/store.IdentityProvider"
                        }
                    },
                    "400": {
                        "description": "Malformatted post identity provider request"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to create identity provider"
                    }
                }
            }
        },
        "/api/v1/idp/{idpId}": {
            "get": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "idp"
                ],
                "summary": "Get an identity provider by ID",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Identity provider ID",
                        "name": "idpId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Requested identity provider",
                        "schema": {
                            "$ref": "#/definitions/store.IdentityProvider"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "404": {
                        "description": "Identity provider not found"
                    },
                    "500": {
                        "description": "Failed to find identity provider list | Failed to find user"
                    }
                }
            },
            "delete": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "idp"
                ],
                "summary": "Delete an identity provider by ID",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Identity Provider ID",
                        "name": "idpId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Identity Provider deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted patch identity provider request"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to patch identity provider"
                    }
                }
            },
            "patch": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "idp"
                ],
                "summary": "Update an identity provider by ID",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Identity Provider ID",
                        "name": "idpId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Patched identity provider information",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.UpdateIdentityProviderRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Patched identity provider",
                        "schema": {
                            "$ref": "#/definitions/store.IdentityProvider"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted patch identity provider request"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to patch identity provider"
                    }
                }
            }
        },
        "/api/v1/memo": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Get a list of memos matching optional filters",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Creator ID",
                        "name": "creatorId",
                        "in": "query"
                    },
                    {
                        "type": "string",
                        "description": "Creator username",
                        "name": "creatorUsername",
                        "in": "query"
                    },
                    {
                        "enum": [
                            "NORMAL",
                            "ARCHIVED"
                        ],
                        "type": "string",
                        "description": "Row status",
                        "name": "rowStatus",
                        "in": "query"
                    },
                    {
                        "type": "boolean",
                        "description": "Pinned",
                        "name": "pinned",
                        "in": "query"
                    },
                    {
                        "type": "string",
                        "description": "Search for tag. Do not append #",
                        "name": "tag",
                        "in": "query"
                    },
                    {
                        "type": "string",
                        "description": "Search for content",
                        "name": "content",
                        "in": "query"
                    },
                    {
                        "type": "integer",
                        "description": "Limit",
                        "name": "limit",
                        "in": "query"
                    },
                    {
                        "type": "integer",
                        "description": "Offset",
                        "name": "offset",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.Memo"
                            }
                        }
                    },
                    "400": {
                        "description": "Missing user to find memo"
                    },
                    "500": {
                        "description": "Failed to get memo display with updated ts setting value | Failed to fetch memo list | Failed to compose memo response"
                    }
                }
            },
            "post": {
                "description": "Visibility can be PUBLIC, PROTECTED or PRIVATE\n*You should omit fields to use their default values",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Create a memo",
                "parameters": [
                    {
                        "description": "Request object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateMemoRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Stored memo",
                        "schema": {
                            "$ref": "#/definitions/store.Memo"
                        }
                    },
                    "400": {
                        "description": "Malformatted post memo request | Content size overflow, up to 1MB"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "404": {
                        "description": "User not found | Memo not found: %d"
                    },
                    "500": {
                        "description": "Failed to find user setting | Failed to unmarshal user setting value | Failed to find system setting | Failed to unmarshal system setting | Failed to find user | Failed to create memo | Failed to create activity | Failed to upsert memo resource | Failed to upsert memo relation | Failed to compose memo | Failed to compose memo response"
                    }
                }
            }
        },
        "/api/v1/memo/all": {
            "get": {
                "description": "This should also list protected memos if the user is logged in\nAuthentication is optional",
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Get a list of public memos matching optional filters",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Limit",
                        "name": "limit",
                        "in": "query"
                    },
                    {
                        "type": "integer",
                        "description": "Offset",
                        "name": "offset",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.Memo"
                            }
                        }
                    },
                    "500": {
                        "description": "Failed to get memo display with updated ts setting value | Failed to fetch all memo list | Failed to compose memo response"
                    }
                }
            }
        },
        "/api/v1/memo/stats": {
            "get": {
                "description": "Used to generate the heatmap",
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Get memo stats by creator ID or username",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Creator ID",
                        "name": "creatorId",
                        "in": "query"
                    },
                    {
                        "type": "string",
                        "description": "Creator username",
                        "name": "creatorUsername",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo createdTs list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "integer"
                            }
                        }
                    },
                    "400": {
                        "description": "Missing user id to find memo"
                    },
                    "500": {
                        "description": "Failed to get memo display with updated ts setting value | Failed to find memo list | Failed to compose memo response"
                    }
                }
            }
        },
        "/api/v1/memo/{memoId}": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Get memo by ID",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Memo ID",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.Memo"
                            }
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "403": {
                        "description": "this memo is private only | this memo is protected, missing user in session"
                    },
                    "404": {
                        "description": "Memo not found: %d"
                    },
                    "500": {
                        "description": "Failed to find memo by ID: %v | Failed to compose memo response"
                    }
                }
            },
            "delete": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Delete memo by ID",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Memo ID to delete",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "404": {
                        "description": "Memo not found: %d"
                    },
                    "500": {
                        "description": "Failed to find memo | Failed to delete memo ID: %v"
                    }
                }
            },
            "patch": {
                "description": "Visibility can be PUBLIC, PROTECTED or PRIVATE\n*You should omit fields to use their default values",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo"
                ],
                "summary": "Update a memo",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "ID of memo to update",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Patched object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.PatchMemoRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Stored memo",
                        "schema": {
                            "$ref": "#/definitions/store.Memo"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted patch memo request | Content size overflow, up to 1MB"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "404": {
                        "description": "Memo not found: %d"
                    },
                    "500": {
                        "description": "Failed to find memo | Failed to patch memo | Failed to upsert memo resource | Failed to delete memo resource | Failed to compose memo response"
                    }
                }
            }
        },
        "/api/v1/memo/{memoId}/organizer": {
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo-organizer"
                ],
                "summary": "Organize memo (pin/unpin)",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "ID of memo to organize",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Memo organizer object",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo information",
                        "schema": {
                            "$ref": "#/definitions/store.Memo"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted post memo organizer request"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "404": {
                        "description": "Memo not found: %v"
                    },
                    "500": {
                        "description": "Failed to find memo | Failed to upsert memo organizer | Failed to find memo by ID: %v | Failed to compose memo response"
                    }
                }
            }
        },
        "/api/v1/memo/{memoId}/relation": {
            "get": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo-relation"
                ],
                "summary": "Get a list of Memo Relations",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "ID of memo to find relations",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo relation information list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.MemoRelation"
                            }
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s"
                    },
                    "500": {
                        "description": "Failed to list memo relations"
                    }
                }
            },
            "post": {
                "description": "Create a relation between two memos",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo-relation"
                ],
                "summary": "Create Memo Relation",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "ID of memo to relate",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Memo relation object",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo relation information",
                        "schema": {
                            "$ref": "#/definitions/store.MemoRelation"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted post memo relation request"
                    },
                    "500": {
                        "description": "Failed to upsert memo relation"
                    }
                }
            }
        },
        "/api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType}": {
            "delete": {
                "description": "Removes a relation between two memos",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "memo-relation"
                ],
                "summary": "Delete a Memo Relation",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "ID of memo to find relations",
                        "name": "memoId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "type": "integer",
                        "description": "ID of memo to remove relation to",
                        "name": "relatedMemoId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "enum": [
                            "REFERENCE",
                            "COMMENT"
                        ],
                        "type": "string",
                        "description": "Type of relation to remove",
                        "name": "relationType",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Memo relation deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "Memo ID is not a number: %s | Related memo ID is not a number: %s"
                    },
                    "500": {
                        "description": "Failed to delete memo relation"
                    }
                }
            }
        },
        "/api/v1/ping": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "system"
                ],
                "summary": "Ping the system",
                "responses": {
                    "200": {
                        "description": "If succeed to ping the system",
                        "schema": {
                            "type": "boolean"
                        }
                    }
                }
            }
        },
        "/api/v1/resource": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "resource"
                ],
                "summary": "Get a list of resources",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Limit",
                        "name": "limit",
                        "in": "query"
                    },
                    {
                        "type": "integer",
                        "description": "Offset",
                        "name": "offset",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Resource list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.Resource"
                            }
                        }
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "500": {
                        "description": "Failed to fetch resource list"
                    }
                }
            },
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "resource"
                ],
                "summary": "Create resource",
                "parameters": [
                    {
                        "description": "Request object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.CreateResourceRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Created resource",
                        "schema": {
                            "$ref": "#/definitions/store.Resource"
                        }
                    },
                    "400": {
                        "description": "Malformatted post resource request | Invalid external link | Invalid external link scheme | Failed to request %s | Failed to read %s | Failed to read mime from %s"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "500": {
                        "description": "Failed to save resource | Failed to create resource | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/resource/blob": {
            "post": {
                "consumes": [
                    "multipart/form-data"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "resource"
                ],
                "summary": "Upload resource",
                "parameters": [
                    {
                        "type": "file",
                        "description": "File to upload",
                        "name": "file",
                        "in": "formData",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Created resource",
                        "schema": {
                            "$ref": "#/definitions/store.Resource"
                        }
                    },
                    "400": {
                        "description": "Upload file not found | File size exceeds allowed limit of %d MiB | Failed to parse upload data"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "500": {
                        "description": "Failed to get uploading file | Failed to open file | Failed to save resource | Failed to create resource | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/resource/{resourceId}": {
            "delete": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "resource"
                ],
                "summary": "Delete a resource",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Resource ID",
                        "name": "resourceId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Resource deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "404": {
                        "description": "Resource not found: %d"
                    },
                    "500": {
                        "description": "Failed to find resource | Failed to delete resource"
                    }
                }
            },
            "patch": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "resource"
                ],
                "summary": "Update a resource",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Resource ID",
                        "name": "resourceId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Patch resource request",
                        "name": "patch",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.UpdateResourceRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Updated resource",
                        "schema": {
                            "$ref": "#/definitions/store.Resource"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted patch resource request"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "404": {
                        "description": "Resource not found: %d"
                    },
                    "500": {
                        "description": "Failed to find resource | Failed to patch resource"
                    }
                }
            }
        },
        "/api/v1/status": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "system"
                ],
                "summary": "Get system GetSystemStatus",
                "responses": {
                    "200": {
                        "description": "System GetSystemStatus",
                        "schema": {
                            "$ref": "#/definitions/api_v1.SystemStatus"
                        }
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find host user | Failed to find system setting list | Failed to unmarshal system setting customized profile value"
                    }
                }
            }
        },
        "/api/v1/storage": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "storage"
                ],
                "summary": "Get a list of storages",
                "responses": {
                    "200": {
                        "description": "List of storages",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.Storage"
                            }
                        }
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to convert storage"
                    }
                }
            },
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "storage"
                ],
                "summary": "Create storage",
                "parameters": [
                    {
                        "description": "Request object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.CreateStorageRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Created storage",
                        "schema": {
                            "$ref": "#/definitions/store.Storage"
                        }
                    },
                    "400": {
                        "description": "Malformatted post storage request"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to create storage | Failed to convert storage"
                    }
                }
            }
        },
        "/api/v1/storage/{storageId}": {
            "delete": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "storage"
                ],
                "summary": "Delete a storage",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Storage ID",
                        "name": "storageId",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Storage deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Storage service %d is using"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
                    }
                }
            },
            "patch": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "storage"
                ],
                "summary": "Update a storage",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "Storage ID",
                        "name": "storageId",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Patch request",
                        "name": "patch",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpdateStorageRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Updated resource",
                        "schema": {
                            "$ref": "#/definitions/store.Storage"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to patch storage | Failed to convert storage"
                    }
                }
            }
        },
        "/api/v1/system/setting": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "system-setting"
                ],
                "summary": "Get a list of system settings",
                "responses": {
                    "200": {
                        "description": "System setting list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/api_v1.SystemSetting"
                            }
                        }
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to find system setting list"
                    }
                }
            },
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "system-setting"
                ],
                "summary": "Create system setting",
                "parameters": [
                    {
                        "description": "Request object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.UpsertSystemSettingRequest"
                        }
                    }
                ],
                "responses": {
                    "400": {
                        "description": "Malformatted post system setting request | invalid system setting"
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "403": {
                        "description": "Cannot disable passwords if no SSO identity provider is configured."
                    },
                    "500": {
                        "description": "Failed to find user | Failed to upsert system setting"
                    }
                }
            }
        },
        "/api/v1/system/vacuum": {
            "post": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "system"
                ],
                "summary": "Vacuum the database",
                "responses": {
                    "200": {
                        "description": "Database vacuumed",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "401": {
                        "description": "Missing user in session | Unauthorized"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to ExecVacuum database"
                    }
                }
            }
        },
        "/api/v1/tag": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "tag"
                ],
                "summary": "Get a list of tags",
                "responses": {
                    "200": {
                        "description": "Tag list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "string"
                            }
                        }
                    },
                    "400": {
                        "description": "Missing user id to find tag"
                    },
                    "500": {
                        "description": "Failed to find tag list"
                    }
                }
            },
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "tag"
                ],
                "summary": "Create a tag",
                "parameters": [
                    {
                        "description": "Request object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertTagRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Created tag name",
                        "schema": {
                            "type": "string"
                        }
                    },
                    "400": {
                        "description": "Malformatted post tag request | Tag name shouldn't be empty"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "500": {
                        "description": "Failed to upsert tag | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/tag/delete": {
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "tag"
                ],
                "summary": "Delete a tag",
                "parameters": [
                    {
                        "description": "Request object.",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.DeleteTagRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Tag deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "Malformatted post tag request | Tag name shouldn't be empty"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "500": {
                        "description": "Failed to delete tag name: %v"
                    }
                }
            }
        },
        "/api/v1/tag/suggestion": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "tag"
                ],
                "summary": "Get a list of tags suggested from other memos contents",
                "responses": {
                    "200": {
                        "description": "Tag list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "string"
                            }
                        }
                    },
                    "400": {
                        "description": "Missing user session"
                    },
                    "500": {
                        "description": "Failed to find memo list | Failed to find tag list"
                    }
                }
            }
        },
        "/api/v1/user": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Get a list of users",
                "responses": {
                    "200": {
                        "description": "User list",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/store.User"
                            }
                        }
                    },
                    "500": {
                        "description": "Failed to fetch user list"
                    }
                }
            },
            "post": {
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Create a user",
                "parameters": [
                    {
                        "description": "Request object",
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.CreateUserRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Created user",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "400": {
                        "description": "Malformatted post user request | Invalid user create format"
                    },
                    "401": {
                        "description": "Missing auth session | Unauthorized to create user"
                    },
                    "403": {
                        "description": "Could not create host user"
                    },
                    "500": {
                        "description": "Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
                    }
                }
            }
        },
        "/api/v1/user/me": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Get current user",
                "responses": {
                    "200": {
                        "description": "Current user",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "401": {
                        "description": "Missing auth session"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to find userSettingList"
                    }
                }
            }
        },
        "/api/v1/user/name/{username}": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Get user by username",
                "parameters": [
                    {
                        "type": "string",
                        "description": "Username",
                        "name": "username",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Requested user",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "404": {
                        "description": "User not found"
                    },
                    "500": {
                        "description": "Failed to find user"
                    }
                }
            }
        },
        "/api/v1/user/{id}": {
            "get": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Get user by id",
                "parameters": [
                    {
                        "type": "integer",
                        "description": "User ID",
                        "name": "id",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Requested user",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "400": {
                        "description": "Malformatted user id"
                    },
                    "404": {
                        "description": "User not found"
                    },
                    "500": {
                        "description": "Failed to find user"
                    }
                }
            },
            "delete": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Delete a user",
                "parameters": [
                    {
                        "type": "string",
                        "description": "User ID",
                        "name": "id",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "User deleted",
                        "schema": {
                            "type": "boolean"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Current session user not found with ID: %d"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "403": {
                        "description": "Unauthorized to delete user"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to delete user"
                    }
                }
            },
            "patch": {
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "user"
                ],
                "summary": "Update a user",
                "parameters": [
                    {
                        "type": "string",
                        "description": "User ID",
                        "name": "id",
                        "in": "path",
                        "required": true
                    },
                    {
                        "description": "Patch request",
                        "name": "patch",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api_v1.UpdateUserRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Updated user",
                        "schema": {
                            "$ref": "#/definitions/store.User"
                        }
                    },
                    "400": {
                        "description": "ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
                    },
                    "401": {
                        "description": "Missing user in session"
                    },
                    "403": {
                        "description": "Unauthorized to update user"
                    },
                    "500": {
                        "description": "Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
                    }
                }
            }
        },
        "/o/get/GetImage": {
            "get": {
                "produces": [
                    "GetImage/*"
                ],
                "tags": [
                    "image-url"
                ],
                "summary": "Get GetImage from URL",
                "parameters": [
                    {
                        "type": "string",
                        "description": "Image url",
                        "name": "url",
                        "in": "query",
                        "required": true
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Image"
                    },
                    "400": {
                        "description": "Missing GetImage url | Wrong url | Failed to get GetImage url: %s"
                    },
                    "500": {
                        "description": "Failed to write GetImage blob"
                    }
                }
            }
        }
    },
    "definitions": {
        "api_v1.CreateIdentityProviderRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/api_v1.IdentityProviderConfig"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/api_v1.IdentityProviderType"
                }
            }
        },
        "api_v1.CreateMemoRequest": {
            "type": "object",
            "properties": {
                "content": {
                    "type": "string"
                },
                "createdTs": {
                    "type": "integer"
                },
                "relationList": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/api_v1.UpsertMemoRelationRequest"
                    }
                },
                "resourceIdList": {
                    "description": "Related fields",
                    "type": "array",
                    "items": {
                        "type": "integer"
                    }
                },
                "visibility": {
                    "description": "Domain specific fields",
                    "allOf": [
                        {
                            "$ref": "#/definitions/api_v1.Visibility"
                        }
                    ]
                }
            }
        },
        "api_v1.CreateResourceRequest": {
            "type": "object",
            "properties": {
                "externalLink": {
                    "type": "string"
                },
                "filename": {
                    "type": "string"
                },
                "type": {
                    "type": "string"
                }
            }
        },
        "api_v1.CreateStorageRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/api_v1.StorageConfig"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/api_v1.StorageType"
                }
            }
        },
        "api_v1.CreateUserRequest": {
            "type": "object",
            "properties": {
                "email": {
                    "type": "string"
                },
                "nickname": {
                    "type": "string"
                },
                "password": {
                    "type": "string"
                },
                "role": {
                    "$ref": "#/definitions/api_v1.Role"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "api_v1.CustomizedProfile": {
            "type": "object",
            "properties": {
                "appearance": {
                    "description": "Appearance is the server default appearance.",
                    "type": "string"
                },
                "description": {
                    "description": "Description is the server description.",
                    "type": "string"
                },
                "locale": {
                    "description": "Locale is the server default locale.",
                    "type": "string"
                },
                "logoUrl": {
                    "description": "LogoURL is the url of logo image.",
                    "type": "string"
                },
                "name": {
                    "description": "Name is the server name, default is ` + "`" + `memos` + "`" + `",
                    "type": "string"
                }
            }
        },
        "api_v1.DeleteTagRequest": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "api_v1.FieldMapping": {
            "type": "object",
            "properties": {
                "displayName": {
                    "type": "string"
                },
                "email": {
                    "type": "string"
                },
                "identifier": {
                    "type": "string"
                }
            }
        },
        "api_v1.IdentityProvider": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/api_v1.IdentityProviderConfig"
                },
                "id": {
                    "type": "integer"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/api_v1.IdentityProviderType"
                }
            }
        },
        "api_v1.IdentityProviderConfig": {
            "type": "object",
            "properties": {
                "oauth2Config": {
                    "$ref": "#/definitions/api_v1.IdentityProviderOAuth2Config"
                }
            }
        },
        "api_v1.IdentityProviderOAuth2Config": {
            "type": "object",
            "properties": {
                "authUrl": {
                    "type": "string"
                },
                "clientId": {
                    "type": "string"
                },
                "clientSecret": {
                    "type": "string"
                },
                "fieldMapping": {
                    "$ref": "#/definitions/api_v1.FieldMapping"
                },
                "scopes": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "tokenUrl": {
                    "type": "string"
                },
                "userInfoUrl": {
                    "type": "string"
                }
            }
        },
        "api_v1.IdentityProviderType": {
            "type": "string",
            "enum": [
                "OAUTH2"
            ],
            "x-enum-varnames": [
                "IdentityProviderOAuth2Type"
            ]
        },
        "api_v1.MemoRelationType": {
            "type": "string",
            "enum": [
                "REFERENCE",
                "COMMENT"
            ],
            "x-enum-varnames": [
                "MemoRelationReference",
                "MemoRelationComment"
            ]
        },
        "api_v1.PatchMemoRequest": {
            "type": "object",
            "properties": {
                "content": {
                    "description": "Domain specific fields",
                    "type": "string"
                },
                "createdTs": {
                    "description": "Standard fields",
                    "type": "integer"
                },
                "relationList": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/api_v1.UpsertMemoRelationRequest"
                    }
                },
                "resourceIdList": {
                    "description": "Related fields",
                    "type": "array",
                    "items": {
                        "type": "integer"
                    }
                },
                "rowStatus": {
                    "$ref": "#/definitions/api_v1.RowStatus"
                },
                "updatedTs": {
                    "type": "integer"
                },
                "visibility": {
                    "$ref": "#/definitions/api_v1.Visibility"
                }
            }
        },
        "api_v1.Role": {
            "type": "string",
            "enum": [
                "HOST",
                "ADMIN",
                "USER"
            ],
            "x-enum-varnames": [
                "RoleHost",
                "RoleAdmin",
                "RoleUser"
            ]
        },
        "api_v1.RowStatus": {
            "type": "string",
            "enum": [
                "NORMAL",
                "ARCHIVED"
            ],
            "x-enum-varnames": [
                "Normal",
                "Archived"
            ]
        },
        "api_v1.SSOSignIn": {
            "type": "object",
            "properties": {
                "code": {
                    "type": "string"
                },
                "identityProviderId": {
                    "type": "integer"
                },
                "redirectUri": {
                    "type": "string"
                }
            }
        },
        "api_v1.SignIn": {
            "type": "object",
            "properties": {
                "password": {
                    "type": "string"
                },
                "remember": {
                    "type": "boolean"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "api_v1.SignUp": {
            "type": "object",
            "properties": {
                "password": {
                    "type": "string"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "api_v1.StorageConfig": {
            "type": "object",
            "properties": {
                "s3Config": {
                    "$ref": "#/definitions/api_v1.StorageS3Config"
                }
            }
        },
        "api_v1.StorageS3Config": {
            "type": "object",
            "properties": {
                "accessKey": {
                    "type": "string"
                },
                "bucket": {
                    "type": "string"
                },
                "endPoint": {
                    "type": "string"
                },
                "path": {
                    "type": "string"
                },
                "presign": {
                    "type": "boolean"
                },
                "region": {
                    "type": "string"
                },
                "secretKey": {
                    "type": "string"
                },
                "urlPrefix": {
                    "type": "string"
                },
                "urlSuffix": {
                    "type": "string"
                }
            }
        },
        "api_v1.StorageType": {
            "type": "string",
            "enum": [
                "S3"
            ],
            "x-enum-varnames": [
                "StorageS3"
            ]
        },
        "api_v1.SystemSetting": {
            "type": "object",
            "properties": {
                "description": {
                    "type": "string"
                },
                "name": {
                    "$ref": "#/definitions/api_v1.SystemSettingName"
                },
                "value": {
                    "description": "Value is a JSON string with basic value.",
                    "type": "string"
                }
            }
        },
        "api_v1.SystemSettingName": {
            "type": "string",
            "enum": [
                "server-id",
                "secret-session",
                "disable-public-memos",
                "max-upload-size-mib",
                "customized-profile",
                "storage-service-id",
                "local-storage-path",
                "telegram-bot-token",
                "memo-display-with-updated-ts"
            ],
            "x-enum-varnames": [
                "SystemSettingServerIDName",
                "SystemSettingSecretSessionName",
                "SystemSettingDisablePublicMemosName",
                "SystemSettingMaxUploadSizeMiBName",
                "SystemSettingCustomizedProfileName",
                "SystemSettingStorageServiceIDName",
                "SystemSettingLocalStoragePathName",
                "SystemSettingTelegramBotTokenName",
                "SystemSettingMemoDisplayWithUpdatedTsName"
            ]
        },
        "api_v1.SystemStatus": {
            "type": "object",
            "properties": {
                "additionalScript": {
                    "description": "Additional script.",
                    "type": "string"
                },
                "additionalStyle": {
                    "description": "Additional style.",
                    "type": "string"
                },
                "allowSignUp": {
                    "description": "System settings\nAllow sign up.",
                    "type": "boolean"
                },
                "customizedProfile": {
                    "description": "Customized server profile, including server name and external url.",
                    "allOf": [
                        {
                            "$ref": "#/definitions/api_v1.CustomizedProfile"
                        }
                    ]
                },
                "dbSize": {
                    "type": "integer"
                },
                "disablePasswordLogin": {
                    "description": "Disable password login.",
                    "type": "boolean"
                },
                "disablePublicMemos": {
                    "description": "Disable public memos.",
                    "type": "boolean"
                },
                "host": {
                    "$ref": "#/definitions/api_v1.User"
                },
                "localStoragePath": {
                    "description": "Local storage path.",
                    "type": "string"
                },
                "maxUploadSizeMiB": {
                    "description": "Max upload size.",
                    "type": "integer"
                },
                "memoDisplayWithUpdatedTs": {
                    "description": "Memo display with updated timestamp.",
                    "type": "boolean"
                },
                "profile": {
                    "$ref": "#/definitions/profile.Profile"
                },
                "storageServiceId": {
                    "description": "Storage service ID.",
                    "type": "integer"
                }
            }
        },
        "api_v1.UpdateIdentityProviderRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/api_v1.IdentityProviderConfig"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/api_v1.IdentityProviderType"
                }
            }
        },
        "api_v1.UpdateResourceRequest": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string"
                }
            }
        },
        "api_v1.UpdateStorageRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/api_v1.StorageConfig"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/api_v1.StorageType"
                }
            }
        },
        "api_v1.UpdateUserRequest": {
            "type": "object",
            "properties": {
                "avatarUrl": {
                    "type": "string"
                },
                "email": {
                    "type": "string"
                },
                "nickname": {
                    "type": "string"
                },
                "password": {
                    "type": "string"
                },
                "rowStatus": {
                    "$ref": "#/definitions/api_v1.RowStatus"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "api_v1.UpsertMemoOrganizerRequest": {
            "type": "object",
            "properties": {
                "pinned": {
                    "type": "boolean"
                }
            }
        },
        "api_v1.UpsertMemoRelationRequest": {
            "type": "object",
            "properties": {
                "relatedMemoId": {
                    "type": "integer"
                },
                "type": {
                    "$ref": "#/definitions/api_v1.MemoRelationType"
                }
            }
        },
        "api_v1.UpsertSystemSettingRequest": {
            "type": "object",
            "properties": {
                "description": {
                    "type": "string"
                },
                "name": {
                    "$ref": "#/definitions/api_v1.SystemSettingName"
                },
                "value": {
                    "type": "string"
                }
            }
        },
        "api_v1.UpsertTagRequest": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "api_v1.User": {
            "type": "object",
            "properties": {
                "avatarUrl": {
                    "type": "string"
                },
                "createdTs": {
                    "type": "integer"
                },
                "email": {
                    "type": "string"
                },
                "id": {
                    "type": "integer"
                },
                "nickname": {
                    "type": "string"
                },
                "role": {
                    "$ref": "#/definitions/api_v1.Role"
                },
                "rowStatus": {
                    "description": "Standard fields",
                    "allOf": [
                        {
                            "$ref": "#/definitions/api_v1.RowStatus"
                        }
                    ]
                },
                "updatedTs": {
                    "type": "integer"
                },
                "username": {
                    "description": "Domain specific fields",
                    "type": "string"
                }
            }
        },
        "api_v1.Visibility": {
            "type": "string",
            "enum": [
                "PUBLIC",
                "PROTECTED",
                "PRIVATE"
            ],
            "x-enum-varnames": [
                "Public",
                "Protected",
                "Private"
            ]
        },
        "github_com_usememos_memos_api_v1.CreateIdentityProviderRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType"
                }
            }
        },
        "github_com_usememos_memos_api_v1.CreateMemoRequest": {
            "type": "object",
            "properties": {
                "content": {
                    "type": "string"
                },
                "createdTs": {
                    "type": "integer"
                },
                "relationList": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
                    }
                },
                "resourceIdList": {
                    "description": "Related fields",
                    "type": "array",
                    "items": {
                        "type": "integer"
                    }
                },
                "visibility": {
                    "description": "Domain specific fields",
                    "allOf": [
                        {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.Visibility"
                        }
                    ]
                }
            }
        },
        "github_com_usememos_memos_api_v1.CreateResourceRequest": {
            "type": "object",
            "properties": {
                "externalLink": {
                    "type": "string"
                },
                "filename": {
                    "type": "string"
                },
                "type": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.CreateStorageRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageConfig"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageType"
                }
            }
        },
        "github_com_usememos_memos_api_v1.CreateUserRequest": {
            "type": "object",
            "properties": {
                "email": {
                    "type": "string"
                },
                "nickname": {
                    "type": "string"
                },
                "password": {
                    "type": "string"
                },
                "role": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.Role"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.CustomizedProfile": {
            "type": "object",
            "properties": {
                "appearance": {
                    "description": "Appearance is the server default appearance.",
                    "type": "string"
                },
                "description": {
                    "description": "Description is the server description.",
                    "type": "string"
                },
                "locale": {
                    "description": "Locale is the server default locale.",
                    "type": "string"
                },
                "logoUrl": {
                    "description": "LogoURL is the url of logo image.",
                    "type": "string"
                },
                "name": {
                    "description": "Name is the server name, default is ` + "`" + `memos` + "`" + `",
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.DeleteTagRequest": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.FieldMapping": {
            "type": "object",
            "properties": {
                "displayName": {
                    "type": "string"
                },
                "email": {
                    "type": "string"
                },
                "identifier": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.IdentityProvider": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig"
                },
                "id": {
                    "type": "integer"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType"
                }
            }
        },
        "github_com_usememos_memos_api_v1.IdentityProviderConfig": {
            "type": "object",
            "properties": {
                "oauth2Config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config"
                }
            }
        },
        "github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config": {
            "type": "object",
            "properties": {
                "authUrl": {
                    "type": "string"
                },
                "clientId": {
                    "type": "string"
                },
                "clientSecret": {
                    "type": "string"
                },
                "fieldMapping": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.FieldMapping"
                },
                "scopes": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "tokenUrl": {
                    "type": "string"
                },
                "userInfoUrl": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.IdentityProviderType": {
            "type": "string",
            "enum": [
                "OAUTH2"
            ],
            "x-enum-varnames": [
                "IdentityProviderOAuth2Type"
            ]
        },
        "github_com_usememos_memos_api_v1.MemoRelationType": {
            "type": "string",
            "enum": [
                "REFERENCE",
                "COMMENT"
            ],
            "x-enum-varnames": [
                "MemoRelationReference",
                "MemoRelationComment"
            ]
        },
        "github_com_usememos_memos_api_v1.PatchMemoRequest": {
            "type": "object",
            "properties": {
                "content": {
                    "description": "Domain specific fields",
                    "type": "string"
                },
                "createdTs": {
                    "description": "Standard fields",
                    "type": "integer"
                },
                "relationList": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest"
                    }
                },
                "resourceIdList": {
                    "description": "Related fields",
                    "type": "array",
                    "items": {
                        "type": "integer"
                    }
                },
                "rowStatus": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus"
                },
                "updatedTs": {
                    "type": "integer"
                },
                "visibility": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.Visibility"
                }
            }
        },
        "github_com_usememos_memos_api_v1.Role": {
            "type": "string",
            "enum": [
                "HOST",
                "ADMIN",
                "USER"
            ],
            "x-enum-varnames": [
                "RoleHost",
                "RoleAdmin",
                "RoleUser"
            ]
        },
        "github_com_usememos_memos_api_v1.RowStatus": {
            "type": "string",
            "enum": [
                "NORMAL",
                "ARCHIVED"
            ],
            "x-enum-varnames": [
                "Normal",
                "Archived"
            ]
        },
        "github_com_usememos_memos_api_v1.SSOSignIn": {
            "type": "object",
            "properties": {
                "code": {
                    "type": "string"
                },
                "identityProviderId": {
                    "type": "integer"
                },
                "redirectUri": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.SignIn": {
            "type": "object",
            "properties": {
                "password": {
                    "type": "string"
                },
                "remember": {
                    "type": "boolean"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.SignUp": {
            "type": "object",
            "properties": {
                "password": {
                    "type": "string"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.StorageConfig": {
            "type": "object",
            "properties": {
                "s3Config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageS3Config"
                }
            }
        },
        "github_com_usememos_memos_api_v1.StorageS3Config": {
            "type": "object",
            "properties": {
                "accessKey": {
                    "type": "string"
                },
                "bucket": {
                    "type": "string"
                },
                "endPoint": {
                    "type": "string"
                },
                "path": {
                    "type": "string"
                },
                "presign": {
                    "type": "boolean"
                },
                "region": {
                    "type": "string"
                },
                "secretKey": {
                    "type": "string"
                },
                "urlPrefix": {
                    "type": "string"
                },
                "urlSuffix": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.StorageType": {
            "type": "string",
            "enum": [
                "S3"
            ],
            "x-enum-varnames": [
                "StorageS3"
            ]
        },
        "github_com_usememos_memos_api_v1.SystemSetting": {
            "type": "object",
            "properties": {
                "description": {
                    "type": "string"
                },
                "name": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemSettingName"
                },
                "value": {
                    "description": "Value is a JSON string with basic value.",
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.SystemSettingName": {
            "type": "string",
            "enum": [
                "server-id",
                "secret-session",
                "disable-public-memos",
                "max-upload-size-mib",
                "customized-profile",
                "storage-service-id",
                "local-storage-path",
                "telegram-bot-token",
                "memo-display-with-updated-ts"
            ],
            "x-enum-varnames": [
                "SystemSettingServerIDName",
                "SystemSettingSecretSessionName",
                "SystemSettingDisablePublicMemosName",
                "SystemSettingMaxUploadSizeMiBName",
                "SystemSettingCustomizedProfileName",
                "SystemSettingStorageServiceIDName",
                "SystemSettingLocalStoragePathName",
                "SystemSettingTelegramBotTokenName",
                "SystemSettingMemoDisplayWithUpdatedTsName"
            ]
        },
        "github_com_usememos_memos_api_v1.SystemStatus": {
            "type": "object",
            "properties": {
                "additionalScript": {
                    "description": "Additional script.",
                    "type": "string"
                },
                "additionalStyle": {
                    "description": "Additional style.",
                    "type": "string"
                },
                "allowSignUp": {
                    "description": "System settings\nAllow sign up.",
                    "type": "boolean"
                },
                "customizedProfile": {
                    "description": "Customized server profile, including server name and external url.",
                    "allOf": [
                        {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.CustomizedProfile"
                        }
                    ]
                },
                "dbSize": {
                    "type": "integer"
                },
                "disablePasswordLogin": {
                    "description": "Disable password login.",
                    "type": "boolean"
                },
                "disablePublicMemos": {
                    "description": "Disable public memos.",
                    "type": "boolean"
                },
                "host": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.User"
                },
                "localStoragePath": {
                    "description": "Local storage path.",
                    "type": "string"
                },
                "maxUploadSizeMiB": {
                    "description": "Max upload size.",
                    "type": "integer"
                },
                "memoDisplayWithUpdatedTs": {
                    "description": "Memo display with updated timestamp.",
                    "type": "boolean"
                },
                "profile": {
                    "$ref": "#/definitions/profile.Profile"
                },
                "storageServiceId": {
                    "description": "Storage service ID.",
                    "type": "integer"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpdateResourceRequest": {
            "type": "object",
            "properties": {
                "filename": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpdateStorageRequest": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageConfig"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.StorageType"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpdateUserRequest": {
            "type": "object",
            "properties": {
                "avatarUrl": {
                    "type": "string"
                },
                "email": {
                    "type": "string"
                },
                "nickname": {
                    "type": "string"
                },
                "password": {
                    "type": "string"
                },
                "rowStatus": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus"
                },
                "username": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest": {
            "type": "object",
            "properties": {
                "pinned": {
                    "type": "boolean"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpsertMemoRelationRequest": {
            "type": "object",
            "properties": {
                "relatedMemoId": {
                    "type": "integer"
                },
                "type": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.MemoRelationType"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpsertSystemSettingRequest": {
            "type": "object",
            "properties": {
                "description": {
                    "type": "string"
                },
                "name": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.SystemSettingName"
                },
                "value": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.UpsertTagRequest": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.User": {
            "type": "object",
            "properties": {
                "avatarUrl": {
                    "type": "string"
                },
                "createdTs": {
                    "type": "integer"
                },
                "email": {
                    "type": "string"
                },
                "id": {
                    "type": "integer"
                },
                "nickname": {
                    "type": "string"
                },
                "role": {
                    "$ref": "#/definitions/github_com_usememos_memos_api_v1.Role"
                },
                "rowStatus": {
                    "description": "Standard fields",
                    "allOf": [
                        {
                            "$ref": "#/definitions/github_com_usememos_memos_api_v1.RowStatus"
                        }
                    ]
                },
                "updatedTs": {
                    "type": "integer"
                },
                "username": {
                    "description": "Domain specific fields",
                    "type": "string"
                }
            }
        },
        "github_com_usememos_memos_api_v1.Visibility": {
            "type": "string",
            "enum": [
                "PUBLIC",
                "PROTECTED",
                "PRIVATE"
            ],
            "x-enum-varnames": [
                "Public",
                "Protected",
                "Private"
            ]
        },
        "profile.Profile": {
            "type": "object",
            "properties": {
                "mode": {
                    "description": "Mode can be \"prod\" or \"dev\" or \"demo\"",
                    "type": "string"
                },
                "version": {
                    "description": "Version is the current version of server",
                    "type": "string"
                }
            }
        },
        "store.FieldMapping": {
            "type": "object",
            "properties": {
                "displayName": {
                    "type": "string"
                },
                "email": {
                    "type": "string"
                },
                "identifier": {
                    "type": "string"
                }
            }
        },
        "store.IdentityProvider": {
            "type": "object",
            "properties": {
                "config": {
                    "$ref": "#/definitions/store.IdentityProviderConfig"
                },
                "id": {
                    "type": "integer"
                },
                "identifierFilter": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "$ref": "#/definitions/store.IdentityProviderType"
                }
            }
        },
        "store.IdentityProviderConfig": {
            "type": "object",
            "properties": {
                "oauth2Config": {
                    "$ref": "#/definitions/store.IdentityProviderOAuth2Config"
                }
            }
        },
        "store.IdentityProviderOAuth2Config": {
            "type": "object",
            "properties": {
                "authUrl": {
                    "type": "string"
                },
                "clientId": {
                    "type": "string"
                },
                "clientSecret": {
                    "type": "string"
                },
                "fieldMapping": {
                    "$ref": "#/definitions/store.FieldMapping"
                },
                "scopes": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "tokenUrl": {
                    "type": "string"
                },
                "userInfoUrl": {
                    "type": "string"
                }
            }
        },
        "store.IdentityProviderType": {
            "type": "string",
            "enum": [
                "OAUTH2"
            ],
            "x-enum-varnames": [
                "IdentityProviderOAuth2Type"
            ]
        },
        "store.Memo": {
            "type": "object",
            "properties": {
                "content": {
                    "description": "Domain specific fields",
                    "type": "string"
                },
                "createdTs": {
                    "type": "integer"
                },
                "creatorID": {
                    "type": "integer"
                },
                "id": {
                    "type": "integer"
                },
                "parentID": {
                    "type": "integer"
                },
                "pinned": {
                    "description": "Composed fields",
                    "type": "boolean"
                },
                "resourceName": {
                    "type": "string"
                },
                "rowStatus": {
                    "description": "Standard fields",
                    "allOf": [
                        {
                            "$ref": "#/definitions/store.RowStatus"
                        }
                    ]
                },
                "updatedTs": {
                    "type": "integer"
                },
                "visibility": {
                    "$ref": "#/definitions/store.Visibility"
                }
            }
        },
        "store.MemoRelation": {
            "type": "object",
            "properties": {
                "memoID": {
                    "type": "integer"
                },
                "relatedMemoID": {
                    "type": "integer"
                },
                "type": {
                    "$ref": "#/definitions/store.MemoRelationType"
                }
            }
        },
        "store.MemoRelationType": {
            "type": "string",
            "enum": [
                "REFERENCE",
                "COMMENT"
            ],
            "x-enum-varnames": [
                "MemoRelationReference",
                "MemoRelationComment"
            ]
        },
        "store.Resource": {
            "type": "object",
            "properties": {
                "blob": {
                    "type": "array",
                    "items": {
                        "type": "integer"
                    }
                },
                "createdTs": {
                    "type": "integer"
                },
                "creatorID": {
                    "description": "Standard fields",
                    "type": "integer"
                },
                "externalLink": {
                    "type": "string"
                },
                "filename": {
                    "description": "Domain specific fields",
                    "type": "string"
                },
                "id": {
                    "type": "integer"
                },
                "internalPath": {
                    "type": "string"
                },
                "memoID": {
                    "type": "integer"
                },
                "resourceName": {
                    "type": "string"
                },
                "size": {
                    "type": "integer"
                },
                "type": {
                    "type": "string"
                },
                "updatedTs": {
                    "type": "integer"
                }
            }
        },
        "store.Role": {
            "type": "string",
            "enum": [
                "HOST",
                "ADMIN",
                "USER"
            ],
            "x-enum-varnames": [
                "RoleHost",
                "RoleAdmin",
                "RoleUser"
            ]
        },
        "store.RowStatus": {
            "type": "string",
            "enum": [
                "NORMAL",
                "ARCHIVED"
            ],
            "x-enum-varnames": [
                "Normal",
                "Archived"
            ]
        },
        "store.Storage": {
            "type": "object",
            "properties": {
                "config": {
                    "type": "string"
                },
                "id": {
                    "type": "integer"
                },
                "name": {
                    "type": "string"
                },
                "type": {
                    "type": "string"
                }
            }
        },
        "store.User": {
            "type": "object",
            "properties": {
                "avatarURL": {
                    "type": "string"
                },
                "createdTs": {
                    "type": "integer"
                },
                "email": {
                    "type": "string"
                },
                "id": {
                    "type": "integer"
                },
                "nickname": {
                    "type": "string"
                },
                "passwordHash": {
                    "type": "string"
                },
                "role": {
                    "$ref": "#/definitions/store.Role"
                },
                "rowStatus": {
                    "description": "Standard fields",
                    "allOf": [
                        {
                            "$ref": "#/definitions/store.RowStatus"
                        }
                    ]
                },
                "updatedTs": {
                    "type": "integer"
                },
                "username": {
                    "description": "Domain specific fields",
                    "type": "string"
                }
            }
        },
        "store.Visibility": {
            "type": "string",
            "enum": [
                "PUBLIC",
                "PROTECTED",
                "PRIVATE"
            ],
            "x-enum-varnames": [
                "Public",
                "Protected",
                "Private"
            ]
        }
    },
    "externalDocs": {
        "description": "Find out more about Memos.",
        "url": "https://usememos.com/"
    }
}`

// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
	Version:          "1.0",
	Host:             "",
	BasePath:         "/",
	Schemes:          []string{},
	Title:            "memos API",
	Description:      "A privacy-first, lightweight note-taking service.",
	InfoInstanceName: "swagger",
	SwaggerTemplate:  docTemplate,
	LeftDelim:        "{{",
	RightDelim:       "}}",
}

func init() {
	swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
package v1

import (
	"fmt"
	"net/http"
	"net/url"

	"github.com/labstack/echo/v4"

	getter "github.com/usememos/memos/plugin/http-getter"
)

func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
	// GET /get/image?url={url} - Get image.
	g.GET("/get/image", GetImage)
}

// GetImage godoc
//
//	@Summary	Get GetImage from URL
//	@Tags		image-url
//	@Produce	GetImage/*
//	@Param		url	query		string	true	"Image url"
//	@Success	200	{object}	nil		"Image"
//	@Failure	400	{object}	nil		"Missing GetImage url | Wrong url | Failed to get GetImage url: %s"
//	@Failure	500	{object}	nil		"Failed to write GetImage blob"
//	@Router		/o/get/GetImage [GET]
func GetImage(c echo.Context) error {
	urlStr := c.QueryParam("url")
	if urlStr == "" {
		return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
	}
	if _, err := url.Parse(urlStr); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
	}

	image, err := getter.GetImage(urlStr)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
	}

	c.Response().Writer.WriteHeader(http.StatusOK)
	c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
	c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
	if _, err := c.Response().Writer.Write(image.Blob); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
	}
	return nil
}
package v1

import (
	"fmt"
	"log/slog"
	"net/http"
	"strings"

	"github.com/golang-jwt/jwt/v5"
	"github.com/labstack/echo/v4"
	"github.com/pkg/errors"

	"github.com/usememos/memos/internal/util"
	storepb "github.com/usememos/memos/proto/gen/store"
	"github.com/usememos/memos/server/route/api/auth"
	"github.com/usememos/memos/store"
)

const (
	// The key name used to store user id in the context
	// user id is extracted from the jwt token subject field.
	userIDContextKey = "user-id"
)

func extractTokenFromHeader(c echo.Context) (string, error) {
	authHeader := c.Request().Header.Get("Authorization")
	if authHeader == "" {
		return "", nil
	}

	authHeaderParts := strings.Fields(authHeader)
	if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
		return "", errors.New("Authorization header format must be Bearer {token}")
	}

	return authHeaderParts[1], nil
}

func findAccessToken(c echo.Context) string {
	// Check the HTTP request header first.
	accessToken, _ := extractTokenFromHeader(c)
	if accessToken == "" {
		// Check the cookie.
		cookie, _ := c.Cookie(auth.AccessTokenCookieName)
		if cookie != nil {
			accessToken = cookie.Value
		}
	}
	return accessToken
}

// JWTMiddleware validates the access token.
func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx := c.Request().Context()
		path := c.Request().URL.Path
		method := c.Request().Method

		if server.defaultAuthSkipper(c) {
			return next(c)
		}

		// Skip validation for server status endpoints.
		if util.HasPrefixes(path, "/api/v1/ping", "/api/v1/status") && method == http.MethodGet {
			return next(c)
		}

		accessToken := findAccessToken(c)
		if accessToken == "" {
			// Allow the user to access the public endpoints.
			if util.HasPrefixes(path, "/o") {
				return next(c)
			}
			// When the request is not authenticated, we allow the user to access the memo endpoints for those public memos.
			if util.HasPrefixes(path, "/api/v1/idp", "/api/v1/memo", "/api/v1/user") && path != "/api/v1/user" && method == http.MethodGet {
				return next(c)
			}
			return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
		}

		userID, err := getUserIDFromAccessToken(accessToken, secret)
		if err != nil {
			err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken)
			if err != nil {
				slog.Warn("fail to remove AccessToken and Cookies", err)
			}
			return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired access token")
		}

		accessTokens, err := server.Store.GetUserAccessTokens(ctx, userID)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user access tokens.").WithInternal(err)
		}
		if !validateAccessToken(accessToken, accessTokens) {
			err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken)
			if err != nil {
				slog.Warn("fail to remove AccessToken and Cookies", err)
			}
			return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.")
		}

		// Even if there is no error, we still need to make sure the user still exists.
		user, err := server.Store.GetUser(ctx, &store.FindUser{
			ID: &userID,
		})
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to find user ID: %d", userID)).SetInternal(err)
		}
		if user == nil {
			return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Failed to find user ID: %d", userID))
		}

		// Stores userID into context.
		c.Set(userIDContextKey, userID)
		return next(c)
	}
}

func getUserIDFromAccessToken(accessToken, secret string) (int32, error) {
	claims := &auth.ClaimsMessage{}
	_, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
		if t.Method.Alg() != jwt.SigningMethodHS256.Name {
			return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
		}
		if kid, ok := t.Header["kid"].(string); ok {
			if kid == "v1" {
				return []byte(secret), nil
			}
		}
		return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
	})
	if err != nil {
		return 0, errors.Wrap(err, "Invalid or expired access token")
	}
	// We either have a valid access token or we will attempt to generate new access token.
	userID, err := util.ConvertStringToInt32(claims.Subject)
	if err != nil {
		return 0, errors.Wrap(err, "Malformed ID in the token")
	}
	return userID, nil
}

func (*APIV1Service) defaultAuthSkipper(c echo.Context) bool {
	path := c.Path()
	return util.HasPrefixes(path, "/api/v1/auth")
}

func validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
	for _, userAccessToken := range userAccessTokens {
		if accessTokenString == userAccessToken.AccessToken {
			return true
		}
	}
	return false
}
package v1

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/lithammer/shortuuid/v4"
	"github.com/pkg/errors"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/plugin/webhook"
	storepb "github.com/usememos/memos/proto/gen/store"
	"github.com/usememos/memos/store"
)

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

type Memo struct {
	ID   int32  `json:"id"`
	Name string `json:"name"`

	// Standard fields
	RowStatus RowStatus `json:"rowStatus"`
	CreatorID int32     `json:"creatorId"`
	CreatedTs int64     `json:"createdTs"`
	UpdatedTs int64     `json:"updatedTs"`

	// Domain specific fields
	DisplayTs  int64      `json:"displayTs"`
	Content    string     `json:"content"`
	Visibility Visibility `json:"visibility"`
	Pinned     bool       `json:"pinned"`

	// Related fields
	CreatorName     string          `json:"creatorName"`
	CreatorUsername string          `json:"creatorUsername"`
	ResourceList    []*Resource     `json:"resourceList"`
	RelationList    []*MemoRelation `json:"relationList"`
}

type CreateMemoRequest struct {
	// Standard fields
	CreatorID int32  `json:"-"`
	CreatedTs *int64 `json:"createdTs"`

	// Domain specific fields
	Visibility Visibility `json:"visibility"`
	Content    string     `json:"content"`

	// Related fields
	ResourceIDList []int32                      `json:"resourceIdList"`
	RelationList   []*UpsertMemoRelationRequest `json:"relationList"`
}

type PatchMemoRequest struct {
	ID int32 `json:"-"`

	// Standard fields
	CreatedTs *int64 `json:"createdTs"`
	UpdatedTs *int64
	RowStatus *RowStatus `json:"rowStatus"`

	// Domain specific fields
	Content    *string     `json:"content"`
	Visibility *Visibility `json:"visibility"`

	// Related fields
	ResourceIDList []int32                      `json:"resourceIdList"`
	RelationList   []*UpsertMemoRelationRequest `json:"relationList"`
}

type FindMemoRequest struct {
	ID *int32

	// Standard fields
	RowStatus *RowStatus
	CreatorID *int32

	// Domain specific fields
	Pinned         *bool
	ContentSearch  []string
	VisibilityList []Visibility

	// Pagination
	Limit  *int
	Offset *int
}

// maxContentLength means the max memo content bytes is 1MB.
const maxContentLength = 1 << 30

func (s *APIV1Service) registerMemoRoutes(g *echo.Group) {
	g.GET("/memo", s.GetMemoList)
	g.POST("/memo", s.CreateMemo)
	g.GET("/memo/all", s.GetAllMemos)
	g.GET("/memo/stats", s.GetMemoStats)
	g.GET("/memo/:memoId", s.GetMemo)
	g.PATCH("/memo/:memoId", s.UpdateMemo)
	g.DELETE("/memo/:memoId", s.DeleteMemo)
}

// GetMemoList godoc
//
//	@Summary	Get a list of memos matching optional filters
//	@Tags		memo
//	@Produce	json
//	@Param		creatorId		query		int				false	"Creator ID"
//	@Param		creatorUsername	query		string			false	"Creator username"
//	@Param		rowStatus		query		store.RowStatus	false	"Row status"
//	@Param		pinned			query		bool			false	"Pinned"
//	@Param		tag				query		string			false	"Search for tag. Do not append #"
//	@Param		content			query		string			false	"Search for content"
//	@Param		limit			query		int				false	"Limit"
//	@Param		offset			query		int				false	"Offset"
//	@Success	200				{object}	[]store.Memo	"Memo list"
//	@Failure	400				{object}	nil				"Missing user to find memo"
//	@Failure	500				{object}	nil				"Failed to get memo display with updated ts setting value | Failed to fetch memo list | Failed to compose memo response"
//	@Router		/api/v1/memo [GET]
func (s *APIV1Service) GetMemoList(c echo.Context) error {
	ctx := c.Request().Context()
	find := &store.FindMemo{
		OrderByPinned: true,
	}
	if userID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil {
		find.CreatorID = &userID
	}

	if username := c.QueryParam("creatorUsername"); username != "" {
		user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
		if user != nil {
			find.CreatorID = &user.ID
		}
	}

	currentUserID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		// Anonymous use should only fetch PUBLIC memos with specified user
		if find.CreatorID == nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Missing user to find memo")
		}
		find.VisibilityList = []store.Visibility{store.Public}
	} else {
		// Authorized user can fetch all PUBLIC/PROTECTED memo
		visibilityList := []store.Visibility{store.Public, store.Protected}

		// If Creator is authorized user (as default), PRIVATE memo is OK
		if find.CreatorID == nil || *find.CreatorID == currentUserID {
			find.CreatorID = &currentUserID
			visibilityList = append(visibilityList, store.Private)
		}
		find.VisibilityList = visibilityList
	}

	rowStatus := store.RowStatus(c.QueryParam("rowStatus"))
	if rowStatus != "" {
		find.RowStatus = &rowStatus
	}

	contentSearch := []string{}
	tag := c.QueryParam("tag")
	if tag != "" {
		contentSearch = append(contentSearch, "#"+tag)
	}
	content := c.QueryParam("content")
	if content != "" {
		contentSearch = append(contentSearch, content)
	}
	find.ContentSearch = contentSearch

	if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
		find.Limit = &limit
	}
	if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
		find.Offset = &offset
	}

	list, err := s.Store.ListMemos(ctx, find)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
	}
	memoResponseList := []*Memo{}
	for _, memo := range list {
		memoResponse, err := s.convertMemoFromStore(ctx, memo)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
		}
		memoResponseList = append(memoResponseList, memoResponse)
	}
	return c.JSON(http.StatusOK, memoResponseList)
}

// CreateMemo godoc
//
//	@Summary		Create a memo
//	@Description	Visibility can be PUBLIC, PROTECTED or PRIVATE
//	@Description	*You should omit fields to use their default values
//	@Tags			memo
//	@Accept			json
//	@Produce		json
//	@Param			body	body		CreateMemoRequest	true	"Request object."
//	@Success		200		{object}	store.Memo			"Stored memo"
//	@Failure		400		{object}	nil					"Malformatted post memo request | Content size overflow, up to 1MB"
//	@Failure		401		{object}	nil					"Missing user in session"
//	@Failure		404		{object}	nil					"User not found | Memo not found: %d"
//	@Failure		500		{object}	nil					"Failed to find user setting | Failed to unmarshal user setting value | Failed to find system setting | Failed to unmarshal system setting | Failed to find user | Failed to create memo | Failed to create activity | Failed to upsert memo resource | Failed to upsert memo relation | Failed to compose memo | Failed to compose memo response"
//	@Router			/api/v1/memo [POST]
//
// NOTES:
// - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
func (s *APIV1Service) CreateMemo(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	createMemoRequest := &CreateMemoRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(createMemoRequest); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
	}
	if len(createMemoRequest.Content) > maxContentLength {
		return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB")
	}

	if createMemoRequest.Visibility == "" {
		userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
			UserID: &userID,
			Key:    storepb.UserSettingKey_USER_SETTING_MEMO_VISIBILITY,
		})
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
		}
		if userMemoVisibilitySetting != nil {
			createMemoRequest.Visibility = Visibility(userMemoVisibilitySetting.GetMemoVisibility())
		} else {
			// Private is the default memo visibility.
			createMemoRequest.Visibility = Private
		}
	}

	createMemoRequest.CreatorID = userID
	memo, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest))
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
	}

	for _, resourceID := range createMemoRequest.ResourceIDList {
		if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
			ID:     resourceID,
			MemoID: &memo.ID,
		}); err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
		}
	}

	for _, memoRelationUpsert := range createMemoRequest.RelationList {
		if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
			MemoID:        memo.ID,
			RelatedMemoID: memoRelationUpsert.RelatedMemoID,
			Type:          store.MemoRelationType(memoRelationUpsert.Type),
		}); err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
		}
		if memo.Visibility != store.Private && memoRelationUpsert.Type == MemoRelationComment {
			relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
				ID: &memoRelationUpsert.RelatedMemoID,
			})
			if err != nil {
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get related memo").SetInternal(err)
			}
			if relatedMemo.CreatorID != memo.CreatorID {
				activity, err := s.Store.CreateActivity(ctx, &store.Activity{
					CreatorID: memo.CreatorID,
					Type:      store.ActivityTypeMemoComment,
					Level:     store.ActivityLevelInfo,
					Payload: &storepb.ActivityPayload{
						MemoComment: &storepb.ActivityMemoCommentPayload{
							MemoId:        memo.ID,
							RelatedMemoId: memoRelationUpsert.RelatedMemoID,
						},
					},
				})
				if err != nil {
					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
				}
				if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
					SenderID:   memo.CreatorID,
					ReceiverID: relatedMemo.CreatorID,
					Status:     store.UNREAD,
					Message: &storepb.InboxMessage{
						Type:       storepb.InboxMessage_TYPE_MEMO_COMMENT,
						ActivityId: &activity.ID,
					},
				}); err != nil {
					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create inbox").SetInternal(err)
				}
			}
		}
	}

	composedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
		ID: &memo.ID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
	}
	if composedMemo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memo.ID))
	}

	memoResponse, err := s.convertMemoFromStore(ctx, composedMemo)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
	}

	// Send notification to telegram if memo is not private.
	if memoResponse.Visibility != Private {
		// fetch all telegram UserID
		userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: storepb.UserSettingKey_USER_SETTING_TELEGRAM_USER_ID})
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to ListUserSettings").SetInternal(err)
		}
		for _, userSetting := range userSettings {
			tgUserID, err := strconv.ParseInt(userSetting.GetTelegramUserId(), 10, 64)
			if err != nil {
				continue
			}

			// send notification to telegram
			content := memoResponse.CreatorName + " Says:\n\n" + memoResponse.Content
			_, err = s.telegramBot.SendMessage(ctx, tgUserID, content)
			if err != nil {
				continue
			}
		}
	}
	// Try to dispatch webhook when memo is created.
	if err := s.DispatchMemoCreatedWebhook(ctx, memoResponse); err != nil {
		slog.Warn("Failed to dispatch memo created webhook", err)
	}

	return c.JSON(http.StatusOK, memoResponse)
}

// GetAllMemos godoc
//
//	@Summary		Get a list of public memos matching optional filters
//	@Description	This should also list protected memos if the user is logged in
//	@Description	Authentication is optional
//	@Tags			memo
//	@Produce		json
//	@Param			limit	query		int				false	"Limit"
//	@Param			offset	query		int				false	"Offset"
//	@Success		200		{object}	[]store.Memo	"Memo list"
//	@Failure		500		{object}	nil				"Failed to get memo display with updated ts setting value | Failed to fetch all memo list | Failed to compose memo response"
//	@Router			/api/v1/memo/all [GET]
//
//	NOTES:
//	- creatorUsername is listed at ./web/src/helpers/api.ts:82, but it's not present here
func (s *APIV1Service) GetAllMemos(c echo.Context) error {
	ctx := c.Request().Context()
	memoFind := &store.FindMemo{}
	_, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		memoFind.VisibilityList = []store.Visibility{store.Public}
	} else {
		memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
	}

	if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
		memoFind.Limit = &limit
	}
	if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
		memoFind.Offset = &offset
	}

	// Only fetch normal status memos.
	normalStatus := store.Normal
	memoFind.RowStatus = &normalStatus

	list, err := s.Store.ListMemos(ctx, memoFind)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
	}
	memoResponseList := []*Memo{}
	for _, memo := range list {
		memoResponse, err := s.convertMemoFromStore(ctx, memo)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
		}
		memoResponseList = append(memoResponseList, memoResponse)
	}
	return c.JSON(http.StatusOK, memoResponseList)
}

// GetMemoStats godoc
//
//	@Summary		Get memo stats by creator ID or username
//	@Description	Used to generate the heatmap
//	@Tags			memo
//	@Produce		json
//	@Param			creatorId		query		int		false	"Creator ID"
//	@Param			creatorUsername	query		string	false	"Creator username"
//	@Success		200				{object}	[]int	"Memo createdTs list"
//	@Failure		400				{object}	nil		"Missing user id to find memo"
//	@Failure		500				{object}	nil		"Failed to get memo display with updated ts setting value | Failed to find memo list | Failed to compose memo response"
//	@Router			/api/v1/memo/stats [GET]
func (s *APIV1Service) GetMemoStats(c echo.Context) error {
	ctx := c.Request().Context()
	normalStatus := store.Normal
	findMemoMessage := &store.FindMemo{
		RowStatus:      &normalStatus,
		ExcludeContent: true,
	}
	if creatorID, err := util.ConvertStringToInt32(c.QueryParam("creatorId")); err == nil {
		findMemoMessage.CreatorID = &creatorID
	}

	if username := c.QueryParam("creatorUsername"); username != "" {
		user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
		if user != nil {
			findMemoMessage.CreatorID = &user.ID
		}
	}

	if findMemoMessage.CreatorID == nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
	}

	currentUserID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		findMemoMessage.VisibilityList = []store.Visibility{store.Public}
	} else {
		if *findMemoMessage.CreatorID != currentUserID {
			findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
		} else {
			findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected, store.Private}
		}
	}

	list, err := s.Store.ListMemos(ctx, findMemoMessage)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
	}

	displayTsList := []int64{}
	for _, memo := range list {
		displayTsList = append(displayTsList, memo.CreatedTs)
	}
	return c.JSON(http.StatusOK, displayTsList)
}

// GetMemo godoc
//
//	@Summary	Get memo by ID
//	@Tags		memo
//	@Produce	json
//	@Param		memoId	path		int				true	"Memo ID"
//	@Success	200		{object}	[]store.Memo	"Memo list"
//	@Failure	400		{object}	nil				"ID is not a number: %s"
//	@Failure	401		{object}	nil				"Missing user in session"
//	@Failure	403		{object}	nil				"this memo is private only | this memo is protected, missing user in session
//	@Failure	404		{object}	nil				"Memo not found: %d"
//	@Failure	500		{object}	nil				"Failed to find memo by ID: %v | Failed to compose memo response"
//	@Router		/api/v1/memo/{memoId} [GET]
func (s *APIV1Service) GetMemo(c echo.Context) error {
	ctx := c.Request().Context()
	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}

	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
		ID: &memoID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
	}

	userID, ok := c.Get(userIDContextKey).(int32)
	if memo.Visibility == store.Private {
		if !ok || memo.CreatorID != userID {
			return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
		}
	} else if memo.Visibility == store.Protected {
		if !ok {
			return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
		}
	}
	memoResponse, err := s.convertMemoFromStore(ctx, memo)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
	}
	return c.JSON(http.StatusOK, memoResponse)
}

// DeleteMemo godoc
//
//	@Summary	Delete memo by ID
//	@Tags		memo
//	@Produce	json
//	@Param		memoId	path		int		true	"Memo ID to delete"
//	@Success	200		{boolean}	true	"Memo deleted"
//	@Failure	400		{object}	nil		"ID is not a number: %s"
//	@Failure	401		{object}	nil		"Missing user in session | Unauthorized"
//	@Failure	404		{object}	nil		"Memo not found: %d"
//	@Failure	500		{object}	nil		"Failed to find memo | Failed to delete memo ID: %v"
//	@Router		/api/v1/memo/{memoId} [DELETE]
func (s *APIV1Service) DeleteMemo(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}
	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}

	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
		ID: &memoID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
	}
	if memo.CreatorID != userID {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil {
		// Try to dispatch webhook when memo is deleted.
		if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil {
			slog.Warn("Failed to dispatch memo deleted webhook", err)
		}
	}

	if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{
		ID: memoID,
	}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

// UpdateMemo godoc
//
//	@Summary		Update a memo
//	@Description	Visibility can be PUBLIC, PROTECTED or PRIVATE
//	@Description	*You should omit fields to use their default values
//	@Tags			memo
//	@Accept			json
//	@Produce		json
//	@Param			memoId	path		int					true	"ID of memo to update"
//	@Param			body	body		PatchMemoRequest	true	"Patched object."
//	@Success		200		{object}	store.Memo			"Stored memo"
//	@Failure		400		{object}	nil					"ID is not a number: %s | Malformatted patch memo request | Content size overflow, up to 1MB"
//	@Failure		401		{object}	nil					"Missing user in session | Unauthorized"
//	@Failure		404		{object}	nil					"Memo not found: %d"
//	@Failure		500		{object}	nil					"Failed to find memo | Failed to patch memo | Failed to upsert memo resource | Failed to delete memo resource | Failed to compose memo response"
//	@Router			/api/v1/memo/{memoId} [PATCH]
//
// NOTES:
// - It's currently possible to create phantom resources and relations. Phantom relations will trigger backend 404's when fetching memo.
// - Passing 0 to createdTs and updatedTs will set them to 0 in the database, which is probably unwanted.
func (s *APIV1Service) UpdateMemo(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}

	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
		ID: &memoID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
	}
	if memo.CreatorID != userID {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	currentTs := time.Now().Unix()
	patchMemoRequest := &PatchMemoRequest{
		ID:        memoID,
		UpdatedTs: &currentTs,
	}
	if err := json.NewDecoder(c.Request().Body).Decode(patchMemoRequest); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
	}

	if patchMemoRequest.Content != nil && len(*patchMemoRequest.Content) > maxContentLength {
		return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
	}

	updateMemoMessage := &store.UpdateMemo{
		ID:        memoID,
		CreatedTs: patchMemoRequest.CreatedTs,
		UpdatedTs: patchMemoRequest.UpdatedTs,
		Content:   patchMemoRequest.Content,
	}
	if patchMemoRequest.RowStatus != nil {
		rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String())
		updateMemoMessage.RowStatus = &rowStatus
	}

	err = s.Store.UpdateMemo(ctx, updateMemoMessage)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
	}
	memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
	}

	memoMessage, err := s.convertMemoFromStore(ctx, memo)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
	}
	if patchMemoRequest.ResourceIDList != nil {
		originResourceIDList := []int32{}
		for _, resource := range memoMessage.ResourceList {
			originResourceIDList = append(originResourceIDList, resource.ID)
		}
		addedResourceIDList, removedResourceIDList := getIDListDiff(originResourceIDList, patchMemoRequest.ResourceIDList)
		for _, resourceID := range addedResourceIDList {
			if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
				ID:     resourceID,
				MemoID: &memo.ID,
			}); err != nil {
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
			}
		}
		for _, resourceID := range removedResourceIDList {
			if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
				ID: resourceID,
			}); err != nil {
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
			}
		}
	}

	if patchMemoRequest.RelationList != nil {
		patchMemoRelationList := make([]*MemoRelation, 0)
		for _, memoRelation := range patchMemoRequest.RelationList {
			patchMemoRelationList = append(patchMemoRelationList, &MemoRelation{
				MemoID:        memo.ID,
				RelatedMemoID: memoRelation.RelatedMemoID,
				Type:          memoRelation.Type,
			})
		}
		addedMemoRelationList, removedMemoRelationList := getMemoRelationListDiff(memoMessage.RelationList, patchMemoRelationList)
		for _, memoRelation := range addedMemoRelationList {
			if _, err := s.Store.UpsertMemoRelation(ctx, memoRelation); err != nil {
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
			}
		}
		for _, memoRelation := range removedMemoRelationList {
			if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
				MemoID:        &memo.ID,
				RelatedMemoID: &memoRelation.RelatedMemoID,
				Type:          &memoRelation.Type,
			}); err != nil {
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
			}
		}
	}

	memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoID})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %d", memoID))
	}

	memoResponse, err := s.convertMemoFromStore(ctx, memo)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
	}
	// Try to dispatch webhook when memo is updated.
	if err := s.DispatchMemoUpdatedWebhook(ctx, memoResponse); err != nil {
		slog.Error("Failed to dispatch memo updated webhook", err)
	}

	return c.JSON(http.StatusOK, memoResponse)
}

func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*Memo, error) {
	memoMessage := &Memo{
		ID:         memo.ID,
		Name:       memo.UID,
		RowStatus:  RowStatus(memo.RowStatus.String()),
		CreatorID:  memo.CreatorID,
		CreatedTs:  memo.CreatedTs,
		UpdatedTs:  memo.UpdatedTs,
		Content:    memo.Content,
		Visibility: Visibility(memo.Visibility.String()),
		Pinned:     memo.Pinned,
	}

	// Compose creator name.
	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &memoMessage.CreatorID,
	})
	if err != nil {
		return nil, err
	}
	if user.Nickname != "" {
		memoMessage.CreatorName = user.Nickname
	} else {
		memoMessage.CreatorName = user.Username
	}
	memoMessage.CreatorUsername = user.Username

	// Compose display ts.
	memoMessage.DisplayTs = memoMessage.CreatedTs

	// Compose related resources.
	resourceList, err := s.Store.ListResources(ctx, &store.FindResource{
		MemoID: &memo.ID,
	})
	if err != nil {
		return nil, errors.Wrapf(err, "failed to list resources")
	}
	memoMessage.ResourceList = []*Resource{}
	for _, resource := range resourceList {
		memoMessage.ResourceList = append(memoMessage.ResourceList, convertResourceFromStore(resource))
	}

	// Compose related memo relations.
	relationList := []*MemoRelation{}
	tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
		MemoID: &memo.ID,
	})
	if err != nil {
		return nil, err
	}
	for _, relation := range tempList {
		relationList = append(relationList, convertMemoRelationFromStore(relation))
	}
	tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
		RelatedMemoID: &memo.ID,
	})
	if err != nil {
		return nil, err
	}
	for _, relation := range tempList {
		relationList = append(relationList, convertMemoRelationFromStore(relation))
	}
	memoMessage.RelationList = relationList
	return memoMessage, nil
}

func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store.Memo {
	createdTs := time.Now().Unix()
	if memoCreate.CreatedTs != nil {
		createdTs = *memoCreate.CreatedTs
	}
	return &store.Memo{
		UID:        shortuuid.New(),
		CreatorID:  memoCreate.CreatorID,
		CreatedTs:  createdTs,
		Content:    memoCreate.Content,
		Visibility: store.Visibility(memoCreate.Visibility),
	}
}

func getMemoRelationListDiff(oldList, newList []*MemoRelation) (addedList, removedList []*store.MemoRelation) {
	oldMap := map[string]bool{}
	for _, relation := range oldList {
		oldMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
	}
	newMap := map[string]bool{}
	for _, relation := range newList {
		newMap[fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)] = true
	}
	for _, relation := range oldList {
		key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
		if !newMap[key] {
			removedList = append(removedList, &store.MemoRelation{
				MemoID:        relation.MemoID,
				RelatedMemoID: relation.RelatedMemoID,
				Type:          store.MemoRelationType(relation.Type),
			})
		}
	}
	for _, relation := range newList {
		key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
		if !oldMap[key] {
			addedList = append(addedList, &store.MemoRelation{
				MemoID:        relation.MemoID,
				RelatedMemoID: relation.RelatedMemoID,
				Type:          store.MemoRelationType(relation.Type),
			})
		}
	}
	return addedList, removedList
}

func getIDListDiff(oldList, newList []int32) (addedList, removedList []int32) {
	oldMap := map[int32]bool{}
	for _, id := range oldList {
		oldMap[id] = true
	}
	newMap := map[int32]bool{}
	for _, id := range newList {
		newMap[id] = true
	}
	for id := range oldMap {
		if !newMap[id] {
			removedList = append(removedList, id)
		}
	}
	for id := range newMap {
		if !oldMap[id] {
			addedList = append(addedList, id)
		}
	}
	return addedList, removedList
}

// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *Memo) error {
	return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
}

// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *Memo) error {
	return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
}

// DispatchMemoDeletedWebhook dispatches webhook when memo is deletedd.
func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *Memo) error {
	return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted")
}

func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Memo, activityType string) error {
	webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
		CreatorID: &memo.CreatorID,
	})
	if err != nil {
		return err
	}
	for _, hook := range webhooks {
		payload := convertMemoToWebhookPayload(memo)
		payload.ActivityType = activityType
		payload.URL = hook.Url
		err := webhook.Post(*payload)
		if err != nil {
			return errors.Wrap(err, "failed to post webhook")
		}
	}
	return nil
}

func convertMemoToWebhookPayload(memo *Memo) *webhook.WebhookPayload {
	return &webhook.WebhookPayload{
		CreatorID: memo.CreatorID,
		CreatedTs: time.Now().Unix(),
		Memo: &webhook.Memo{
			ID:         memo.ID,
			CreatorID:  memo.CreatorID,
			CreatedTs:  memo.CreatedTs,
			UpdatedTs:  memo.UpdatedTs,
			Content:    memo.Content,
			Visibility: memo.Visibility.String(),
			Pinned:     memo.Pinned,
			ResourceList: func() []*webhook.Resource {
				resources := []*webhook.Resource{}
				for _, resource := range memo.ResourceList {
					resources = append(resources, &webhook.Resource{
						ID:           resource.ID,
						CreatorID:    resource.CreatorID,
						CreatedTs:    resource.CreatedTs,
						UpdatedTs:    resource.UpdatedTs,
						Filename:     resource.Filename,
						InternalPath: resource.InternalPath,
						ExternalLink: resource.ExternalLink,
						Type:         resource.Type,
						Size:         resource.Size,
					})
				}
				return resources
			}(),
			RelationList: func() []*webhook.MemoRelation {
				relations := []*webhook.MemoRelation{}
				for _, relation := range memo.RelationList {
					relations = append(relations, &webhook.MemoRelation{
						MemoID:        relation.MemoID,
						RelatedMemoID: relation.RelatedMemoID,
						Type:          relation.Type.String(),
					})
				}
				return relations
			}(),
		},
	}
}
package v1

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/store"
)

type MemoOrganizer struct {
	MemoID int32 `json:"memoId"`
	UserID int32 `json:"userId"`
	Pinned bool  `json:"pinned"`
}

type UpsertMemoOrganizerRequest struct {
	Pinned bool `json:"pinned"`
}

func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) {
	g.POST("/memo/:memoId/organizer", s.CreateMemoOrganizer)
}

// CreateMemoOrganizer godoc
//
//	@Summary	Organize memo (pin/unpin)
//	@Tags		memo-organizer
//	@Accept		json
//	@Produce	json
//	@Param		memoId	path		int							true	"ID of memo to organize"
//	@Param		body	body		UpsertMemoOrganizerRequest	true	"Memo organizer object"
//	@Success	200		{object}	store.Memo					"Memo information"
//	@Failure	400		{object}	nil							"ID is not a number: %s | Malformatted post memo organizer request"
//	@Failure	401		{object}	nil							"Missing user in session | Unauthorized"
//	@Failure	404		{object}	nil							"Memo not found: %v"
//	@Failure	500		{object}	nil							"Failed to find memo | Failed to upsert memo organizer | Failed to find memo by ID: %v | Failed to compose memo response"
//	@Router		/api/v1/memo/{memoId}/organizer [POST]
func (s *APIV1Service) CreateMemoOrganizer(c echo.Context) error {
	ctx := c.Request().Context()
	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}

	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
		ID: &memoID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %v", memoID))
	}
	if memo.CreatorID != userID {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	request := &UpsertMemoOrganizerRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
	}

	upsert := &store.MemoOrganizer{
		MemoID: memoID,
		UserID: userID,
		Pinned: request.Pinned,
	}
	_, err = s.Store.UpsertMemoOrganizer(ctx, upsert)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
	}

	memo, err = s.Store.GetMemo(ctx, &store.FindMemo{
		ID: &memoID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
	}
	if memo == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %v", memoID))
	}

	memoResponse, err := s.convertMemoFromStore(ctx, memo)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
	}
	return c.JSON(http.StatusOK, memoResponse)
}
package v1

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/store"
)

type MemoRelationType string

const (
	MemoRelationReference MemoRelationType = "REFERENCE"
	MemoRelationComment   MemoRelationType = "COMMENT"
)

func (t MemoRelationType) String() string {
	return string(t)
}

type MemoRelation struct {
	MemoID        int32            `json:"memoId"`
	RelatedMemoID int32            `json:"relatedMemoId"`
	Type          MemoRelationType `json:"type"`
}

type UpsertMemoRelationRequest struct {
	RelatedMemoID int32            `json:"relatedMemoId"`
	Type          MemoRelationType `json:"type"`
}

func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
	g.GET("/memo/:memoId/relation", s.GetMemoRelationList)
	g.POST("/memo/:memoId/relation", s.CreateMemoRelation)
	g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", s.DeleteMemoRelation)
}

// GetMemoRelationList godoc
//
//	@Summary	Get a list of Memo Relations
//	@Tags		memo-relation
//	@Accept		json
//	@Produce	json
//	@Param		memoId	path		int						true	"ID of memo to find relations"
//	@Success	200		{object}	[]store.MemoRelation	"Memo relation information list"
//	@Failure	400		{object}	nil						"ID is not a number: %s"
//	@Failure	500		{object}	nil						"Failed to list memo relations"
//	@Router		/api/v1/memo/{memoId}/relation [GET]
func (s *APIV1Service) GetMemoRelationList(c echo.Context) error {
	ctx := c.Request().Context()
	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}

	memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
		MemoID: &memoID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
	}
	return c.JSON(http.StatusOK, memoRelationList)
}

// CreateMemoRelation godoc
//
//	@Summary		Create Memo Relation
//	@Description	Create a relation between two memos
//	@Tags			memo-relation
//	@Accept			json
//	@Produce		json
//	@Param			memoId	path		int							true	"ID of memo to relate"
//	@Param			body	body		UpsertMemoRelationRequest	true	"Memo relation object"
//	@Success		200		{object}	store.MemoRelation			"Memo relation information"
//	@Failure		400		{object}	nil							"ID is not a number: %s | Malformatted post memo relation request"
//	@Failure		500		{object}	nil							"Failed to upsert memo relation"
//	@Router			/api/v1/memo/{memoId}/relation [POST]
//
// NOTES:
// - Currently not secured
// - It's possible to create relations to memos that doesn't exist, which will trigger 404 errors when the frontend tries to load them.
// - It's possible to create multiple relations, though the interface only shows first.
func (s *APIV1Service) CreateMemoRelation(c echo.Context) error {
	ctx := c.Request().Context()
	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}

	request := &UpsertMemoRelationRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo relation request").SetInternal(err)
	}

	memoRelation, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
		MemoID:        memoID,
		RelatedMemoID: request.RelatedMemoID,
		Type:          store.MemoRelationType(request.Type),
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
	}
	return c.JSON(http.StatusOK, memoRelation)
}

// DeleteMemoRelation godoc
//
//	@Summary		Delete a Memo Relation
//	@Description	Removes a relation between two memos
//	@Tags			memo-relation
//	@Accept			json
//	@Produce		json
//	@Param			memoId			path		int					true	"ID of memo to find relations"
//	@Param			relatedMemoId	path		int					true	"ID of memo to remove relation to"
//	@Param			relationType	path		MemoRelationType	true	"Type of relation to remove"
//	@Success		200				{boolean}	true				"Memo relation deleted"
//	@Failure		400				{object}	nil					"Memo ID is not a number: %s | Related memo ID is not a number: %s"
//	@Failure		500				{object}	nil					"Failed to delete memo relation"
//	@Router			/api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType} [DELETE]
//
// NOTES:
// - Currently not secured.
// - Will always return true, even if the relation doesn't exist.
func (s *APIV1Service) DeleteMemoRelation(c echo.Context) error {
	ctx := c.Request().Context()
	memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
	}
	relatedMemoID, err := util.ConvertStringToInt32(c.Param("relatedMemoId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("relatedMemoId"))).SetInternal(err)
	}
	relationType := store.MemoRelationType(c.Param("relationType"))

	if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
		MemoID:        &memoID,
		RelatedMemoID: &relatedMemoID,
		Type:          &relationType,
	}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *MemoRelation {
	return &MemoRelation{
		MemoID:        memoRelation.MemoID,
		RelatedMemoID: memoRelation.RelatedMemoID,
		Type:          MemoRelationType(memoRelation.Type),
	}
}
package v1

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/lithammer/shortuuid/v4"
	"github.com/pkg/errors"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/plugin/storage/s3"
	"github.com/usememos/memos/store"
)

type Resource struct {
	ID   int32  `json:"id"`
	Name string `json:"name"`
	UID  string `json:"uid"`

	// Standard fields
	CreatorID int32 `json:"creatorId"`
	CreatedTs int64 `json:"createdTs"`
	UpdatedTs int64 `json:"updatedTs"`

	// Domain specific fields
	Filename     string `json:"filename"`
	Blob         []byte `json:"-"`
	InternalPath string `json:"-"`
	ExternalLink string `json:"externalLink"`
	Type         string `json:"type"`
	Size         int64  `json:"size"`
}

type CreateResourceRequest struct {
	Filename     string `json:"filename"`
	ExternalLink string `json:"externalLink"`
	Type         string `json:"type"`
}

type FindResourceRequest struct {
	ID        *int32  `json:"id"`
	CreatorID *int32  `json:"creatorId"`
	Filename  *string `json:"filename"`
}

type UpdateResourceRequest struct {
	Filename *string `json:"filename"`
}

const (
	// The upload memory buffer is 32 MiB.
	// It should be kept low, so RAM usage doesn't get out of control.
	// This is unrelated to maximum upload size limit, which is now set through system setting.
	maxUploadBufferSizeBytes = 32 << 20
	MebiByte                 = 1024 * 1024
)

var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)

func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
	g.GET("/resource", s.GetResourceList)
	g.POST("/resource", s.CreateResource)
	g.POST("/resource/blob", s.UploadResource)
	g.PATCH("/resource/:resourceId", s.UpdateResource)
	g.DELETE("/resource/:resourceId", s.DeleteResource)
}

// GetResourceList godoc
//
//	@Summary	Get a list of resources
//	@Tags		resource
//	@Produce	json
//	@Param		limit	query		int					false	"Limit"
//	@Param		offset	query		int					false	"Offset"
//	@Success	200		{object}	[]store.Resource	"Resource list"
//	@Failure	401		{object}	nil					"Missing user in session"
//	@Failure	500		{object}	nil					"Failed to fetch resource list"
//	@Router		/api/v1/resource [GET]
func (s *APIV1Service) GetResourceList(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}
	find := &store.FindResource{
		CreatorID: &userID,
	}
	if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
		find.Limit = &limit
	}
	if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
		find.Offset = &offset
	}

	list, err := s.Store.ListResources(ctx, find)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
	}
	resourceMessageList := []*Resource{}
	for _, resource := range list {
		resourceMessageList = append(resourceMessageList, convertResourceFromStore(resource))
	}
	return c.JSON(http.StatusOK, resourceMessageList)
}

// CreateResource godoc
//
//	@Summary	Create resource
//	@Tags		resource
//	@Accept		json
//	@Produce	json
//	@Param		body	body		CreateResourceRequest	true	"Request object."
//	@Success	200		{object}	store.Resource			"Created resource"
//	@Failure	400		{object}	nil						"Malformatted post resource request | Invalid external link | Invalid external link scheme | Failed to request %s | Failed to read %s | Failed to read mime from %s"
//	@Failure	401		{object}	nil						"Missing user in session"
//	@Failure	500		{object}	nil						"Failed to save resource | Failed to create resource | Failed to create activity"
//	@Router		/api/v1/resource [POST]
func (s *APIV1Service) CreateResource(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	request := &CreateResourceRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
	}

	create := &store.Resource{
		UID:          shortuuid.New(),
		CreatorID:    userID,
		Filename:     request.Filename,
		ExternalLink: request.ExternalLink,
		Type:         request.Type,
	}
	if request.ExternalLink != "" {
		// Only allow those external links scheme with http/https
		linkURL, err := url.Parse(request.ExternalLink)
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err)
		}
		if linkURL.Scheme != "http" && linkURL.Scheme != "https" {
			return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme")
		}
	}

	resource, err := s.Store.CreateResource(ctx, create)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
	}
	return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}

// UploadResource godoc
//
//	@Summary	Upload resource
//	@Tags		resource
//	@Accept		multipart/form-data
//	@Produce	json
//	@Param		file	formData	file			true	"File to upload"
//	@Success	200		{object}	store.Resource	"Created resource"
//	@Failure	400		{object}	nil				"Upload file not found | File size exceeds allowed limit of %d MiB | Failed to parse upload data"
//	@Failure	401		{object}	nil				"Missing user in session"
//	@Failure	500		{object}	nil				"Failed to get uploading file | Failed to open file | Failed to save resource | Failed to create resource | Failed to create activity"
//	@Router		/api/v1/resource/blob [POST]
func (s *APIV1Service) UploadResource(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	maxUploadSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingMaxUploadSizeMiBName.String()})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get max upload size").SetInternal(err)
	}
	var settingMaxUploadSizeBytes int
	if maxUploadSetting != nil {
		if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting.Value); err == nil {
			settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
		} else {
			settingMaxUploadSizeBytes = 0
		}
	} else {
		// Default to 32 MiB.
		settingMaxUploadSizeBytes = 32 * MebiByte
	}

	file, err := c.FormFile("file")
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
	}
	if file == nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
	}

	if file.Size > int64(settingMaxUploadSizeBytes) {
		message := fmt.Sprintf("File size exceeds allowed limit of %d MiB", settingMaxUploadSizeBytes/MebiByte)
		return echo.NewHTTPError(http.StatusBadRequest, message).SetInternal(err)
	}
	if err := c.Request().ParseMultipartForm(maxUploadBufferSizeBytes); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse upload data").SetInternal(err)
	}

	sourceFile, err := file.Open()
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to open file").SetInternal(err)
	}
	defer sourceFile.Close()

	create := &store.Resource{
		UID:       shortuuid.New(),
		CreatorID: userID,
		Filename:  file.Filename,
		Type:      file.Header.Get("Content-Type"),
		Size:      file.Size,
	}
	err = SaveResourceBlob(ctx, s.Store, create, sourceFile)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save resource").SetInternal(err)
	}

	resource, err := s.Store.CreateResource(ctx, create)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
	}
	return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}

// DeleteResource godoc
//
//	@Summary	Delete a resource
//	@Tags		resource
//	@Produce	json
//	@Param		resourceId	path		int		true	"Resource ID"
//	@Success	200			{boolean}	true	"Resource deleted"
//	@Failure	400			{object}	nil		"ID is not a number: %s"
//	@Failure	401			{object}	nil		"Missing user in session"
//	@Failure	404			{object}	nil		"Resource not found: %d"
//	@Failure	500			{object}	nil		"Failed to find resource | Failed to delete resource"
//	@Router		/api/v1/resource/{resourceId} [DELETE]
func (s *APIV1Service) DeleteResource(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
	}

	resource, err := s.Store.GetResource(ctx, &store.FindResource{
		ID:        &resourceID,
		CreatorID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
	}
	if resource == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
	}

	if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
		ID: resourceID,
	}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

// UpdateResource godoc
//
//	@Summary	Update a resource
//	@Tags		resource
//	@Produce	json
//	@Param		resourceId	path		int						true	"Resource ID"
//	@Param		patch		body		UpdateResourceRequest	true	"Patch resource request"
//	@Success	200			{object}	store.Resource			"Updated resource"
//	@Failure	400			{object}	nil						"ID is not a number: %s | Malformatted patch resource request"
//	@Failure	401			{object}	nil						"Missing user in session | Unauthorized"
//	@Failure	404			{object}	nil						"Resource not found: %d"
//	@Failure	500			{object}	nil						"Failed to find resource | Failed to patch resource"
//	@Router		/api/v1/resource/{resourceId} [PATCH]
func (s *APIV1Service) UpdateResource(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
	}

	resource, err := s.Store.GetResource(ctx, &store.FindResource{
		ID: &resourceID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
	}
	if resource == nil {
		return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
	}
	if resource.CreatorID != userID {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	request := &UpdateResourceRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
	}

	currentTs := time.Now().Unix()
	update := &store.UpdateResource{
		ID:        resourceID,
		UpdatedTs: &currentTs,
	}
	if request.Filename != nil && *request.Filename != "" {
		update.Filename = request.Filename
	}

	resource, err = s.Store.UpdateResource(ctx, update)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
	}
	return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}

func replacePathTemplate(path, filename string) string {
	t := time.Now()
	path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
		switch s {
		case "{filename}":
			return filename
		case "{timestamp}":
			return fmt.Sprintf("%d", t.Unix())
		case "{year}":
			return fmt.Sprintf("%d", t.Year())
		case "{month}":
			return fmt.Sprintf("%02d", t.Month())
		case "{day}":
			return fmt.Sprintf("%02d", t.Day())
		case "{hour}":
			return fmt.Sprintf("%02d", t.Hour())
		case "{minute}":
			return fmt.Sprintf("%02d", t.Minute())
		case "{second}":
			return fmt.Sprintf("%02d", t.Second())
		case "{uuid}":
			return util.GenUUID()
		}
		return s
	})
	return path
}

func convertResourceFromStore(resource *store.Resource) *Resource {
	return &Resource{
		ID:           resource.ID,
		Name:         fmt.Sprintf("resources/%d", resource.ID),
		UID:          resource.UID,
		CreatorID:    resource.CreatorID,
		CreatedTs:    resource.CreatedTs,
		UpdatedTs:    resource.UpdatedTs,
		Filename:     resource.Filename,
		Blob:         resource.Blob,
		InternalPath: resource.InternalPath,
		ExternalLink: resource.ExternalLink,
		Type:         resource.Type,
		Size:         resource.Size,
	}
}

// SaveResourceBlob save the blob of resource based on the storage config
//
// Depend on the storage config, some fields of *store.ResourceCreate will be changed:
// 1. *DatabaseStorage*: `create.Blob`.
// 2. *LocalStorage*: `create.InternalPath`.
// 3. Others( external service): `create.ExternalLink`.
func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource, r io.Reader) error {
	systemSettingStorageServiceID, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
	if err != nil {
		return errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")
	}

	storageServiceID := DefaultStorage
	if systemSettingStorageServiceID != nil {
		err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
		if err != nil {
			return errors.Wrap(err, "Failed to unmarshal storage service id")
		}
	}

	// `DatabaseStorage` means store blob into database
	if storageServiceID == DatabaseStorage {
		fileBytes, err := io.ReadAll(r)
		if err != nil {
			return errors.Wrap(err, "Failed to read file")
		}
		create.Blob = fileBytes
		return nil
	} else if storageServiceID == LocalStorage {
		// `LocalStorage` means save blob into local disk
		systemSettingLocalStoragePath, err := s.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingLocalStoragePathName.String()})
		if err != nil {
			return errors.Wrap(err, "Failed to find SystemSettingLocalStoragePathName")
		}
		localStoragePath := "assets/{timestamp}_{filename}"
		if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
			err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
			if err != nil {
				return errors.Wrap(err, "Failed to unmarshal SystemSettingLocalStoragePathName")
			}
		}

		internalPath := localStoragePath
		if !strings.Contains(internalPath, "{filename}") {
			internalPath = filepath.Join(internalPath, "{filename}")
		}
		internalPath = replacePathTemplate(internalPath, create.Filename)
		internalPath = filepath.ToSlash(internalPath)
		create.InternalPath = internalPath

		osPath := filepath.FromSlash(internalPath)
		if !filepath.IsAbs(osPath) {
			osPath = filepath.Join(s.Profile.Data, osPath)
		}
		dir := filepath.Dir(osPath)
		if err = os.MkdirAll(dir, os.ModePerm); err != nil {
			return errors.Wrap(err, "Failed to create directory")
		}
		dst, err := os.Create(osPath)
		if err != nil {
			return errors.Wrap(err, "Failed to create file")
		}
		defer dst.Close()
		_, err = io.Copy(dst, r)
		if err != nil {
			return errors.Wrap(err, "Failed to copy file")
		}

		return nil
	}

	// Others: store blob into external service, such as S3
	storage, err := s.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
	if err != nil {
		return errors.Wrap(err, "Failed to find StorageServiceID")
	}
	if storage == nil {
		return errors.Errorf("Storage %d not found", storageServiceID)
	}
	storageMessage, err := ConvertStorageFromStore(storage)
	if err != nil {
		return errors.Wrap(err, "Failed to ConvertStorageFromStore")
	}

	if storageMessage.Type != StorageS3 {
		return errors.Errorf("Unsupported storage type: %s", storageMessage.Type)
	}

	s3Config := storageMessage.Config.S3Config
	s3Client, err := s3.NewClient(ctx, &s3.Config{
		AccessKey: s3Config.AccessKey,
		SecretKey: s3Config.SecretKey,
		EndPoint:  s3Config.EndPoint,
		Region:    s3Config.Region,
		Bucket:    s3Config.Bucket,
		URLPrefix: s3Config.URLPrefix,
		URLSuffix: s3Config.URLSuffix,
		PreSign:   s3Config.PreSign,
	})
	if err != nil {
		return errors.Wrap(err, "Failed to create s3 client")
	}

	filePath := s3Config.Path
	if !strings.Contains(filePath, "{filename}") {
		filePath = filepath.Join(filePath, "{filename}")
	}
	filePath = replacePathTemplate(filePath, create.Filename)

	link, err := s3Client.UploadFile(ctx, filePath, create.Type, r)
	if err != nil {
		return errors.Wrap(err, "Failed to upload via s3 client")
	}

	create.ExternalLink = link
	return nil
}
package v1

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/store"
)

const (
	// LocalStorage means the storage service is local file system.
	LocalStorage int32 = -1
	// DatabaseStorage means the storage service is database.
	DatabaseStorage int32 = 0
	// Default storage service is database.
	DefaultStorage int32 = DatabaseStorage
)

type StorageType string

const (
	StorageS3 StorageType = "S3"
)

func (t StorageType) String() string {
	return string(t)
}

type StorageConfig struct {
	S3Config *StorageS3Config `json:"s3Config"`
}

type StorageS3Config struct {
	EndPoint  string `json:"endPoint"`
	Path      string `json:"path"`
	Region    string `json:"region"`
	AccessKey string `json:"accessKey"`
	SecretKey string `json:"secretKey"`
	Bucket    string `json:"bucket"`
	URLPrefix string `json:"urlPrefix"`
	URLSuffix string `json:"urlSuffix"`
	PreSign   bool   `json:"presign"`
}

type Storage struct {
	ID     int32          `json:"id"`
	Name   string         `json:"name"`
	Type   StorageType    `json:"type"`
	Config *StorageConfig `json:"config"`
}

type CreateStorageRequest struct {
	Name   string         `json:"name"`
	Type   StorageType    `json:"type"`
	Config *StorageConfig `json:"config"`
}

type UpdateStorageRequest struct {
	Type   StorageType    `json:"type"`
	Name   *string        `json:"name"`
	Config *StorageConfig `json:"config"`
}

func (s *APIV1Service) registerStorageRoutes(g *echo.Group) {
	g.GET("/storage", s.GetStorageList)
	g.POST("/storage", s.CreateStorage)
	g.PATCH("/storage/:storageId", s.UpdateStorage)
	g.DELETE("/storage/:storageId", s.DeleteStorage)
}

// GetStorageList godoc
//
//	@Summary	Get a list of storages
//	@Tags		storage
//	@Produce	json
//	@Success	200	{object}	[]store.Storage	"List of storages"
//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
//	@Failure	500	{object}	nil				"Failed to find user | Failed to convert storage"
//	@Router		/api/v1/storage [GET]
func (s *APIV1Service) GetStorageList(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	// We should only show storage list to host user.
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	list, err := s.Store.ListStorages(ctx, &store.FindStorage{})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
	}

	storageList := []*Storage{}
	for _, storage := range list {
		storageMessage, err := ConvertStorageFromStore(storage)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
		}
		storageList = append(storageList, storageMessage)
	}
	return c.JSON(http.StatusOK, storageList)
}

// CreateStorage godoc
//
//	@Summary	Create storage
//	@Tags		storage
//	@Accept		json
//	@Produce	json
//	@Param		body	body		CreateStorageRequest	true	"Request object."
//	@Success	200		{object}	store.Storage			"Created storage"
//	@Failure	400		{object}	nil						"Malformatted post storage request"
//	@Failure	401		{object}	nil						"Missing user in session"
//	@Failure	500		{object}	nil						"Failed to find user | Failed to create storage | Failed to convert storage"
//	@Router		/api/v1/storage [POST]
func (s *APIV1Service) CreateStorage(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	create := &CreateStorageRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
	}

	configString := ""
	if create.Type == StorageS3 && create.Config.S3Config != nil {
		configBytes, err := json.Marshal(create.Config.S3Config)
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
		}
		configString = string(configBytes)
	}

	storage, err := s.Store.CreateStorage(ctx, &store.Storage{
		Name:   create.Name,
		Type:   create.Type.String(),
		Config: configString,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
	}
	storageMessage, err := ConvertStorageFromStore(storage)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
	}
	return c.JSON(http.StatusOK, storageMessage)
}

// DeleteStorage godoc
//
//	@Summary	Delete a storage
//	@Tags		storage
//	@Produce	json
//	@Param		storageId	path		int		true	"Storage ID"
//	@Success	200			{boolean}	true	"Storage deleted"
//	@Failure	400			{object}	nil		"ID is not a number: %s | Storage service %d is using"
//	@Failure	401			{object}	nil		"Missing user in session | Unauthorized"
//	@Failure	500			{object}	nil		"Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
//	@Router		/api/v1/storage/{storageId} [DELETE]
//
// NOTES:
// - error message "Storage service %d is using" probably should be "Storage service %d is in use".
func (s *APIV1Service) DeleteStorage(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
	}

	systemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{Name: SystemSettingStorageServiceIDName.String()})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
	}
	if systemSetting != nil {
		storageServiceID := DefaultStorage
		err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
		}
		if storageServiceID == storageID {
			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
		}
	}

	if err = s.Store.DeleteStorage(ctx, &store.DeleteStorage{ID: storageID}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

// UpdateStorage godoc
//
//	@Summary	Update a storage
//	@Tags		storage
//	@Produce	json
//	@Param		storageId	path		int						true	"Storage ID"
//	@Param		patch		body		UpdateStorageRequest	true	"Patch request"
//	@Success	200			{object}	store.Storage			"Updated resource"
//	@Failure	400			{object}	nil						"ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
//	@Failure	401			{object}	nil						"Missing user in session | Unauthorized"
//	@Failure	500			{object}	nil						"Failed to find user | Failed to patch storage | Failed to convert storage"
//	@Router		/api/v1/storage/{storageId} [PATCH]
func (s *APIV1Service) UpdateStorage(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
	}

	update := &UpdateStorageRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(update); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
	}
	storageUpdate := &store.UpdateStorage{
		ID: storageID,
	}
	if update.Name != nil {
		storageUpdate.Name = update.Name
	}
	if update.Config != nil {
		if update.Type == StorageS3 {
			configBytes, err := json.Marshal(update.Config.S3Config)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
			}
			configString := string(configBytes)
			storageUpdate.Config = &configString
		}
	}

	storage, err := s.Store.UpdateStorage(ctx, storageUpdate)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
	}
	storageMessage, err := ConvertStorageFromStore(storage)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
	}
	return c.JSON(http.StatusOK, storageMessage)
}

func ConvertStorageFromStore(storage *store.Storage) (*Storage, error) {
	storageMessage := &Storage{
		ID:     storage.ID,
		Name:   storage.Name,
		Type:   StorageType(storage.Type),
		Config: &StorageConfig{},
	}
	if storageMessage.Type == StorageS3 {
		s3Config := &StorageS3Config{}
		if err := json.Unmarshal([]byte(storage.Config), s3Config); err != nil {
			return nil, err
		}
		storageMessage.Config = &StorageConfig{
			S3Config: s3Config,
		}
	}
	return storageMessage, nil
}
# memos API
A privacy-first, lightweight note-taking service.

## Version: 1.0

**Contact information:**  
API Support  
https://github.com/orgs/usememos/discussions  

**License:** [MIT License](https://github.com/usememos/memos/blob/main/LICENSE)

[Find out more about Memos.](https://usememos.com/)

### /api/v1/auth/signin

#### POST
##### Summary

Sign-in to memos.

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Sign-in object | Yes | [github_com_usememos_memos_api_v1.SignIn](#github_com_usememos_memos_api_v1signin) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | User information | [store.User](#storeuser) |
| 400 | Malformatted signin request |  |
| 401 | Password login is deactivated \| Incorrect login credentials, please try again |  |
| 403 | User has been archived with username %s |  |
| 500 | Failed to find system setting \| Failed to unmarshal system setting \| Incorrect login credentials, please try again \| Failed to generate tokens \| Failed to create activity |  |

### /api/v1/auth/signin/sso

#### POST
##### Summary

Sign-in to memos using SSO.

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | SSO sign-in object | Yes | [github_com_usememos_memos_api_v1.SSOSignIn](#github_com_usememos_memos_api_v1ssosignin) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | User information | [store.User](#storeuser) |
| 400 | Malformatted signin request |  |
| 401 | Access denied, identifier does not match the filter. |  |
| 403 | User has been archived with username {username} |  |
| 404 | Identity provider not found |  |
| 500 | Failed to find identity provider \| Failed to create identity provider instance \| Failed to exchange token \| Failed to get user info \| Failed to compile identifier filter \| Incorrect login credentials, please try again \| Failed to generate random password \| Failed to generate password hash \| Failed to create user \| Failed to generate tokens \| Failed to create activity |  |

### /api/v1/auth/signout

#### POST
##### Summary

Sign-out from memos.

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Sign-out success | boolean |

### /api/v1/auth/signup

#### POST
##### Summary

Sign-up to memos.

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Sign-up object | Yes | [github_com_usememos_memos_api_v1.SignUp](#github_com_usememos_memos_api_v1signup) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | User information | [store.User](#storeuser) |
| 400 | Malformatted signup request \| Failed to find users |  |
| 401 | signup is disabled |  |
| 403 | Forbidden |  |
| 404 | Not found |  |
| 500 | Failed to find system setting \| Failed to unmarshal system setting allow signup \| Failed to generate password hash \| Failed to create user \| Failed to generate tokens \| Failed to create activity |  |

### /api/v1/idp

#### GET
##### Summary

Get a list of identity providers

##### Description

*clientSecret is only available for host user

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | List of available identity providers | [ [api_v1.IdentityProvider](#api_v1identityprovider) ] |
| 500 | Failed to find identity provider list \| Failed to find user |  |

#### POST
##### Summary

Create Identity Provider

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Identity provider information | Yes | [api_v1.CreateIdentityProviderRequest](#api_v1createidentityproviderrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Identity provider information | [store.IdentityProvider](#storeidentityprovider) |
| 400 | Malformatted post identity provider request |  |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to create identity provider |  |

### /api/v1/idp/{idpId}

#### DELETE
##### Summary

Delete an identity provider by ID

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| idpId | path | Identity Provider ID | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Identity Provider deleted | boolean |
| 400 | ID is not a number: %s \| Malformatted patch identity provider request |  |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to patch identity provider |  |

#### GET
##### Summary

Get an identity provider by ID

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| idpId | path | Identity provider ID | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Requested identity provider | [store.IdentityProvider](#storeidentityprovider) |
| 400 | ID is not a number: %s |  |
| 401 | Missing user in session \| Unauthorized |  |
| 404 | Identity provider not found |  |
| 500 | Failed to find identity provider list \| Failed to find user |  |

#### PATCH
##### Summary

Update an identity provider by ID

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| idpId | path | Identity Provider ID | Yes | integer |
| body | body | Patched identity provider information | Yes | [api_v1.UpdateIdentityProviderRequest](#api_v1updateidentityproviderrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Patched identity provider | [store.IdentityProvider](#storeidentityprovider) |
| 400 | ID is not a number: %s \| Malformatted patch identity provider request |  |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to patch identity provider |  |

### /api/v1/memo

#### GET
##### Summary

Get a list of memos matching optional filters

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| creatorId | query | Creator ID | No | integer |
| creatorUsername | query | Creator username | No | string |
| rowStatus | query | Row status | No | string |
| pinned | query | Pinned | No | boolean |
| tag | query | Search for tag. Do not append # | No | string |
| content | query | Search for content | No | string |
| limit | query | Limit | No | integer |
| offset | query | Offset | No | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo list | [ [store.Memo](#storememo) ] |
| 400 | Missing user to find memo |  |
| 500 | Failed to get memo display with updated ts setting value \| Failed to fetch memo list \| Failed to compose memo response |  |

#### POST
##### Summary

Create a memo

##### Description

Visibility can be PUBLIC, PROTECTED or PRIVATE
*You should omit fields to use their default values

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.CreateMemoRequest](#github_com_usememos_memos_api_v1creatememorequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Stored memo | [store.Memo](#storememo) |
| 400 | Malformatted post memo request \| Content size overflow, up to 1MB |  |
| 401 | Missing user in session |  |
| 404 | User not found \| Memo not found: %d |  |
| 500 | Failed to find user setting \| Failed to unmarshal user setting value \| Failed to find system setting \| Failed to unmarshal system setting \| Failed to find user \| Failed to create memo \| Failed to create activity \| Failed to upsert memo resource \| Failed to upsert memo relation \| Failed to compose memo \| Failed to compose memo response |  |

### /api/v1/memo/{memoId}

#### DELETE
##### Summary

Delete memo by ID

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | Memo ID to delete | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo deleted | boolean |
| 400 | ID is not a number: %s |  |
| 401 | Missing user in session \| Unauthorized |  |
| 404 | Memo not found: %d |  |
| 500 | Failed to find memo \| Failed to delete memo ID: %v |  |

#### GET
##### Summary

Get memo by ID

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | Memo ID | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo list | [ [store.Memo](#storememo) ] |
| 400 | ID is not a number: %s |  |
| 401 | Missing user in session |  |
| 403 | this memo is private only \| this memo is protected, missing user in session |  |
| 404 | Memo not found: %d |  |
| 500 | Failed to find memo by ID: %v \| Failed to compose memo response |  |

#### PATCH
##### Summary

Update a memo

##### Description

Visibility can be PUBLIC, PROTECTED or PRIVATE
*You should omit fields to use their default values

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | ID of memo to update | Yes | integer |
| body | body | Patched object. | Yes | [github_com_usememos_memos_api_v1.PatchMemoRequest](#github_com_usememos_memos_api_v1patchmemorequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Stored memo | [store.Memo](#storememo) |
| 400 | ID is not a number: %s \| Malformatted patch memo request \| Content size overflow, up to 1MB |  |
| 401 | Missing user in session \| Unauthorized |  |
| 404 | Memo not found: %d |  |
| 500 | Failed to find memo \| Failed to patch memo \| Failed to upsert memo resource \| Failed to delete memo resource \| Failed to compose memo response |  |

### /api/v1/memo/all

#### GET
##### Summary

Get a list of public memos matching optional filters

##### Description

This should also list protected memos if the user is logged in
Authentication is optional

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| limit | query | Limit | No | integer |
| offset | query | Offset | No | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo list | [ [store.Memo](#storememo) ] |
| 500 | Failed to get memo display with updated ts setting value \| Failed to fetch all memo list \| Failed to compose memo response |  |

### /api/v1/memo/stats

#### GET
##### Summary

Get memo stats by creator ID or username

##### Description

Used to generate the heatmap

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| creatorId | query | Creator ID | No | integer |
| creatorUsername | query | Creator username | No | string |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo createdTs list | [ integer ] |
| 400 | Missing user id to find memo |  |
| 500 | Failed to get memo display with updated ts setting value \| Failed to find memo list \| Failed to compose memo response |  |

### /api/v1/memo/{memoId}/organizer

#### POST
##### Summary

Organize memo (pin/unpin)

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | ID of memo to organize | Yes | integer |
| body | body | Memo organizer object | Yes | [github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest](#github_com_usememos_memos_api_v1upsertmemoorganizerrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo information | [store.Memo](#storememo) |
| 400 | ID is not a number: %s \| Malformatted post memo organizer request |  |
| 401 | Missing user in session \| Unauthorized |  |
| 404 | Memo not found: %v |  |
| 500 | Failed to find memo \| Failed to upsert memo organizer \| Failed to find memo by ID: %v \| Failed to compose memo response |  |

### /api/v1/memo/{memoId}/relation

#### GET
##### Summary

Get a list of Memo Relations

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | ID of memo to find relations | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo relation information list | [ [store.MemoRelation](#storememorelation) ] |
| 400 | ID is not a number: %s |  |
| 500 | Failed to list memo relations |  |

#### POST
##### Summary

Create Memo Relation

##### Description

Create a relation between two memos

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | ID of memo to relate | Yes | integer |
| body | body | Memo relation object | Yes | [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo relation information | [store.MemoRelation](#storememorelation) |
| 400 | ID is not a number: %s \| Malformatted post memo relation request |  |
| 500 | Failed to upsert memo relation |  |

### /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType}

#### DELETE
##### Summary

Delete a Memo Relation

##### Description

Removes a relation between two memos

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| memoId | path | ID of memo to find relations | Yes | integer |
| relatedMemoId | path | ID of memo to remove relation to | Yes | integer |
| relationType | path | Type of relation to remove | Yes | string |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Memo relation deleted | boolean |
| 400 | Memo ID is not a number: %s \| Related memo ID is not a number: %s |  |
| 500 | Failed to delete memo relation |  |

### /api/v1/ping

#### GET
##### Summary

Ping the system

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | If succeed to ping the system | boolean |

### /api/v1/status

#### GET
##### Summary

Get system GetSystemStatus

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | System GetSystemStatus | [api_v1.SystemStatus](#api_v1systemstatus) |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find host user \| Failed to find system setting list \| Failed to unmarshal system setting customized profile value |  |

### /api/v1/system/vacuum

#### POST
##### Summary

Vacuum the database

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Database vacuumed | boolean |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to ExecVacuum database |  |

### /api/v1/resource

#### GET
##### Summary

Get a list of resources

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| limit | query | Limit | No | integer |
| offset | query | Offset | No | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Resource list | [ [store.Resource](#storeresource) ] |
| 401 | Missing user in session |  |
| 500 | Failed to fetch resource list |  |

#### POST
##### Summary

Create resource

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object. | Yes | [api_v1.CreateResourceRequest](#api_v1createresourcerequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Created resource | [store.Resource](#storeresource) |
| 400 | Malformatted post resource request \| Invalid external link \| Invalid external link scheme \| Failed to request %s \| Failed to read %s \| Failed to read mime from %s |  |
| 401 | Missing user in session |  |
| 500 | Failed to save resource \| Failed to create resource \| Failed to create activity |  |

### /api/v1/resource/{resourceId}

#### DELETE
##### Summary

Delete a resource

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| resourceId | path | Resource ID | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Resource deleted | boolean |
| 400 | ID is not a number: %s |  |
| 401 | Missing user in session |  |
| 404 | Resource not found: %d |  |
| 500 | Failed to find resource \| Failed to delete resource |  |

#### PATCH
##### Summary

Update a resource

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| resourceId | path | Resource ID | Yes | integer |
| patch | body | Patch resource request | Yes | [api_v1.UpdateResourceRequest](#api_v1updateresourcerequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Updated resource | [store.Resource](#storeresource) |
| 400 | ID is not a number: %s \| Malformatted patch resource request |  |
| 401 | Missing user in session \| Unauthorized |  |
| 404 | Resource not found: %d |  |
| 500 | Failed to find resource \| Failed to patch resource |  |

### /api/v1/resource/blob

#### POST
##### Summary

Upload resource

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| file | formData | File to upload | Yes | file |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Created resource | [store.Resource](#storeresource) |
| 400 | Upload file not found \| File size exceeds allowed limit of %d MiB \| Failed to parse upload data |  |
| 401 | Missing user in session |  |
| 500 | Failed to get uploading file \| Failed to open file \| Failed to save resource \| Failed to create resource \| Failed to create activity |  |

### /api/v1/storage

#### GET
##### Summary

Get a list of storages

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | List of storages | [ [store.Storage](#storestorage) ] |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to convert storage |  |

#### POST
##### Summary

Create storage

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.CreateStorageRequest](#github_com_usememos_memos_api_v1createstoragerequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Created storage | [store.Storage](#storestorage) |
| 400 | Malformatted post storage request |  |
| 401 | Missing user in session |  |
| 500 | Failed to find user \| Failed to create storage \| Failed to convert storage |  |

### /api/v1/storage/{storageId}

#### DELETE
##### Summary

Delete a storage

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| storageId | path | Storage ID | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Storage deleted | boolean |
| 400 | ID is not a number: %s \| Storage service %d is using |  |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to find storage \| Failed to unmarshal storage service id \| Failed to delete storage |  |

#### PATCH
##### Summary

Update a storage

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| storageId | path | Storage ID | Yes | integer |
| patch | body | Patch request | Yes | [github_com_usememos_memos_api_v1.UpdateStorageRequest](#github_com_usememos_memos_api_v1updatestoragerequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Updated resource | [store.Storage](#storestorage) |
| 400 | ID is not a number: %s \| Malformatted patch storage request \| Malformatted post storage request |  |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to patch storage \| Failed to convert storage |  |

### /api/v1/system/setting

#### GET
##### Summary

Get a list of system settings

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | System setting list | [ [api_v1.SystemSetting](#api_v1systemsetting) ] |
| 401 | Missing user in session \| Unauthorized |  |
| 500 | Failed to find user \| Failed to find system setting list |  |

#### POST
##### Summary

Create system setting

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object. | Yes | [api_v1.UpsertSystemSettingRequest](#api_v1upsertsystemsettingrequest) |

##### Responses

| Code | Description |
| ---- | ----------- |
| 400 | Malformatted post system setting request \| invalid system setting |
| 401 | Missing user in session \| Unauthorized |
| 403 | Cannot disable passwords if no SSO identity provider is configured. |
| 500 | Failed to find user \| Failed to upsert system setting |

### /api/v1/tag

#### GET
##### Summary

Get a list of tags

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Tag list | [ string ] |
| 400 | Missing user id to find tag |  |
| 500 | Failed to find tag list |  |

#### POST
##### Summary

Create a tag

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.UpsertTagRequest](#github_com_usememos_memos_api_v1upserttagrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Created tag name | string |
| 400 | Malformatted post tag request \| Tag name shouldn't be empty |  |
| 401 | Missing user in session |  |
| 500 | Failed to upsert tag \| Failed to create activity |  |

### /api/v1/tag/delete

#### POST
##### Summary

Delete a tag

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object. | Yes | [github_com_usememos_memos_api_v1.DeleteTagRequest](#github_com_usememos_memos_api_v1deletetagrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Tag deleted | boolean |
| 400 | Malformatted post tag request \| Tag name shouldn't be empty |  |
| 401 | Missing user in session |  |
| 500 | Failed to delete tag name: %v |  |

### /api/v1/tag/suggestion

#### GET
##### Summary

Get a list of tags suggested from other memos contents

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Tag list | [ string ] |
| 400 | Missing user session |  |
| 500 | Failed to find memo list \| Failed to find tag list |  |

### /api/v1/user

#### GET
##### Summary

Get a list of users

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | User list | [ [store.User](#storeuser) ] |
| 500 | Failed to fetch user list |  |

#### POST
##### Summary

Create a user

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| body | body | Request object | Yes | [api_v1.CreateUserRequest](#api_v1createuserrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Created user | [store.User](#storeuser) |
| 400 | Malformatted post user request \| Invalid user create format |  |
| 401 | Missing auth session \| Unauthorized to create user |  |
| 403 | Could not create host user |  |
| 500 | Failed to find user by id \| Failed to generate password hash \| Failed to create user \| Failed to create activity |  |

### /api/v1/user/{id}

#### DELETE
##### Summary

Delete a user

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| id | path | User ID | Yes | string |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | User deleted | boolean |
| 400 | ID is not a number: %s \| Current session user not found with ID: %d |  |
| 401 | Missing user in session |  |
| 403 | Unauthorized to delete user |  |
| 500 | Failed to find user \| Failed to delete user |  |

#### GET
##### Summary

Get user by id

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| id | path | User ID | Yes | integer |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Requested user | [store.User](#storeuser) |
| 400 | Malformatted user id |  |
| 404 | User not found |  |
| 500 | Failed to find user |  |

#### PATCH
##### Summary

Update a user

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| id | path | User ID | Yes | string |
| patch | body | Patch request | Yes | [api_v1.UpdateUserRequest](#api_v1updateuserrequest) |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Updated user | [store.User](#storeuser) |
| 400 | ID is not a number: %s \| Current session user not found with ID: %d \| Malformatted patch user request \| Invalid update user request |  |
| 401 | Missing user in session |  |
| 403 | Unauthorized to update user |  |
| 500 | Failed to find user \| Failed to generate password hash \| Failed to patch user \| Failed to find userSettingList |  |

### /api/v1/user/me

#### GET
##### Summary

Get current user

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Current user | [store.User](#storeuser) |
| 401 | Missing auth session |  |
| 500 | Failed to find user \| Failed to find userSettingList |  |

### /api/v1/user/name/{username}

#### GET
##### Summary

Get user by username

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| username | path | Username | Yes | string |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Requested user | [store.User](#storeuser) |
| 404 | User not found |  |
| 500 | Failed to find user |  |

### /o/get/GetImage

#### GET
##### Summary

Get GetImage from URL

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| url | query | Image url | Yes | string |

##### Responses

| Code | Description |
| ---- | ----------- |
| 200 | Image |
| 400 | Missing GetImage url \| Wrong url \| Failed to get GetImage url: %s |
| 500 | Failed to write GetImage blob |

### Models

#### api_v1.CreateIdentityProviderRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) |  | No |

#### api_v1.CreateMemoRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string |  | No |
| createdTs | integer |  | No |
| relationList | [ [api_v1.UpsertMemoRelationRequest](#api_v1upsertmemorelationrequest) ] |  | No |
| resourceIdList | [ integer ] | Related fields | No |
| visibility | [api_v1.Visibility](#api_v1visibility) | Domain specific fields | No |

#### api_v1.CreateResourceRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| externalLink | string |  | No |
| filename | string |  | No |
| type | string |  | No |

#### api_v1.CreateStorageRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [api_v1.StorageConfig](#api_v1storageconfig) |  | No |
| name | string |  | No |
| type | [api_v1.StorageType](#api_v1storagetype) |  | No |

#### api_v1.CreateUserRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| email | string |  | No |
| nickname | string |  | No |
| password | string |  | No |
| role | [api_v1.Role](#api_v1role) |  | No |
| username | string |  | No |

#### api_v1.CustomizedProfile

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| appearance | string | Appearance is the server default appearance. | No |
| description | string | Description is the server description. | No |
| locale | string | Locale is the server default locale. | No |
| logoUrl | string | LogoURL is the url of logo image. | No |
| name | string | Name is the server name, default is `memos` | No |

#### api_v1.DeleteTagRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| name | string |  | No |

#### api_v1.FieldMapping

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| displayName | string |  | No |
| email | string |  | No |
| identifier | string |  | No |

#### api_v1.IdentityProvider

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) |  | No |
| id | integer |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) |  | No |

#### api_v1.IdentityProviderConfig

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| oauth2Config | [api_v1.IdentityProviderOAuth2Config](#api_v1identityprovideroauth2config) |  | No |

#### api_v1.IdentityProviderOAuth2Config

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| authUrl | string |  | No |
| clientId | string |  | No |
| clientSecret | string |  | No |
| fieldMapping | [api_v1.FieldMapping](#api_v1fieldmapping) |  | No |
| scopes | [ string ] |  | No |
| tokenUrl | string |  | No |
| userInfoUrl | string |  | No |

#### api_v1.IdentityProviderType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.IdentityProviderType | string |  |  |

#### api_v1.MemoRelationType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.MemoRelationType | string |  |  |

#### api_v1.PatchMemoRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | Domain specific fields | No |
| createdTs | integer | Standard fields | No |
| relationList | [ [api_v1.UpsertMemoRelationRequest](#api_v1upsertmemorelationrequest) ] |  | No |
| resourceIdList | [ integer ] | Related fields | No |
| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) |  | No |
| updatedTs | integer |  | No |
| visibility | [api_v1.Visibility](#api_v1visibility) |  | No |

#### api_v1.Role

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.Role | string |  |  |

#### api_v1.RowStatus

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.RowStatus | string |  |  |

#### api_v1.SSOSignIn

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| code | string |  | No |
| identityProviderId | integer |  | No |
| redirectUri | string |  | No |

#### api_v1.SignIn

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| password | string |  | No |
| remember | boolean |  | No |
| username | string |  | No |

#### api_v1.SignUp

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| password | string |  | No |
| username | string |  | No |

#### api_v1.StorageConfig

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| s3Config | [api_v1.StorageS3Config](#api_v1storages3config) |  | No |

#### api_v1.StorageS3Config

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| accessKey | string |  | No |
| bucket | string |  | No |
| endPoint | string |  | No |
| path | string |  | No |
| presign | boolean |  | No |
| region | string |  | No |
| secretKey | string |  | No |
| urlPrefix | string |  | No |
| urlSuffix | string |  | No |

#### api_v1.StorageType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.StorageType | string |  |  |

#### api_v1.SystemSetting

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string |  | No |
| name | [api_v1.SystemSettingName](#api_v1systemsettingname) |  | No |
| value | string | Value is a JSON string with basic value. | No |

#### api_v1.SystemSettingName

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.SystemSettingName | string |  |  |

#### api_v1.SystemStatus

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| additionalScript | string | Additional script. | No |
| additionalStyle | string | Additional style. | No |
| allowSignUp | boolean | System settings Allow sign up. | No |
| customizedProfile | [api_v1.CustomizedProfile](#api_v1customizedprofile) | Customized server profile, including server name and external url. | No |
| dbSize | integer |  | No |
| disablePasswordLogin | boolean | Disable password login. | No |
| disablePublicMemos | boolean | Disable public memos. | No |
| host | [api_v1.User](#api_v1user) |  | No |
| localStoragePath | string | Local storage path. | No |
| maxUploadSizeMiB | integer | Max upload size. | No |
| memoDisplayWithUpdatedTs | boolean | Memo display with updated timestamp. | No |
| profile | [profile.Profile](#profileprofile) |  | No |
| storageServiceId | integer | Storage service ID. | No |

#### api_v1.UpdateIdentityProviderRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [api_v1.IdentityProviderConfig](#api_v1identityproviderconfig) |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [api_v1.IdentityProviderType](#api_v1identityprovidertype) |  | No |

#### api_v1.UpdateResourceRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| filename | string |  | No |

#### api_v1.UpdateStorageRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [api_v1.StorageConfig](#api_v1storageconfig) |  | No |
| name | string |  | No |
| type | [api_v1.StorageType](#api_v1storagetype) |  | No |

#### api_v1.UpdateUserRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatarUrl | string |  | No |
| email | string |  | No |
| nickname | string |  | No |
| password | string |  | No |
| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) |  | No |
| username | string |  | No |

#### api_v1.UpsertMemoOrganizerRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| pinned | boolean |  | No |

#### api_v1.UpsertMemoRelationRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| relatedMemoId | integer |  | No |
| type | [api_v1.MemoRelationType](#api_v1memorelationtype) |  | No |

#### api_v1.UpsertSystemSettingRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string |  | No |
| name | [api_v1.SystemSettingName](#api_v1systemsettingname) |  | No |
| value | string |  | No |

#### api_v1.UpsertTagRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| name | string |  | No |

#### api_v1.User

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatarUrl | string |  | No |
| createdTs | integer |  | No |
| email | string |  | No |
| id | integer |  | No |
| nickname | string |  | No |
| role | [api_v1.Role](#api_v1role) |  | No |
| rowStatus | [api_v1.RowStatus](#api_v1rowstatus) | Standard fields | No |
| updatedTs | integer |  | No |
| username | string | Domain specific fields | No |

#### api_v1.Visibility

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_v1.Visibility | string |  |  |

#### github_com_usememos_memos_api_v1.CreateIdentityProviderRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) |  | No |

#### github_com_usememos_memos_api_v1.CreateMemoRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string |  | No |
| createdTs | integer |  | No |
| relationList | [ [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) ] |  | No |
| resourceIdList | [ integer ] | Related fields | No |
| visibility | [github_com_usememos_memos_api_v1.Visibility](#github_com_usememos_memos_api_v1visibility) | Domain specific fields | No |

#### github_com_usememos_memos_api_v1.CreateResourceRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| externalLink | string |  | No |
| filename | string |  | No |
| type | string |  | No |

#### github_com_usememos_memos_api_v1.CreateStorageRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [github_com_usememos_memos_api_v1.StorageConfig](#github_com_usememos_memos_api_v1storageconfig) |  | No |
| name | string |  | No |
| type | [github_com_usememos_memos_api_v1.StorageType](#github_com_usememos_memos_api_v1storagetype) |  | No |

#### github_com_usememos_memos_api_v1.CreateUserRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| email | string |  | No |
| nickname | string |  | No |
| password | string |  | No |
| role | [github_com_usememos_memos_api_v1.Role](#github_com_usememos_memos_api_v1role) |  | No |
| username | string |  | No |

#### github_com_usememos_memos_api_v1.CustomizedProfile

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| appearance | string | Appearance is the server default appearance. | No |
| description | string | Description is the server description. | No |
| locale | string | Locale is the server default locale. | No |
| logoUrl | string | LogoURL is the url of logo image. | No |
| name | string | Name is the server name, default is `memos` | No |

#### github_com_usememos_memos_api_v1.DeleteTagRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| name | string |  | No |

#### github_com_usememos_memos_api_v1.FieldMapping

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| displayName | string |  | No |
| email | string |  | No |
| identifier | string |  | No |

#### github_com_usememos_memos_api_v1.IdentityProvider

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) |  | No |
| id | integer |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) |  | No |

#### github_com_usememos_memos_api_v1.IdentityProviderConfig

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| oauth2Config | [github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config](#github_com_usememos_memos_api_v1identityprovideroauth2config) |  | No |

#### github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| authUrl | string |  | No |
| clientId | string |  | No |
| clientSecret | string |  | No |
| fieldMapping | [github_com_usememos_memos_api_v1.FieldMapping](#github_com_usememos_memos_api_v1fieldmapping) |  | No |
| scopes | [ string ] |  | No |
| tokenUrl | string |  | No |
| userInfoUrl | string |  | No |

#### github_com_usememos_memos_api_v1.IdentityProviderType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.IdentityProviderType | string |  |  |

#### github_com_usememos_memos_api_v1.MemoRelationType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.MemoRelationType | string |  |  |

#### github_com_usememos_memos_api_v1.PatchMemoRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | Domain specific fields | No |
| createdTs | integer | Standard fields | No |
| relationList | [ [github_com_usememos_memos_api_v1.UpsertMemoRelationRequest](#github_com_usememos_memos_api_v1upsertmemorelationrequest) ] |  | No |
| resourceIdList | [ integer ] | Related fields | No |
| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) |  | No |
| updatedTs | integer |  | No |
| visibility | [github_com_usememos_memos_api_v1.Visibility](#github_com_usememos_memos_api_v1visibility) |  | No |

#### github_com_usememos_memos_api_v1.Role

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.Role | string |  |  |

#### github_com_usememos_memos_api_v1.RowStatus

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.RowStatus | string |  |  |

#### github_com_usememos_memos_api_v1.SSOSignIn

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| code | string |  | No |
| identityProviderId | integer |  | No |
| redirectUri | string |  | No |

#### github_com_usememos_memos_api_v1.SignIn

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| password | string |  | No |
| remember | boolean |  | No |
| username | string |  | No |

#### github_com_usememos_memos_api_v1.SignUp

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| password | string |  | No |
| username | string |  | No |

#### github_com_usememos_memos_api_v1.StorageConfig

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| s3Config | [github_com_usememos_memos_api_v1.StorageS3Config](#github_com_usememos_memos_api_v1storages3config) |  | No |

#### github_com_usememos_memos_api_v1.StorageS3Config

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| accessKey | string |  | No |
| bucket | string |  | No |
| endPoint | string |  | No |
| path | string |  | No |
| presign | boolean |  | No |
| region | string |  | No |
| secretKey | string |  | No |
| urlPrefix | string |  | No |
| urlSuffix | string |  | No |

#### github_com_usememos_memos_api_v1.StorageType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.StorageType | string |  |  |

#### github_com_usememos_memos_api_v1.SystemSetting

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string |  | No |
| name | [github_com_usememos_memos_api_v1.SystemSettingName](#github_com_usememos_memos_api_v1systemsettingname) |  | No |
| value | string | Value is a JSON string with basic value. | No |

#### github_com_usememos_memos_api_v1.SystemSettingName

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.SystemSettingName | string |  |  |

#### github_com_usememos_memos_api_v1.SystemStatus

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| additionalScript | string | Additional script. | No |
| additionalStyle | string | Additional style. | No |
| allowSignUp | boolean | System settings Allow sign up. | No |
| customizedProfile | [github_com_usememos_memos_api_v1.CustomizedProfile](#github_com_usememos_memos_api_v1customizedprofile) | Customized server profile, including server name and external url. | No |
| dbSize | integer |  | No |
| disablePasswordLogin | boolean | Disable password login. | No |
| disablePublicMemos | boolean | Disable public memos. | No |
| host | [github_com_usememos_memos_api_v1.User](#github_com_usememos_memos_api_v1user) |  | No |
| localStoragePath | string | Local storage path. | No |
| maxUploadSizeMiB | integer | Max upload size. | No |
| memoDisplayWithUpdatedTs | boolean | Memo display with updated timestamp. | No |
| profile | [profile.Profile](#profileprofile) |  | No |
| storageServiceId | integer | Storage service ID. | No |

#### github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [github_com_usememos_memos_api_v1.IdentityProviderConfig](#github_com_usememos_memos_api_v1identityproviderconfig) |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [github_com_usememos_memos_api_v1.IdentityProviderType](#github_com_usememos_memos_api_v1identityprovidertype) |  | No |

#### github_com_usememos_memos_api_v1.UpdateResourceRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| filename | string |  | No |

#### github_com_usememos_memos_api_v1.UpdateStorageRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [github_com_usememos_memos_api_v1.StorageConfig](#github_com_usememos_memos_api_v1storageconfig) |  | No |
| name | string |  | No |
| type | [github_com_usememos_memos_api_v1.StorageType](#github_com_usememos_memos_api_v1storagetype) |  | No |

#### github_com_usememos_memos_api_v1.UpdateUserRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatarUrl | string |  | No |
| email | string |  | No |
| nickname | string |  | No |
| password | string |  | No |
| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) |  | No |
| username | string |  | No |

#### github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| pinned | boolean |  | No |

#### github_com_usememos_memos_api_v1.UpsertMemoRelationRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| relatedMemoId | integer |  | No |
| type | [github_com_usememos_memos_api_v1.MemoRelationType](#github_com_usememos_memos_api_v1memorelationtype) |  | No |

#### github_com_usememos_memos_api_v1.UpsertSystemSettingRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string |  | No |
| name | [github_com_usememos_memos_api_v1.SystemSettingName](#github_com_usememos_memos_api_v1systemsettingname) |  | No |
| value | string |  | No |

#### github_com_usememos_memos_api_v1.UpsertTagRequest

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| name | string |  | No |

#### github_com_usememos_memos_api_v1.User

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatarUrl | string |  | No |
| createdTs | integer |  | No |
| email | string |  | No |
| id | integer |  | No |
| nickname | string |  | No |
| role | [github_com_usememos_memos_api_v1.Role](#github_com_usememos_memos_api_v1role) |  | No |
| rowStatus | [github_com_usememos_memos_api_v1.RowStatus](#github_com_usememos_memos_api_v1rowstatus) | Standard fields | No |
| updatedTs | integer |  | No |
| username | string | Domain specific fields | No |

#### github_com_usememos_memos_api_v1.Visibility

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| github_com_usememos_memos_api_v1.Visibility | string |  |  |

#### profile.Profile

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| mode | string | Mode can be "prod" or "dev" or "demo" | No |
| version | string | Version is the current version of server | No |

#### store.FieldMapping

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| displayName | string |  | No |
| email | string |  | No |
| identifier | string |  | No |

#### store.IdentityProvider

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [store.IdentityProviderConfig](#storeidentityproviderconfig) |  | No |
| id | integer |  | No |
| identifierFilter | string |  | No |
| name | string |  | No |
| type | [store.IdentityProviderType](#storeidentityprovidertype) |  | No |

#### store.IdentityProviderConfig

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| oauth2Config | [store.IdentityProviderOAuth2Config](#storeidentityprovideroauth2config) |  | No |

#### store.IdentityProviderOAuth2Config

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| authUrl | string |  | No |
| clientId | string |  | No |
| clientSecret | string |  | No |
| fieldMapping | [store.FieldMapping](#storefieldmapping) |  | No |
| scopes | [ string ] |  | No |
| tokenUrl | string |  | No |
| userInfoUrl | string |  | No |

#### store.IdentityProviderType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| store.IdentityProviderType | string |  |  |

#### store.Memo

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | Domain specific fields | No |
| createdTs | integer |  | No |
| creatorID | integer |  | No |
| id | integer |  | No |
| parentID | integer |  | No |
| pinned | boolean | Composed fields | No |
| resourceName | string |  | No |
| rowStatus | [store.RowStatus](#storerowstatus) | Standard fields | No |
| updatedTs | integer |  | No |
| visibility | [store.Visibility](#storevisibility) |  | No |

#### store.MemoRelation

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| memoID | integer |  | No |
| relatedMemoID | integer |  | No |
| type | [store.MemoRelationType](#storememorelationtype) |  | No |

#### store.MemoRelationType

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| store.MemoRelationType | string |  |  |

#### store.Resource

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| blob | [ integer ] |  | No |
| createdTs | integer |  | No |
| creatorID | integer | Standard fields | No |
| externalLink | string |  | No |
| filename | string | Domain specific fields | No |
| id | integer |  | No |
| internalPath | string |  | No |
| memoID | integer |  | No |
| resourceName | string |  | No |
| size | integer |  | No |
| type | string |  | No |
| updatedTs | integer |  | No |

#### store.Role

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| store.Role | string |  |  |

#### store.RowStatus

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| store.RowStatus | string |  |  |

#### store.Storage

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | string |  | No |
| id | integer |  | No |
| name | string |  | No |
| type | string |  | No |

#### store.User

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| avatarURL | string |  | No |
| createdTs | integer |  | No |
| email | string |  | No |
| id | integer |  | No |
| nickname | string |  | No |
| passwordHash | string |  | No |
| role | [store.Role](#storerole) |  | No |
| rowStatus | [store.RowStatus](#storerowstatus) | Standard fields | No |
| updatedTs | integer |  | No |
| username | string | Domain specific fields | No |

#### store.Visibility

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| store.Visibility | string |  |  |
basePath: /
definitions:
  api_v1.CreateIdentityProviderRequest:
    properties:
      config:
        $ref: '#/definitions/api_v1.IdentityProviderConfig'
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/api_v1.IdentityProviderType'
    type: object
  api_v1.CreateMemoRequest:
    properties:
      content:
        type: string
      createdTs:
        type: integer
      relationList:
        items:
          $ref: '#/definitions/api_v1.UpsertMemoRelationRequest'
        type: array
      resourceIdList:
        description: Related fields
        items:
          type: integer
        type: array
      visibility:
        allOf:
        - $ref: '#/definitions/api_v1.Visibility'
        description: Domain specific fields
    type: object
  api_v1.CreateResourceRequest:
    properties:
      externalLink:
        type: string
      filename:
        type: string
      type:
        type: string
    type: object
  api_v1.CreateStorageRequest:
    properties:
      config:
        $ref: '#/definitions/api_v1.StorageConfig'
      name:
        type: string
      type:
        $ref: '#/definitions/api_v1.StorageType'
    type: object
  api_v1.CreateUserRequest:
    properties:
      email:
        type: string
      nickname:
        type: string
      password:
        type: string
      role:
        $ref: '#/definitions/api_v1.Role'
      username:
        type: string
    type: object
  api_v1.CustomizedProfile:
    properties:
      appearance:
        description: Appearance is the server default appearance.
        type: string
      description:
        description: Description is the server description.
        type: string
      locale:
        description: Locale is the server default locale.
        type: string
      logoUrl:
        description: LogoURL is the url of logo image.
        type: string
      name:
        description: Name is the server name, default is `memos`
        type: string
    type: object
  api_v1.DeleteTagRequest:
    properties:
      name:
        type: string
    type: object
  api_v1.FieldMapping:
    properties:
      displayName:
        type: string
      email:
        type: string
      identifier:
        type: string
    type: object
  api_v1.IdentityProvider:
    properties:
      config:
        $ref: '#/definitions/api_v1.IdentityProviderConfig'
      id:
        type: integer
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/api_v1.IdentityProviderType'
    type: object
  api_v1.IdentityProviderConfig:
    properties:
      oauth2Config:
        $ref: '#/definitions/api_v1.IdentityProviderOAuth2Config'
    type: object
  api_v1.IdentityProviderOAuth2Config:
    properties:
      authUrl:
        type: string
      clientId:
        type: string
      clientSecret:
        type: string
      fieldMapping:
        $ref: '#/definitions/api_v1.FieldMapping'
      scopes:
        items:
          type: string
        type: array
      tokenUrl:
        type: string
      userInfoUrl:
        type: string
    type: object
  api_v1.IdentityProviderType:
    enum:
    - OAUTH2
    type: string
    x-enum-varnames:
    - IdentityProviderOAuth2Type
  api_v1.MemoRelationType:
    enum:
    - REFERENCE
    - COMMENT
    type: string
    x-enum-varnames:
    - MemoRelationReference
    - MemoRelationComment
  api_v1.PatchMemoRequest:
    properties:
      content:
        description: Domain specific fields
        type: string
      createdTs:
        description: Standard fields
        type: integer
      relationList:
        items:
          $ref: '#/definitions/api_v1.UpsertMemoRelationRequest'
        type: array
      resourceIdList:
        description: Related fields
        items:
          type: integer
        type: array
      rowStatus:
        $ref: '#/definitions/api_v1.RowStatus'
      updatedTs:
        type: integer
      visibility:
        $ref: '#/definitions/api_v1.Visibility'
    type: object
  api_v1.Role:
    enum:
    - HOST
    - ADMIN
    - USER
    type: string
    x-enum-varnames:
    - RoleHost
    - RoleAdmin
    - RoleUser
  api_v1.RowStatus:
    enum:
    - NORMAL
    - ARCHIVED
    type: string
    x-enum-varnames:
    - Normal
    - Archived
  api_v1.SSOSignIn:
    properties:
      code:
        type: string
      identityProviderId:
        type: integer
      redirectUri:
        type: string
    type: object
  api_v1.SignIn:
    properties:
      password:
        type: string
      remember:
        type: boolean
      username:
        type: string
    type: object
  api_v1.SignUp:
    properties:
      password:
        type: string
      username:
        type: string
    type: object
  api_v1.StorageConfig:
    properties:
      s3Config:
        $ref: '#/definitions/api_v1.StorageS3Config'
    type: object
  api_v1.StorageS3Config:
    properties:
      accessKey:
        type: string
      bucket:
        type: string
      endPoint:
        type: string
      path:
        type: string
      presign:
        type: boolean
      region:
        type: string
      secretKey:
        type: string
      urlPrefix:
        type: string
      urlSuffix:
        type: string
    type: object
  api_v1.StorageType:
    enum:
    - S3
    type: string
    x-enum-varnames:
    - StorageS3
  api_v1.SystemSetting:
    properties:
      description:
        type: string
      name:
        $ref: '#/definitions/api_v1.SystemSettingName'
      value:
        description: Value is a JSON string with basic value.
        type: string
    type: object
  api_v1.SystemSettingName:
    enum:
    - server-id
    - secret-session
    - disable-public-memos
    - max-upload-size-mib
    - customized-profile
    - storage-service-id
    - local-storage-path
    - telegram-bot-token
    - memo-display-with-updated-ts
    type: string
    x-enum-varnames:
    - SystemSettingServerIDName
    - SystemSettingSecretSessionName
    - SystemSettingDisablePublicMemosName
    - SystemSettingMaxUploadSizeMiBName
    - SystemSettingCustomizedProfileName
    - SystemSettingStorageServiceIDName
    - SystemSettingLocalStoragePathName
    - SystemSettingTelegramBotTokenName
    - SystemSettingMemoDisplayWithUpdatedTsName
  api_v1.SystemStatus:
    properties:
      additionalScript:
        description: Additional script.
        type: string
      additionalStyle:
        description: Additional style.
        type: string
      allowSignUp:
        description: |-
          System settings
          Allow sign up.
        type: boolean
      customizedProfile:
        allOf:
        - $ref: '#/definitions/api_v1.CustomizedProfile'
        description: Customized server profile, including server name and external
          url.
      dbSize:
        type: integer
      disablePasswordLogin:
        description: Disable password login.
        type: boolean
      disablePublicMemos:
        description: Disable public memos.
        type: boolean
      host:
        $ref: '#/definitions/api_v1.User'
      localStoragePath:
        description: Local storage path.
        type: string
      maxUploadSizeMiB:
        description: Max upload size.
        type: integer
      memoDisplayWithUpdatedTs:
        description: Memo display with updated timestamp.
        type: boolean
      profile:
        $ref: '#/definitions/profile.Profile'
      storageServiceId:
        description: Storage service ID.
        type: integer
    type: object
  api_v1.UpdateIdentityProviderRequest:
    properties:
      config:
        $ref: '#/definitions/api_v1.IdentityProviderConfig'
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/api_v1.IdentityProviderType'
    type: object
  api_v1.UpdateResourceRequest:
    properties:
      filename:
        type: string
    type: object
  api_v1.UpdateStorageRequest:
    properties:
      config:
        $ref: '#/definitions/api_v1.StorageConfig'
      name:
        type: string
      type:
        $ref: '#/definitions/api_v1.StorageType'
    type: object
  api_v1.UpdateUserRequest:
    properties:
      avatarUrl:
        type: string
      email:
        type: string
      nickname:
        type: string
      password:
        type: string
      rowStatus:
        $ref: '#/definitions/api_v1.RowStatus'
      username:
        type: string
    type: object
  api_v1.UpsertMemoOrganizerRequest:
    properties:
      pinned:
        type: boolean
    type: object
  api_v1.UpsertMemoRelationRequest:
    properties:
      relatedMemoId:
        type: integer
      type:
        $ref: '#/definitions/api_v1.MemoRelationType'
    type: object
  api_v1.UpsertSystemSettingRequest:
    properties:
      description:
        type: string
      name:
        $ref: '#/definitions/api_v1.SystemSettingName'
      value:
        type: string
    type: object
  api_v1.UpsertTagRequest:
    properties:
      name:
        type: string
    type: object
  api_v1.User:
    properties:
      avatarUrl:
        type: string
      createdTs:
        type: integer
      email:
        type: string
      id:
        type: integer
      nickname:
        type: string
      role:
        $ref: '#/definitions/api_v1.Role'
      rowStatus:
        allOf:
        - $ref: '#/definitions/api_v1.RowStatus'
        description: Standard fields
      updatedTs:
        type: integer
      username:
        description: Domain specific fields
        type: string
    type: object
  api_v1.Visibility:
    enum:
    - PUBLIC
    - PROTECTED
    - PRIVATE
    type: string
    x-enum-varnames:
    - Public
    - Protected
    - Private
  github_com_usememos_memos_api_v1.CreateIdentityProviderRequest:
    properties:
      config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig'
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType'
    type: object
  github_com_usememos_memos_api_v1.CreateMemoRequest:
    properties:
      content:
        type: string
      createdTs:
        type: integer
      relationList:
        items:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
        type: array
      resourceIdList:
        description: Related fields
        items:
          type: integer
        type: array
      visibility:
        allOf:
        - $ref: '#/definitions/github_com_usememos_memos_api_v1.Visibility'
        description: Domain specific fields
    type: object
  github_com_usememos_memos_api_v1.CreateResourceRequest:
    properties:
      externalLink:
        type: string
      filename:
        type: string
      type:
        type: string
    type: object
  github_com_usememos_memos_api_v1.CreateStorageRequest:
    properties:
      config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageConfig'
      name:
        type: string
      type:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageType'
    type: object
  github_com_usememos_memos_api_v1.CreateUserRequest:
    properties:
      email:
        type: string
      nickname:
        type: string
      password:
        type: string
      role:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.Role'
      username:
        type: string
    type: object
  github_com_usememos_memos_api_v1.CustomizedProfile:
    properties:
      appearance:
        description: Appearance is the server default appearance.
        type: string
      description:
        description: Description is the server description.
        type: string
      locale:
        description: Locale is the server default locale.
        type: string
      logoUrl:
        description: LogoURL is the url of logo image.
        type: string
      name:
        description: Name is the server name, default is `memos`
        type: string
    type: object
  github_com_usememos_memos_api_v1.DeleteTagRequest:
    properties:
      name:
        type: string
    type: object
  github_com_usememos_memos_api_v1.FieldMapping:
    properties:
      displayName:
        type: string
      email:
        type: string
      identifier:
        type: string
    type: object
  github_com_usememos_memos_api_v1.IdentityProvider:
    properties:
      config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig'
      id:
        type: integer
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType'
    type: object
  github_com_usememos_memos_api_v1.IdentityProviderConfig:
    properties:
      oauth2Config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config'
    type: object
  github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config:
    properties:
      authUrl:
        type: string
      clientId:
        type: string
      clientSecret:
        type: string
      fieldMapping:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.FieldMapping'
      scopes:
        items:
          type: string
        type: array
      tokenUrl:
        type: string
      userInfoUrl:
        type: string
    type: object
  github_com_usememos_memos_api_v1.IdentityProviderType:
    enum:
    - OAUTH2
    type: string
    x-enum-varnames:
    - IdentityProviderOAuth2Type
  github_com_usememos_memos_api_v1.MemoRelationType:
    enum:
    - REFERENCE
    - COMMENT
    type: string
    x-enum-varnames:
    - MemoRelationReference
    - MemoRelationComment
  github_com_usememos_memos_api_v1.PatchMemoRequest:
    properties:
      content:
        description: Domain specific fields
        type: string
      createdTs:
        description: Standard fields
        type: integer
      relationList:
        items:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
        type: array
      resourceIdList:
        description: Related fields
        items:
          type: integer
        type: array
      rowStatus:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus'
      updatedTs:
        type: integer
      visibility:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.Visibility'
    type: object
  github_com_usememos_memos_api_v1.Role:
    enum:
    - HOST
    - ADMIN
    - USER
    type: string
    x-enum-varnames:
    - RoleHost
    - RoleAdmin
    - RoleUser
  github_com_usememos_memos_api_v1.RowStatus:
    enum:
    - NORMAL
    - ARCHIVED
    type: string
    x-enum-varnames:
    - Normal
    - Archived
  github_com_usememos_memos_api_v1.SSOSignIn:
    properties:
      code:
        type: string
      identityProviderId:
        type: integer
      redirectUri:
        type: string
    type: object
  github_com_usememos_memos_api_v1.SignIn:
    properties:
      password:
        type: string
      remember:
        type: boolean
      username:
        type: string
    type: object
  github_com_usememos_memos_api_v1.SignUp:
    properties:
      password:
        type: string
      username:
        type: string
    type: object
  github_com_usememos_memos_api_v1.StorageConfig:
    properties:
      s3Config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageS3Config'
    type: object
  github_com_usememos_memos_api_v1.StorageS3Config:
    properties:
      accessKey:
        type: string
      bucket:
        type: string
      endPoint:
        type: string
      path:
        type: string
      presign:
        type: boolean
      region:
        type: string
      secretKey:
        type: string
      urlPrefix:
        type: string
      urlSuffix:
        type: string
    type: object
  github_com_usememos_memos_api_v1.StorageType:
    enum:
    - S3
    type: string
    x-enum-varnames:
    - StorageS3
  github_com_usememos_memos_api_v1.SystemSetting:
    properties:
      description:
        type: string
      name:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.SystemSettingName'
      value:
        description: Value is a JSON string with basic value.
        type: string
    type: object
  github_com_usememos_memos_api_v1.SystemSettingName:
    enum:
    - server-id
    - secret-session
    - disable-public-memos
    - max-upload-size-mib
    - customized-profile
    - storage-service-id
    - local-storage-path
    - telegram-bot-token
    - memo-display-with-updated-ts
    type: string
    x-enum-varnames:
    - SystemSettingServerIDName
    - SystemSettingSecretSessionName
    - SystemSettingDisablePublicMemosName
    - SystemSettingMaxUploadSizeMiBName
    - SystemSettingCustomizedProfileName
    - SystemSettingStorageServiceIDName
    - SystemSettingLocalStoragePathName
    - SystemSettingTelegramBotTokenName
    - SystemSettingMemoDisplayWithUpdatedTsName
  github_com_usememos_memos_api_v1.SystemStatus:
    properties:
      additionalScript:
        description: Additional script.
        type: string
      additionalStyle:
        description: Additional style.
        type: string
      allowSignUp:
        description: |-
          System settings
          Allow sign up.
        type: boolean
      customizedProfile:
        allOf:
        - $ref: '#/definitions/github_com_usememos_memos_api_v1.CustomizedProfile'
        description: Customized server profile, including server name and external
          url.
      dbSize:
        type: integer
      disablePasswordLogin:
        description: Disable password login.
        type: boolean
      disablePublicMemos:
        description: Disable public memos.
        type: boolean
      host:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.User'
      localStoragePath:
        description: Local storage path.
        type: string
      maxUploadSizeMiB:
        description: Max upload size.
        type: integer
      memoDisplayWithUpdatedTs:
        description: Memo display with updated timestamp.
        type: boolean
      profile:
        $ref: '#/definitions/profile.Profile'
      storageServiceId:
        description: Storage service ID.
        type: integer
    type: object
  github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest:
    properties:
      config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderConfig'
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.IdentityProviderType'
    type: object
  github_com_usememos_memos_api_v1.UpdateResourceRequest:
    properties:
      filename:
        type: string
    type: object
  github_com_usememos_memos_api_v1.UpdateStorageRequest:
    properties:
      config:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageConfig'
      name:
        type: string
      type:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.StorageType'
    type: object
  github_com_usememos_memos_api_v1.UpdateUserRequest:
    properties:
      avatarUrl:
        type: string
      email:
        type: string
      nickname:
        type: string
      password:
        type: string
      rowStatus:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus'
      username:
        type: string
    type: object
  github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest:
    properties:
      pinned:
        type: boolean
    type: object
  github_com_usememos_memos_api_v1.UpsertMemoRelationRequest:
    properties:
      relatedMemoId:
        type: integer
      type:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.MemoRelationType'
    type: object
  github_com_usememos_memos_api_v1.UpsertSystemSettingRequest:
    properties:
      description:
        type: string
      name:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.SystemSettingName'
      value:
        type: string
    type: object
  github_com_usememos_memos_api_v1.UpsertTagRequest:
    properties:
      name:
        type: string
    type: object
  github_com_usememos_memos_api_v1.User:
    properties:
      avatarUrl:
        type: string
      createdTs:
        type: integer
      email:
        type: string
      id:
        type: integer
      nickname:
        type: string
      role:
        $ref: '#/definitions/github_com_usememos_memos_api_v1.Role'
      rowStatus:
        allOf:
        - $ref: '#/definitions/github_com_usememos_memos_api_v1.RowStatus'
        description: Standard fields
      updatedTs:
        type: integer
      username:
        description: Domain specific fields
        type: string
    type: object
  github_com_usememos_memos_api_v1.Visibility:
    enum:
    - PUBLIC
    - PROTECTED
    - PRIVATE
    type: string
    x-enum-varnames:
    - Public
    - Protected
    - Private
  profile.Profile:
    properties:
      mode:
        description: Mode can be "prod" or "dev" or "demo"
        type: string
      version:
        description: Version is the current version of server
        type: string
    type: object
  store.FieldMapping:
    properties:
      displayName:
        type: string
      email:
        type: string
      identifier:
        type: string
    type: object
  store.IdentityProvider:
    properties:
      config:
        $ref: '#/definitions/store.IdentityProviderConfig'
      id:
        type: integer
      identifierFilter:
        type: string
      name:
        type: string
      type:
        $ref: '#/definitions/store.IdentityProviderType'
    type: object
  store.IdentityProviderConfig:
    properties:
      oauth2Config:
        $ref: '#/definitions/store.IdentityProviderOAuth2Config'
    type: object
  store.IdentityProviderOAuth2Config:
    properties:
      authUrl:
        type: string
      clientId:
        type: string
      clientSecret:
        type: string
      fieldMapping:
        $ref: '#/definitions/store.FieldMapping'
      scopes:
        items:
          type: string
        type: array
      tokenUrl:
        type: string
      userInfoUrl:
        type: string
    type: object
  store.IdentityProviderType:
    enum:
    - OAUTH2
    type: string
    x-enum-varnames:
    - IdentityProviderOAuth2Type
  store.Memo:
    properties:
      content:
        description: Domain specific fields
        type: string
      createdTs:
        type: integer
      creatorID:
        type: integer
      id:
        type: integer
      parentID:
        type: integer
      pinned:
        description: Composed fields
        type: boolean
      resourceName:
        type: string
      rowStatus:
        allOf:
        - $ref: '#/definitions/store.RowStatus'
        description: Standard fields
      updatedTs:
        type: integer
      visibility:
        $ref: '#/definitions/store.Visibility'
    type: object
  store.MemoRelation:
    properties:
      memoID:
        type: integer
      relatedMemoID:
        type: integer
      type:
        $ref: '#/definitions/store.MemoRelationType'
    type: object
  store.MemoRelationType:
    enum:
    - REFERENCE
    - COMMENT
    type: string
    x-enum-varnames:
    - MemoRelationReference
    - MemoRelationComment
  store.Resource:
    properties:
      blob:
        items:
          type: integer
        type: array
      createdTs:
        type: integer
      creatorID:
        description: Standard fields
        type: integer
      externalLink:
        type: string
      filename:
        description: Domain specific fields
        type: string
      id:
        type: integer
      internalPath:
        type: string
      memoID:
        type: integer
      resourceName:
        type: string
      size:
        type: integer
      type:
        type: string
      updatedTs:
        type: integer
    type: object
  store.Role:
    enum:
    - HOST
    - ADMIN
    - USER
    type: string
    x-enum-varnames:
    - RoleHost
    - RoleAdmin
    - RoleUser
  store.RowStatus:
    enum:
    - NORMAL
    - ARCHIVED
    type: string
    x-enum-varnames:
    - Normal
    - Archived
  store.Storage:
    properties:
      config:
        type: string
      id:
        type: integer
      name:
        type: string
      type:
        type: string
    type: object
  store.User:
    properties:
      avatarURL:
        type: string
      createdTs:
        type: integer
      email:
        type: string
      id:
        type: integer
      nickname:
        type: string
      passwordHash:
        type: string
      role:
        $ref: '#/definitions/store.Role'
      rowStatus:
        allOf:
        - $ref: '#/definitions/store.RowStatus'
        description: Standard fields
      updatedTs:
        type: integer
      username:
        description: Domain specific fields
        type: string
    type: object
  store.Visibility:
    enum:
    - PUBLIC
    - PROTECTED
    - PRIVATE
    type: string
    x-enum-varnames:
    - Public
    - Protected
    - Private
externalDocs:
  description: Find out more about Memos.
  url: https://usememos.com/
info:
  contact:
    name: API Support
    url: https://github.com/orgs/usememos/discussions
  description: A privacy-first, lightweight note-taking service.
  license:
    name: MIT License
    url: https://github.com/usememos/memos/blob/main/LICENSE
  title: memos API
  version: "1.0"
paths:
  /api/v1/auth/signin:
    post:
      consumes:
      - application/json
      parameters:
      - description: Sign-in object
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.SignIn'
      produces:
      - application/json
      responses:
        "200":
          description: User information
          schema:
            $ref: '#/definitions/store.User'
        "400":
          description: Malformatted signin request
        "401":
          description: Password login is deactivated | Incorrect login credentials,
            please try again
        "403":
          description: User has been archived with username %s
        "500":
          description: Failed to find system setting | Failed to unmarshal system
            setting | Incorrect login credentials, please try again | Failed to generate
            tokens | Failed to create activity
      summary: Sign-in to memos.
      tags:
      - auth
  /api/v1/auth/signin/sso:
    post:
      consumes:
      - application/json
      parameters:
      - description: SSO sign-in object
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.SSOSignIn'
      produces:
      - application/json
      responses:
        "200":
          description: User information
          schema:
            $ref: '#/definitions/store.User'
        "400":
          description: Malformatted signin request
        "401":
          description: Access denied, identifier does not match the filter.
        "403":
          description: User has been archived with username {username}
        "404":
          description: Identity provider not found
        "500":
          description: Failed to find identity provider | Failed to create identity
            provider instance | Failed to exchange token | Failed to get user info
            | Failed to compile identifier filter | Incorrect login credentials, please
            try again | Failed to generate random password | Failed to generate password
            hash | Failed to create user | Failed to generate tokens | Failed to create
            activity
      summary: Sign-in to memos using SSO.
      tags:
      - auth
  /api/v1/auth/signout:
    post:
      produces:
      - application/json
      responses:
        "200":
          description: Sign-out success
          schema:
            type: boolean
      summary: Sign-out from memos.
      tags:
      - auth
  /api/v1/auth/signup:
    post:
      consumes:
      - application/json
      parameters:
      - description: Sign-up object
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.SignUp'
      produces:
      - application/json
      responses:
        "200":
          description: User information
          schema:
            $ref: '#/definitions/store.User'
        "400":
          description: Malformatted signup request | Failed to find users
        "401":
          description: signup is disabled
        "403":
          description: Forbidden
        "404":
          description: Not found
        "500":
          description: Failed to find system setting | Failed to unmarshal system
            setting allow signup | Failed to generate password hash | Failed to create
            user | Failed to generate tokens | Failed to create activity
      summary: Sign-up to memos.
      tags:
      - auth
  /api/v1/idp:
    get:
      description: '*clientSecret is only available for host user'
      produces:
      - application/json
      responses:
        "200":
          description: List of available identity providers
          schema:
            items:
              $ref: '#/definitions/api_v1.IdentityProvider'
            type: array
        "500":
          description: Failed to find identity provider list | Failed to find user
      summary: Get a list of identity providers
      tags:
      - idp
    post:
      consumes:
      - application/json
      parameters:
      - description: Identity provider information
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/api_v1.CreateIdentityProviderRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Identity provider information
          schema:
            $ref: '#/definitions/store.IdentityProvider'
        "400":
          description: Malformatted post identity provider request
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to create identity provider
      summary: Create Identity Provider
      tags:
      - idp
  /api/v1/idp/{idpId}:
    delete:
      consumes:
      - application/json
      parameters:
      - description: Identity Provider ID
        in: path
        name: idpId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Identity Provider deleted
          schema:
            type: boolean
        "400":
          description: 'ID is not a number: %s | Malformatted patch identity provider
            request'
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to patch identity provider
      summary: Delete an identity provider by ID
      tags:
      - idp
    get:
      consumes:
      - application/json
      parameters:
      - description: Identity provider ID
        in: path
        name: idpId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Requested identity provider
          schema:
            $ref: '#/definitions/store.IdentityProvider'
        "400":
          description: 'ID is not a number: %s'
        "401":
          description: Missing user in session | Unauthorized
        "404":
          description: Identity provider not found
        "500":
          description: Failed to find identity provider list | Failed to find user
      summary: Get an identity provider by ID
      tags:
      - idp
    patch:
      consumes:
      - application/json
      parameters:
      - description: Identity Provider ID
        in: path
        name: idpId
        required: true
        type: integer
      - description: Patched identity provider information
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/api_v1.UpdateIdentityProviderRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Patched identity provider
          schema:
            $ref: '#/definitions/store.IdentityProvider'
        "400":
          description: 'ID is not a number: %s | Malformatted patch identity provider
            request'
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to patch identity provider
      summary: Update an identity provider by ID
      tags:
      - idp
  /api/v1/memo:
    get:
      parameters:
      - description: Creator ID
        in: query
        name: creatorId
        type: integer
      - description: Creator username
        in: query
        name: creatorUsername
        type: string
      - description: Row status
        enum:
        - NORMAL
        - ARCHIVED
        in: query
        name: rowStatus
        type: string
      - description: Pinned
        in: query
        name: pinned
        type: boolean
      - description: 'Search for tag. Do not append #'
        in: query
        name: tag
        type: string
      - description: Search for content
        in: query
        name: content
        type: string
      - description: Limit
        in: query
        name: limit
        type: integer
      - description: Offset
        in: query
        name: offset
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Memo list
          schema:
            items:
              $ref: '#/definitions/store.Memo'
            type: array
        "400":
          description: Missing user to find memo
        "500":
          description: Failed to get memo display with updated ts setting value |
            Failed to fetch memo list | Failed to compose memo response
      summary: Get a list of memos matching optional filters
      tags:
      - memo
    post:
      consumes:
      - application/json
      description: |-
        Visibility can be PUBLIC, PROTECTED or PRIVATE
        *You should omit fields to use their default values
      parameters:
      - description: Request object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.CreateMemoRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Stored memo
          schema:
            $ref: '#/definitions/store.Memo'
        "400":
          description: Malformatted post memo request | Content size overflow, up
            to 1MB
        "401":
          description: Missing user in session
        "404":
          description: 'User not found | Memo not found: %d'
        "500":
          description: Failed to find user setting | Failed to unmarshal user setting
            value | Failed to find system setting | Failed to unmarshal system setting
            | Failed to find user | Failed to create memo | Failed to create activity
            | Failed to upsert memo resource | Failed to upsert memo relation | Failed
            to compose memo | Failed to compose memo response
      summary: Create a memo
      tags:
      - memo
  /api/v1/memo/{memoId}:
    delete:
      parameters:
      - description: Memo ID to delete
        in: path
        name: memoId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Memo deleted
          schema:
            type: boolean
        "400":
          description: 'ID is not a number: %s'
        "401":
          description: Missing user in session | Unauthorized
        "404":
          description: 'Memo not found: %d'
        "500":
          description: 'Failed to find memo | Failed to delete memo ID: %v'
      summary: Delete memo by ID
      tags:
      - memo
    get:
      parameters:
      - description: Memo ID
        in: path
        name: memoId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Memo list
          schema:
            items:
              $ref: '#/definitions/store.Memo'
            type: array
        "400":
          description: 'ID is not a number: %s'
        "401":
          description: Missing user in session
        "403":
          description: this memo is private only | this memo is protected, missing
            user in session
        "404":
          description: 'Memo not found: %d'
        "500":
          description: 'Failed to find memo by ID: %v | Failed to compose memo response'
      summary: Get memo by ID
      tags:
      - memo
    patch:
      consumes:
      - application/json
      description: |-
        Visibility can be PUBLIC, PROTECTED or PRIVATE
        *You should omit fields to use their default values
      parameters:
      - description: ID of memo to update
        in: path
        name: memoId
        required: true
        type: integer
      - description: Patched object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.PatchMemoRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Stored memo
          schema:
            $ref: '#/definitions/store.Memo'
        "400":
          description: 'ID is not a number: %s | Malformatted patch memo request |
            Content size overflow, up to 1MB'
        "401":
          description: Missing user in session | Unauthorized
        "404":
          description: 'Memo not found: %d'
        "500":
          description: Failed to find memo | Failed to patch memo | Failed to upsert
            memo resource | Failed to delete memo resource | Failed to compose memo
            response
      summary: Update a memo
      tags:
      - memo
  /api/v1/memo/{memoId}/organizer:
    post:
      consumes:
      - application/json
      parameters:
      - description: ID of memo to organize
        in: path
        name: memoId
        required: true
        type: integer
      - description: Memo organizer object
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Memo information
          schema:
            $ref: '#/definitions/store.Memo'
        "400":
          description: 'ID is not a number: %s | Malformatted post memo organizer
            request'
        "401":
          description: Missing user in session | Unauthorized
        "404":
          description: 'Memo not found: %v'
        "500":
          description: 'Failed to find memo | Failed to upsert memo organizer | Failed
            to find memo by ID: %v | Failed to compose memo response'
      summary: Organize memo (pin/unpin)
      tags:
      - memo-organizer
  /api/v1/memo/{memoId}/relation:
    get:
      consumes:
      - application/json
      parameters:
      - description: ID of memo to find relations
        in: path
        name: memoId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Memo relation information list
          schema:
            items:
              $ref: '#/definitions/store.MemoRelation'
            type: array
        "400":
          description: 'ID is not a number: %s'
        "500":
          description: Failed to list memo relations
      summary: Get a list of Memo Relations
      tags:
      - memo-relation
    post:
      consumes:
      - application/json
      description: Create a relation between two memos
      parameters:
      - description: ID of memo to relate
        in: path
        name: memoId
        required: true
        type: integer
      - description: Memo relation object
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertMemoRelationRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Memo relation information
          schema:
            $ref: '#/definitions/store.MemoRelation'
        "400":
          description: 'ID is not a number: %s | Malformatted post memo relation request'
        "500":
          description: Failed to upsert memo relation
      summary: Create Memo Relation
      tags:
      - memo-relation
  /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType}:
    delete:
      consumes:
      - application/json
      description: Removes a relation between two memos
      parameters:
      - description: ID of memo to find relations
        in: path
        name: memoId
        required: true
        type: integer
      - description: ID of memo to remove relation to
        in: path
        name: relatedMemoId
        required: true
        type: integer
      - description: Type of relation to remove
        enum:
        - REFERENCE
        - COMMENT
        in: path
        name: relationType
        required: true
        type: string
      produces:
      - application/json
      responses:
        "200":
          description: Memo relation deleted
          schema:
            type: boolean
        "400":
          description: 'Memo ID is not a number: %s | Related memo ID is not a number:
            %s'
        "500":
          description: Failed to delete memo relation
      summary: Delete a Memo Relation
      tags:
      - memo-relation
  /api/v1/memo/all:
    get:
      description: |-
        This should also list protected memos if the user is logged in
        Authentication is optional
      parameters:
      - description: Limit
        in: query
        name: limit
        type: integer
      - description: Offset
        in: query
        name: offset
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Memo list
          schema:
            items:
              $ref: '#/definitions/store.Memo'
            type: array
        "500":
          description: Failed to get memo display with updated ts setting value |
            Failed to fetch all memo list | Failed to compose memo response
      summary: Get a list of public memos matching optional filters
      tags:
      - memo
  /api/v1/memo/stats:
    get:
      description: Used to generate the heatmap
      parameters:
      - description: Creator ID
        in: query
        name: creatorId
        type: integer
      - description: Creator username
        in: query
        name: creatorUsername
        type: string
      produces:
      - application/json
      responses:
        "200":
          description: Memo createdTs list
          schema:
            items:
              type: integer
            type: array
        "400":
          description: Missing user id to find memo
        "500":
          description: Failed to get memo display with updated ts setting value |
            Failed to find memo list | Failed to compose memo response
      summary: Get memo stats by creator ID or username
      tags:
      - memo
  /api/v1/ping:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: If succeed to ping the system
          schema:
            type: boolean
      summary: Ping the system
      tags:
      - system
  /api/v1/resource:
    get:
      parameters:
      - description: Limit
        in: query
        name: limit
        type: integer
      - description: Offset
        in: query
        name: offset
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Resource list
          schema:
            items:
              $ref: '#/definitions/store.Resource'
            type: array
        "401":
          description: Missing user in session
        "500":
          description: Failed to fetch resource list
      summary: Get a list of resources
      tags:
      - resource
    post:
      consumes:
      - application/json
      parameters:
      - description: Request object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/api_v1.CreateResourceRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Created resource
          schema:
            $ref: '#/definitions/store.Resource'
        "400":
          description: Malformatted post resource request | Invalid external link
            | Invalid external link scheme | Failed to request %s | Failed to read
            %s | Failed to read mime from %s
        "401":
          description: Missing user in session
        "500":
          description: Failed to save resource | Failed to create resource | Failed
            to create activity
      summary: Create resource
      tags:
      - resource
  /api/v1/resource/{resourceId}:
    delete:
      parameters:
      - description: Resource ID
        in: path
        name: resourceId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Resource deleted
          schema:
            type: boolean
        "400":
          description: 'ID is not a number: %s'
        "401":
          description: Missing user in session
        "404":
          description: 'Resource not found: %d'
        "500":
          description: Failed to find resource | Failed to delete resource
      summary: Delete a resource
      tags:
      - resource
    patch:
      parameters:
      - description: Resource ID
        in: path
        name: resourceId
        required: true
        type: integer
      - description: Patch resource request
        in: body
        name: patch
        required: true
        schema:
          $ref: '#/definitions/api_v1.UpdateResourceRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Updated resource
          schema:
            $ref: '#/definitions/store.Resource'
        "400":
          description: 'ID is not a number: %s | Malformatted patch resource request'
        "401":
          description: Missing user in session | Unauthorized
        "404":
          description: 'Resource not found: %d'
        "500":
          description: Failed to find resource | Failed to patch resource
      summary: Update a resource
      tags:
      - resource
  /api/v1/resource/blob:
    post:
      consumes:
      - multipart/form-data
      parameters:
      - description: File to upload
        in: formData
        name: file
        required: true
        type: file
      produces:
      - application/json
      responses:
        "200":
          description: Created resource
          schema:
            $ref: '#/definitions/store.Resource'
        "400":
          description: Upload file not found | File size exceeds allowed limit of
            %d MiB | Failed to parse upload data
        "401":
          description: Missing user in session
        "500":
          description: Failed to get uploading file | Failed to open file | Failed
            to save resource | Failed to create resource | Failed to create activity
      summary: Upload resource
      tags:
      - resource
  /api/v1/status:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: System GetSystemStatus
          schema:
            $ref: '#/definitions/api_v1.SystemStatus'
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find host user | Failed to find system setting list
            | Failed to unmarshal system setting customized profile value
      summary: Get system GetSystemStatus
      tags:
      - system
  /api/v1/storage:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: List of storages
          schema:
            items:
              $ref: '#/definitions/store.Storage'
            type: array
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to convert storage
      summary: Get a list of storages
      tags:
      - storage
    post:
      consumes:
      - application/json
      parameters:
      - description: Request object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.CreateStorageRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Created storage
          schema:
            $ref: '#/definitions/store.Storage'
        "400":
          description: Malformatted post storage request
        "401":
          description: Missing user in session
        "500":
          description: Failed to find user | Failed to create storage | Failed to
            convert storage
      summary: Create storage
      tags:
      - storage
  /api/v1/storage/{storageId}:
    delete:
      parameters:
      - description: Storage ID
        in: path
        name: storageId
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Storage deleted
          schema:
            type: boolean
        "400":
          description: 'ID is not a number: %s | Storage service %d is using'
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to find storage | Failed to unmarshal
            storage service id | Failed to delete storage
      summary: Delete a storage
      tags:
      - storage
    patch:
      parameters:
      - description: Storage ID
        in: path
        name: storageId
        required: true
        type: integer
      - description: Patch request
        in: body
        name: patch
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpdateStorageRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Updated resource
          schema:
            $ref: '#/definitions/store.Storage'
        "400":
          description: 'ID is not a number: %s | Malformatted patch storage request
            | Malformatted post storage request'
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to patch storage | Failed to convert
            storage
      summary: Update a storage
      tags:
      - storage
  /api/v1/system/setting:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: System setting list
          schema:
            items:
              $ref: '#/definitions/api_v1.SystemSetting'
            type: array
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to find system setting list
      summary: Get a list of system settings
      tags:
      - system-setting
    post:
      consumes:
      - application/json
      parameters:
      - description: Request object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/api_v1.UpsertSystemSettingRequest'
      produces:
      - application/json
      responses:
        "400":
          description: Malformatted post system setting request | invalid system setting
        "401":
          description: Missing user in session | Unauthorized
        "403":
          description: Cannot disable passwords if no SSO identity provider is configured.
        "500":
          description: Failed to find user | Failed to upsert system setting
      summary: Create system setting
      tags:
      - system-setting
  /api/v1/system/vacuum:
    post:
      produces:
      - application/json
      responses:
        "200":
          description: Database vacuumed
          schema:
            type: boolean
        "401":
          description: Missing user in session | Unauthorized
        "500":
          description: Failed to find user | Failed to ExecVacuum database
      summary: Vacuum the database
      tags:
      - system
  /api/v1/tag:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: Tag list
          schema:
            items:
              type: string
            type: array
        "400":
          description: Missing user id to find tag
        "500":
          description: Failed to find tag list
      summary: Get a list of tags
      tags:
      - tag
    post:
      consumes:
      - application/json
      parameters:
      - description: Request object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.UpsertTagRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Created tag name
          schema:
            type: string
        "400":
          description: Malformatted post tag request | Tag name shouldn't be empty
        "401":
          description: Missing user in session
        "500":
          description: Failed to upsert tag | Failed to create activity
      summary: Create a tag
      tags:
      - tag
  /api/v1/tag/delete:
    post:
      consumes:
      - application/json
      parameters:
      - description: Request object.
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/github_com_usememos_memos_api_v1.DeleteTagRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Tag deleted
          schema:
            type: boolean
        "400":
          description: Malformatted post tag request | Tag name shouldn't be empty
        "401":
          description: Missing user in session
        "500":
          description: 'Failed to delete tag name: %v'
      summary: Delete a tag
      tags:
      - tag
  /api/v1/tag/suggestion:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: Tag list
          schema:
            items:
              type: string
            type: array
        "400":
          description: Missing user session
        "500":
          description: Failed to find memo list | Failed to find tag list
      summary: Get a list of tags suggested from other memos contents
      tags:
      - tag
  /api/v1/user:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: User list
          schema:
            items:
              $ref: '#/definitions/store.User'
            type: array
        "500":
          description: Failed to fetch user list
      summary: Get a list of users
      tags:
      - user
    post:
      consumes:
      - application/json
      parameters:
      - description: Request object
        in: body
        name: body
        required: true
        schema:
          $ref: '#/definitions/api_v1.CreateUserRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Created user
          schema:
            $ref: '#/definitions/store.User'
        "400":
          description: Malformatted post user request | Invalid user create format
        "401":
          description: Missing auth session | Unauthorized to create user
        "403":
          description: Could not create host user
        "500":
          description: Failed to find user by id | Failed to generate password hash
            | Failed to create user | Failed to create activity
      summary: Create a user
      tags:
      - user
  /api/v1/user/{id}:
    delete:
      parameters:
      - description: User ID
        in: path
        name: id
        required: true
        type: string
      produces:
      - application/json
      responses:
        "200":
          description: User deleted
          schema:
            type: boolean
        "400":
          description: 'ID is not a number: %s | Current session user not found with
            ID: %d'
        "401":
          description: Missing user in session
        "403":
          description: Unauthorized to delete user
        "500":
          description: Failed to find user | Failed to delete user
      summary: Delete a user
      tags:
      - user
    get:
      parameters:
      - description: User ID
        in: path
        name: id
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: Requested user
          schema:
            $ref: '#/definitions/store.User'
        "400":
          description: Malformatted user id
        "404":
          description: User not found
        "500":
          description: Failed to find user
      summary: Get user by id
      tags:
      - user
    patch:
      parameters:
      - description: User ID
        in: path
        name: id
        required: true
        type: string
      - description: Patch request
        in: body
        name: patch
        required: true
        schema:
          $ref: '#/definitions/api_v1.UpdateUserRequest'
      produces:
      - application/json
      responses:
        "200":
          description: Updated user
          schema:
            $ref: '#/definitions/store.User'
        "400":
          description: 'ID is not a number: %s | Current session user not found with
            ID: %d | Malformatted patch user request | Invalid update user request'
        "401":
          description: Missing user in session
        "403":
          description: Unauthorized to update user
        "500":
          description: Failed to find user | Failed to generate password hash | Failed
            to patch user | Failed to find userSettingList
      summary: Update a user
      tags:
      - user
  /api/v1/user/me:
    get:
      produces:
      - application/json
      responses:
        "200":
          description: Current user
          schema:
            $ref: '#/definitions/store.User'
        "401":
          description: Missing auth session
        "500":
          description: Failed to find user | Failed to find userSettingList
      summary: Get current user
      tags:
      - user
  /api/v1/user/name/{username}:
    get:
      parameters:
      - description: Username
        in: path
        name: username
        required: true
        type: string
      produces:
      - application/json
      responses:
        "200":
          description: Requested user
          schema:
            $ref: '#/definitions/store.User'
        "404":
          description: User not found
        "500":
          description: Failed to find user
      summary: Get user by username
      tags:
      - user
  /o/get/GetImage:
    get:
      parameters:
      - description: Image url
        in: query
        name: url
        required: true
        type: string
      produces:
      - GetImage/*
      responses:
        "200":
          description: Image
        "400":
          description: 'Missing GetImage url | Wrong url | Failed to get GetImage
            url: %s'
        "500":
          description: Failed to write GetImage blob
      summary: Get GetImage from URL
      tags:
      - image-url
swagger: "2.0"
package v1

import (
	"encoding/json"
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/usememos/memos/server/profile"
	"github.com/usememos/memos/store"
)

type SystemStatus struct {
	Host    *User           `json:"host"`
	Profile profile.Profile `json:"profile"`
	DBSize  int64           `json:"dbSize"`

	// System settings
	// Disable password login.
	DisablePasswordLogin bool `json:"disablePasswordLogin"`
	// Disable public memos.
	DisablePublicMemos bool `json:"disablePublicMemos"`
	// Max upload size.
	MaxUploadSizeMiB int `json:"maxUploadSizeMiB"`
	// Customized server profile, including server name and external url.
	CustomizedProfile CustomizedProfile `json:"customizedProfile"`
	// Storage service ID.
	StorageServiceID int32 `json:"storageServiceId"`
	// Local storage path.
	LocalStoragePath string `json:"localStoragePath"`
	// Memo display with updated timestamp.
	MemoDisplayWithUpdatedTs bool `json:"memoDisplayWithUpdatedTs"`
}

func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
	g.GET("/ping", s.PingSystem)
	g.GET("/status", s.GetSystemStatus)
	g.POST("/system/vacuum", s.ExecVacuum)
}

// PingSystem godoc
//
//	@Summary	Ping the system
//	@Tags		system
//	@Produce	json
//	@Success	200	{boolean}	true	"If succeed to ping the system"
//	@Router		/api/v1/ping [GET]
func (*APIV1Service) PingSystem(c echo.Context) error {
	return c.JSON(http.StatusOK, true)
}

// GetSystemStatus godoc
//
//	@Summary	Get system GetSystemStatus
//	@Tags		system
//	@Produce	json
//	@Success	200	{object}	SystemStatus	"System GetSystemStatus"
//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
//	@Failure	500	{object}	nil				"Failed to find host user | Failed to find system setting list | Failed to unmarshal system setting customized profile value"
//	@Router		/api/v1/status [GET]
func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
	ctx := c.Request().Context()

	systemStatus := SystemStatus{
		Profile: profile.Profile{
			Mode:    s.Profile.Mode,
			Version: s.Profile.Version,
		},
		MaxUploadSizeMiB: 32,
		CustomizedProfile: CustomizedProfile{
			Name:       "Memos",
			Locale:     "en",
			Appearance: "system",
		},
		StorageServiceID: DefaultStorage,
		LocalStoragePath: "assets/{timestamp}_{filename}",
	}

	hostUserType := store.RoleHost
	hostUser, err := s.Store.GetUser(ctx, &store.FindUser{
		Role: &hostUserType,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
	}
	if hostUser != nil {
		systemStatus.Host = &User{ID: hostUser.ID}
	}

	workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace general setting").SetInternal(err)
	}
	systemStatus.DisablePasswordLogin = workspaceGeneralSetting.DisallowPasswordLogin

	systemSettingList, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
	}
	for _, systemSetting := range systemSettingList {
		var baseValue any
		err := json.Unmarshal([]byte(systemSetting.Value), &baseValue)
		if err != nil {
			// Skip invalid value.
			continue
		}

		switch systemSetting.Name {
		case SystemSettingMaxUploadSizeMiBName.String():
			systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
		case SystemSettingCustomizedProfileName.String():
			customizedProfile := CustomizedProfile{}
			if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil {
				return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting customized profile value").SetInternal(err)
			}
			systemStatus.CustomizedProfile = customizedProfile
		case SystemSettingStorageServiceIDName.String():
			systemStatus.StorageServiceID = int32(baseValue.(float64))
		default:
			// Skip unknown system setting.
		}
	}

	return c.JSON(http.StatusOK, systemStatus)
}

// ExecVacuum godoc
//
//	@Summary	Vacuum the database
//	@Tags		system
//	@Produce	json
//	@Success	200	{boolean}	true	"Database vacuumed"
//	@Failure	401	{object}	nil		"Missing user in session | Unauthorized"
//	@Failure	500	{object}	nil		"Failed to find user | Failed to ExecVacuum database"
//	@Router		/api/v1/system/vacuum [POST]
func (s *APIV1Service) ExecVacuum(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	if err := s.Store.Vacuum(ctx); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}
package v1

import (
	"encoding/json"
	"net/http"
	"path/filepath"
	"strings"

	"github.com/labstack/echo/v4"
	"github.com/pkg/errors"

	"github.com/usememos/memos/store"
)

type SystemSettingName string

const (
	// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
	SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
	// 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"
)
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`

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

func (key SystemSettingName) String() string {
	return string(key)
}

type SystemSetting struct {
	Name SystemSettingName `json:"name"`
	// Value is a JSON string with basic value.
	Value       string `json:"value"`
	Description string `json:"description"`
}

type UpsertSystemSettingRequest struct {
	Name        SystemSettingName `json:"name"`
	Value       string            `json:"value"`
	Description string            `json:"description"`
}

func (s *APIV1Service) registerSystemSettingRoutes(g *echo.Group) {
	g.GET("/system/setting", s.GetSystemSettingList)
	g.POST("/system/setting", s.CreateSystemSetting)
}

// GetSystemSettingList godoc
//
//	@Summary	Get a list of system settings
//	@Tags		system-setting
//	@Produce	json
//	@Success	200	{object}	[]SystemSetting	"System setting list"
//	@Failure	401	{object}	nil				"Missing user in session | Unauthorized"
//	@Failure	500	{object}	nil				"Failed to find user | Failed to find system setting list"
//	@Router		/api/v1/system/setting [GET]
func (s *APIV1Service) GetSystemSettingList(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	list, err := s.Store.ListWorkspaceSettings(ctx, &store.FindWorkspaceSetting{})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
	}

	systemSettingList := make([]*SystemSetting, 0, len(list))
	for _, systemSetting := range list {
		systemSettingList = append(systemSettingList, convertSystemSettingFromStore(systemSetting))
	}
	return c.JSON(http.StatusOK, systemSettingList)
}

// CreateSystemSetting godoc
//
//	@Summary	Create system setting
//	@Tags		system-setting
//	@Accept		json
//	@Produce	json
//	@Param		body	body		UpsertSystemSettingRequest	true	"Request object."
//	@Failure	400		{object}	nil							"Malformatted post system setting request | invalid system setting"
//	@Failure	401		{object}	nil							"Missing user in session | Unauthorized"
//	@Failure	403		{object}	nil							"Cannot disable passwords if no SSO identity provider is configured."
//	@Failure	500		{object}	nil							"Failed to find user | Failed to upsert system setting"
//	@Router		/api/v1/system/setting [POST]
func (s *APIV1Service) CreateSystemSetting(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil || user.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
	}

	systemSettingUpsert := &UpsertSystemSettingRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
	}
	if err := systemSettingUpsert.Validate(); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
	}

	systemSetting, err := s.Store.UpsertWorkspaceSetting(ctx, &store.WorkspaceSetting{
		Name:        systemSettingUpsert.Name.String(),
		Value:       systemSettingUpsert.Value,
		Description: systemSettingUpsert.Description,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
	}
	return c.JSON(http.StatusOK, convertSystemSettingFromStore(systemSetting))
}

func (upsert UpsertSystemSettingRequest) Validate() error {
	switch settingName := upsert.Name; settingName {
	case SystemSettingMaxUploadSizeMiBName:
		var value int
		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
			return errors.Errorf(systemSettingUnmarshalError, settingName)
		}
	case SystemSettingCustomizedProfileName:
		customizedProfile := CustomizedProfile{
			Name:        "Memos",
			LogoURL:     "",
			Description: "",
			Locale:      "en",
			Appearance:  "system",
		}
		if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
			return errors.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 errors.Errorf(systemSettingUnmarshalError, settingName)
		}
		return nil
	case SystemSettingLocalStoragePathName:
		value := ""
		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
			return errors.Errorf(systemSettingUnmarshalError, settingName)
		}

		trimmedValue := strings.TrimSpace(value)
		switch {
		case trimmedValue != value:
			return errors.New("local storage path must not contain leading or trailing whitespace")
		case trimmedValue == "":
			return errors.New("local storage path can't be empty")
		case strings.Contains(trimmedValue, "\\"):
			return errors.New("local storage path must use forward slashes `/`")
		case strings.Contains(trimmedValue, "../"):
			return errors.New("local storage path is not allowed to contain `../`")
		case strings.HasPrefix(trimmedValue, "./"):
			return errors.New("local storage path is not allowed to start with `./`")
		case filepath.IsAbs(trimmedValue) || trimmedValue[0] == '/':
			return errors.New("local storage path must be a relative path")
		case !strings.Contains(trimmedValue, "{filename}"):
			return errors.New("local storage path must contain `{filename}`")
		}
	default:
		return errors.New("invalid system setting name")
	}
	return nil
}

func convertSystemSettingFromStore(systemSetting *store.WorkspaceSetting) *SystemSetting {
	return &SystemSetting{
		Name:        SystemSettingName(systemSetting.Name),
		Value:       systemSetting.Value,
		Description: systemSetting.Description,
	}
}
package v1

import (
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"slices"
	"sort"

	"github.com/labstack/echo/v4"

	"github.com/usememos/memos/store"
)

type Tag struct {
	Name      string
	CreatorID int32
}

type UpsertTagRequest struct {
	Name string `json:"name"`
}

type DeleteTagRequest struct {
	Name string `json:"name"`
}

func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
	g.GET("/tag", s.GetTagList)
	g.POST("/tag", s.CreateTag)
	g.GET("/tag/suggestion", s.GetTagSuggestion)
	g.POST("/tag/delete", s.DeleteTag)
}

// GetTagList godoc
//
//	@Summary	Get a list of tags
//	@Tags		tag
//	@Produce	json
//	@Success	200	{object}	[]string	"Tag list"
//	@Failure	400	{object}	nil			"Missing user id to find tag"
//	@Failure	500	{object}	nil			"Failed to find tag list"
//	@Router		/api/v1/tag [GET]
func (s *APIV1Service) GetTagList(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
	}

	list, err := s.Store.ListTags(ctx, &store.FindTag{
		CreatorID: userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
	}

	tagNameList := []string{}
	for _, tag := range list {
		tagNameList = append(tagNameList, tag.Name)
	}
	return c.JSON(http.StatusOK, tagNameList)
}

// CreateTag godoc
//
//	@Summary	Create a tag
//	@Tags		tag
//	@Accept		json
//	@Produce	json
//	@Param		body	body		UpsertTagRequest	true	"Request object."
//	@Success	200		{object}	string				"Created tag name"
//	@Failure	400		{object}	nil					"Malformatted post tag request | Tag name shouldn't be empty"
//	@Failure	401		{object}	nil					"Missing user in session"
//	@Failure	500		{object}	nil					"Failed to upsert tag | Failed to create activity"
//	@Router		/api/v1/tag [POST]
func (s *APIV1Service) CreateTag(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	tagUpsert := &UpsertTagRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
	}
	if tagUpsert.Name == "" {
		return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
	}

	tag, err := s.Store.UpsertTag(ctx, &store.Tag{
		Name:      tagUpsert.Name,
		CreatorID: userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
	}
	tagMessage := convertTagFromStore(tag)
	return c.JSON(http.StatusOK, tagMessage.Name)
}

// DeleteTag godoc
//
//	@Summary	Delete a tag
//	@Tags		tag
//	@Accept		json
//	@Produce	json
//	@Param		body	body		DeleteTagRequest	true	"Request object."
//	@Success	200		{boolean}	true				"Tag deleted"
//	@Failure	400		{object}	nil					"Malformatted post tag request | Tag name shouldn't be empty"
//	@Failure	401		{object}	nil					"Missing user in session"
//	@Failure	500		{object}	nil					"Failed to delete tag name: %v"
//	@Router		/api/v1/tag/delete [POST]
func (s *APIV1Service) DeleteTag(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}

	tagDelete := &DeleteTagRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
	}
	if tagDelete.Name == "" {
		return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
	}

	err := s.Store.DeleteTag(ctx, &store.DeleteTag{
		Name:      tagDelete.Name,
		CreatorID: userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

// GetTagSuggestion godoc
//
//	@Summary	Get a list of tags suggested from other memos contents
//	@Tags		tag
//	@Produce	json
//	@Success	200	{object}	[]string	"Tag list"
//	@Failure	400	{object}	nil			"Missing user session"
//	@Failure	500	{object}	nil			"Failed to find memo list | Failed to find tag list"
//	@Router		/api/v1/tag/suggestion [GET]
func (s *APIV1Service) GetTagSuggestion(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
	}
	normalRowStatus := store.Normal
	memoFind := &store.FindMemo{
		CreatorID:     &userID,
		ContentSearch: []string{"#"},
		RowStatus:     &normalRowStatus,
	}

	memoMessageList, err := s.Store.ListMemos(ctx, memoFind)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
	}

	list, err := s.Store.ListTags(ctx, &store.FindTag{
		CreatorID: userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
	}
	tagNameList := []string{}
	for _, tag := range list {
		tagNameList = append(tagNameList, tag.Name)
	}

	tagMapSet := make(map[string]bool)
	for _, memo := range memoMessageList {
		for _, tag := range findTagListFromMemoContent(memo.Content) {
			if !slices.Contains(tagNameList, tag) {
				tagMapSet[tag] = true
			}
		}
	}
	tagList := []string{}
	for tag := range tagMapSet {
		tagList = append(tagList, tag)
	}
	sort.Strings(tagList)
	return c.JSON(http.StatusOK, tagList)
}

func convertTagFromStore(tag *store.Tag) *Tag {
	return &Tag{
		Name:      tag.Name,
		CreatorID: tag.CreatorID,
	}
}

var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)

func findTagListFromMemoContent(memoContent string) []string {
	tagMapSet := make(map[string]bool)
	matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
	for _, v := range matches {
		tagName := v[1]
		tagMapSet[tagName] = true
	}

	tagList := []string{}
	for tag := range tagMapSet {
		tagList = append(tagList, tag)
	}
	sort.Strings(tagList)
	return tagList
}
package v1

import (
	"testing"
)

func TestFindTagListFromMemoContent(t *testing.T) {
	tests := []struct {
		memoContent string
		want        []string
	}{
		{
			memoContent: "#tag1 ",
			want:        []string{"tag1"},
		},
		{
			memoContent: "#tag1 #tag2 ",
			want:        []string{"tag1", "tag2"},
		},
		{
			memoContent: "#tag1 #tag2 \n#tag3 ",
			want:        []string{"tag1", "tag2", "tag3"},
		},
		{
			memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
			want:        []string{"tag1", "tag2", "tag3", "tag4"},
		},
		{
			memoContent: "#tag1 #tag2 \n#tag3  #tag4 ",
			want:        []string{"tag1", "tag2", "tag3", "tag4"},
		},
		{
			memoContent: "#tag1 123123#tag2 \n#tag3  #tag4 ",
			want:        []string{"tag1", "tag2", "tag3", "tag4"},
		},
		{
			memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3  #tag4 http://123123.com?123123#tag2) ",
			want:        []string{"tag1", "tag2", "tag2)", "tag3", "tag4"},
		},
	}
	for _, test := range tests {
		result := findTagListFromMemoContent(test.memoContent)
		if len(result) != len(test.want) {
			t.Errorf("Find tag list %s: got result %v, want %v.", test.memoContent, result, test.want)
		}
	}
}
package v1

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/pkg/errors"
	"golang.org/x/crypto/bcrypt"

	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/store"
)

// Role is the type of a role.
type Role string

const (
	// RoleHost is the HOST role.
	RoleHost Role = "HOST"
	// RoleAdmin is the ADMIN role.
	RoleAdmin Role = "ADMIN"
	// RoleUser is the USER role.
	RoleUser Role = "USER"
)

func (role Role) String() string {
	return string(role)
}

type User struct {
	ID int32 `json:"id"`

	// Standard fields
	RowStatus RowStatus `json:"rowStatus"`
	CreatedTs int64     `json:"createdTs"`
	UpdatedTs int64     `json:"updatedTs"`

	// Domain specific fields
	Username     string `json:"username"`
	Role         Role   `json:"role"`
	Email        string `json:"email"`
	Nickname     string `json:"nickname"`
	PasswordHash string `json:"-"`
	AvatarURL    string `json:"avatarUrl"`
}

type CreateUserRequest struct {
	Username string `json:"username"`
	Role     Role   `json:"role"`
	Email    string `json:"email"`
	Nickname string `json:"nickname"`
	Password string `json:"password"`
}

type UpdateUserRequest struct {
	RowStatus *RowStatus `json:"rowStatus"`
	Username  *string    `json:"username"`
	Email     *string    `json:"email"`
	Nickname  *string    `json:"nickname"`
	Password  *string    `json:"password"`
	AvatarURL *string    `json:"avatarUrl"`
}

func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
	g.GET("/user", s.GetUserList)
	g.POST("/user", s.CreateUser)
	g.GET("/user/me", s.GetCurrentUser)
	// NOTE: This should be moved to /api/v2/user/:username
	g.GET("/user/name/:username", s.GetUserByUsername)
	g.GET("/user/:id", s.GetUserByID)
	g.PATCH("/user/:id", s.UpdateUser)
	g.DELETE("/user/:id", s.DeleteUser)
}

// GetUserList godoc
//
//	@Summary	Get a list of users
//	@Tags		user
//	@Produce	json
//	@Success	200	{object}	[]store.User	"User list"
//	@Failure	500	{object}	nil				"Failed to fetch user list"
//	@Router		/api/v1/user [GET]
func (s *APIV1Service) GetUserList(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
	}
	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
	}
	if currentUser == nil {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
	}
	if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to list users")
	}

	list, err := s.Store.ListUsers(ctx, &store.FindUser{})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
	}

	userMessageList := make([]*User, 0, len(list))
	for _, user := range list {
		userMessage := convertUserFromStore(user)
		// data desensitize
		userMessage.Email = ""
		userMessageList = append(userMessageList, userMessage)
	}
	return c.JSON(http.StatusOK, userMessageList)
}

// CreateUser godoc
//
//	@Summary	Create a user
//	@Tags		user
//	@Accept		json
//	@Produce	json
//	@Param		body	body		CreateUserRequest	true	"Request object"
//	@Success	200		{object}	store.User			"Created user"
//	@Failure	400		{object}	nil					"Malformatted post user request | Invalid user create format"
//	@Failure	401		{object}	nil					"Missing auth session | Unauthorized to create user"
//	@Failure	403		{object}	nil					"Could not create host user"
//	@Failure	500		{object}	nil					"Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
//	@Router		/api/v1/user [POST]
func (s *APIV1Service) CreateUser(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
	}
	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &userID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
	}
	if currentUser == nil {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
	}
	if currentUser.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to create user")
	}

	userCreate := &CreateUserRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
	}
	if err := userCreate.Validate(); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
	}
	if !util.UIDMatcher.MatchString(strings.ToLower(userCreate.Username)) {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err)
	}
	// Disallow host user to be created.
	if userCreate.Role == RoleHost {
		return echo.NewHTTPError(http.StatusForbidden, "Could not create host user")
	}

	passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
	}

	user, err := s.Store.CreateUser(ctx, &store.User{
		Username:     userCreate.Username,
		Role:         store.Role(userCreate.Role),
		Email:        userCreate.Email,
		Nickname:     userCreate.Nickname,
		PasswordHash: string(passwordHash),
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
	}

	userMessage := convertUserFromStore(user)
	return c.JSON(http.StatusOK, userMessage)
}

// GetCurrentUser godoc
//
//	@Summary	Get current user
//	@Tags		user
//	@Produce	json
//	@Success	200	{object}	store.User	"Current user"
//	@Failure	401	{object}	nil			"Missing auth session"
//	@Failure	500	{object}	nil			"Failed to find user | Failed to find userSettingList"
//	@Router		/api/v1/user/me [GET]
func (s *APIV1Service) GetCurrentUser(c echo.Context) error {
	ctx := c.Request().Context()
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
	}

	userMessage := convertUserFromStore(user)
	return c.JSON(http.StatusOK, userMessage)
}

// GetUserByUsername godoc
//
//	@Summary	Get user by username
//	@Tags		user
//	@Produce	json
//	@Param		username	path		string		true	"Username"
//	@Success	200			{object}	store.User	"Requested user"
//	@Failure	404			{object}	nil			"User not found"
//	@Failure	500			{object}	nil			"Failed to find user"
//	@Router		/api/v1/user/name/{username} [GET]
func (s *APIV1Service) GetUserByUsername(c echo.Context) error {
	ctx := c.Request().Context()
	username := c.Param("username")
	user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil {
		return echo.NewHTTPError(http.StatusNotFound, "User not found")
	}

	userMessage := convertUserFromStore(user)
	// data desensitize
	userMessage.Email = ""
	return c.JSON(http.StatusOK, userMessage)
}

// GetUserByID godoc
//
//	@Summary	Get user by id
//	@Tags		user
//	@Produce	json
//	@Param		id	path		int			true	"User ID"
//	@Success	200	{object}	store.User	"Requested user"
//	@Failure	400	{object}	nil			"Malformatted user id"
//	@Failure	404	{object}	nil			"User not found"
//	@Failure	500	{object}	nil			"Failed to find user"
//	@Router		/api/v1/user/{id} [GET]
func (s *APIV1Service) GetUserByID(c echo.Context) error {
	ctx := c.Request().Context()
	id, err := util.ConvertStringToInt32(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
	}

	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &id})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if user == nil {
		return echo.NewHTTPError(http.StatusNotFound, "User not found")
	}

	userMessage := convertUserFromStore(user)
	userID, ok := c.Get(userIDContextKey).(int32)
	if !ok || userID != user.ID {
		// Data desensitize.
		userMessage.Email = ""
	}

	return c.JSON(http.StatusOK, userMessage)
}

// DeleteUser godoc
//
//	@Summary	Delete a user
//	@Tags		user
//	@Produce	json
//	@Param		id	path		string	true	"User ID"
//	@Success	200	{boolean}	true	"User deleted"
//	@Failure	400	{object}	nil		"ID is not a number: %s | Current session user not found with ID: %d"
//	@Failure	401	{object}	nil		"Missing user in session"
//	@Failure	403	{object}	nil		"Unauthorized to delete user"
//	@Failure	500	{object}	nil		"Failed to find user | Failed to delete user"
//	@Router		/api/v1/user/{id} [DELETE]
func (s *APIV1Service) DeleteUser(c echo.Context) error {
	ctx := c.Request().Context()
	currentUserID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}
	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
		ID: &currentUserID,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if currentUser == nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
	} else if currentUser.Role != store.RoleHost {
		return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete user").SetInternal(err)
	}

	userID, err := util.ConvertStringToInt32(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
	}
	if currentUserID == userID {
		return echo.NewHTTPError(http.StatusBadRequest, "Cannot delete current user")
	}

	if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
		ID: userID,
	}); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
	}
	return c.JSON(http.StatusOK, true)
}

// UpdateUser godoc
//
//	@Summary	Update a user
//	@Tags		user
//	@Produce	json
//	@Param		id		path		string				true	"User ID"
//	@Param		patch	body		UpdateUserRequest	true	"Patch request"
//	@Success	200		{object}	store.User			"Updated user"
//	@Failure	400		{object}	nil					"ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
//	@Failure	401		{object}	nil					"Missing user in session"
//	@Failure	403		{object}	nil					"Unauthorized to update user"
//	@Failure	500		{object}	nil					"Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
//	@Router		/api/v1/user/{id} [PATCH]
func (s *APIV1Service) UpdateUser(c echo.Context) error {
	ctx := c.Request().Context()
	userID, err := util.ConvertStringToInt32(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
	}

	currentUserID, ok := c.Get(userIDContextKey).(int32)
	if !ok {
		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
	}
	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &currentUserID})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
	}
	if currentUser == nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
	} else if currentUser.Role != store.RoleHost && currentUserID != userID {
		return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to update user").SetInternal(err)
	}

	request := &UpdateUserRequest{}
	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
	}
	if err := request.Validate(); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Invalid update user request").SetInternal(err)
	}

	currentTs := time.Now().Unix()
	userUpdate := &store.UpdateUser{
		ID:        userID,
		UpdatedTs: &currentTs,
	}
	if request.RowStatus != nil {
		rowStatus := store.RowStatus(request.RowStatus.String())
		userUpdate.RowStatus = &rowStatus
		if rowStatus == store.Archived && currentUserID == userID {
			return echo.NewHTTPError(http.StatusBadRequest, "Cannot archive current user")
		}
	}
	if request.Username != nil {
		if !util.UIDMatcher.MatchString(strings.ToLower(*request.Username)) {
			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err)
		}
		userUpdate.Username = request.Username
	}
	if request.Email != nil {
		userUpdate.Email = request.Email
	}
	if request.Nickname != nil {
		userUpdate.Nickname = request.Nickname
	}
	if request.Password != nil {
		passwordHash, err := bcrypt.GenerateFromPassword([]byte(*request.Password), bcrypt.DefaultCost)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
		}

		passwordHashStr := string(passwordHash)
		userUpdate.PasswordHash = &passwordHashStr
	}
	if request.AvatarURL != nil {
		userUpdate.AvatarURL = request.AvatarURL
	}

	user, err := s.Store.UpdateUser(ctx, userUpdate)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
	}

	userMessage := convertUserFromStore(user)
	return c.JSON(http.StatusOK, userMessage)
}

func (create CreateUserRequest) Validate() error {
	if len(create.Username) < 3 {
		return errors.New("username is too short, minimum length is 3")
	}
	if len(create.Username) > 32 {
		return errors.New("username is too long, maximum length is 32")
	}
	if len(create.Password) < 3 {
		return errors.New("password is too short, minimum length is 3")
	}
	if len(create.Password) > 512 {
		return errors.New("password is too long, maximum length is 512")
	}
	if len(create.Nickname) > 64 {
		return errors.New("nickname is too long, maximum length is 64")
	}
	if create.Email != "" {
		if len(create.Email) > 256 {
			return errors.New("email is too long, maximum length is 256")
		}
		if !util.ValidateEmail(create.Email) {
			return errors.New("invalid email format")
		}
	}

	return nil
}

func (update UpdateUserRequest) Validate() error {
	if update.Username != nil && len(*update.Username) < 3 {
		return errors.New("username is too short, minimum length is 3")
	}
	if update.Username != nil && len(*update.Username) > 32 {
		return errors.New("username is too long, maximum length is 32")
	}
	if update.Password != nil && len(*update.Password) < 3 {
		return errors.New("password is too short, minimum length is 3")
	}
	if update.Password != nil && len(*update.Password) > 512 {
		return errors.New("password is too long, maximum length is 512")
	}
	if update.Nickname != nil && len(*update.Nickname) > 64 {
		return errors.New("nickname is too long, maximum length is 64")
	}
	if update.AvatarURL != nil {
		if len(*update.AvatarURL) > 2<<20 {
			return errors.New("avatar is too large, maximum is 2MB")
		}
	}
	if update.Email != nil && *update.Email != "" {
		if len(*update.Email) > 256 {
			return errors.New("email is too long, maximum length is 256")
		}
		if !util.ValidateEmail(*update.Email) {
			return errors.New("invalid email format")
		}
	}

	return nil
}

func convertUserFromStore(user *store.User) *User {
	return &User{
		ID:           user.ID,
		RowStatus:    RowStatus(user.RowStatus),
		CreatedTs:    user.CreatedTs,
		UpdatedTs:    user.UpdatedTs,
		Username:     user.Username,
		Role:         Role(user.Role),
		Email:        user.Email,
		Nickname:     user.Nickname,
		PasswordHash: user.PasswordHash,
		AvatarURL:    user.AvatarURL,
	}
}
package v1

import (
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"

	"github.com/usememos/memos/plugin/telegram"
	"github.com/usememos/memos/server/profile"
	"github.com/usememos/memos/server/route/resource"
	"github.com/usememos/memos/server/route/rss"
	"github.com/usememos/memos/store"
)

type APIV1Service struct {
	Secret      string
	Profile     *profile.Profile
	Store       *store.Store
	telegramBot *telegram.Bot
}

// @title						memos API
// @version					1.0
// @description				A privacy-first, lightweight note-taking service.
//
// @contact.name				API Support
// @contact.url				https://github.com/orgs/usememos/discussions
//
// @license.name				MIT License
// @license.url				https://github.com/usememos/memos/blob/main/LICENSE
//
// @BasePath					/
//
// @externalDocs.url			https://usememos.com/
// @externalDocs.description	Find out more about Memos.
func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store, telegramBot *telegram.Bot) *APIV1Service {
	return &APIV1Service{
		Secret:      secret,
		Profile:     profile,
		Store:       store,
		telegramBot: telegramBot,
	}
}

func (s *APIV1Service) Register(rootGroup *echo.Group) {
	// Register API v1 routes.
	apiV1Group := rootGroup.Group("/api/v1")
	apiV1Group.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
		Store: middleware.NewRateLimiterMemoryStoreWithConfig(
			middleware.RateLimiterMemoryStoreConfig{Rate: 30, Burst: 100, ExpiresIn: 3 * time.Minute},
		),
		IdentifierExtractor: func(ctx echo.Context) (string, error) {
			id := ctx.RealIP()
			return id, nil
		},
		ErrorHandler: func(context echo.Context, err error) error {
			return context.JSON(http.StatusForbidden, nil)
		},
		DenyHandler: func(context echo.Context, identifier string, err error) error {
			return context.JSON(http.StatusTooManyRequests, nil)
		},
	}))
	apiV1Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return JWTMiddleware(s, next, s.Secret)
	})
	s.registerSystemRoutes(apiV1Group)
	s.registerSystemSettingRoutes(apiV1Group)
	s.registerAuthRoutes(apiV1Group)
	s.registerUserRoutes(apiV1Group)
	s.registerTagRoutes(apiV1Group)
	s.registerStorageRoutes(apiV1Group)
	s.registerResourceRoutes(apiV1Group)
	s.registerMemoRoutes(apiV1Group)
	s.registerMemoOrganizerRoutes(apiV1Group)
	s.registerMemoRelationRoutes(apiV1Group)

	// Register public routes.
	publicGroup := rootGroup.Group("/o")
	publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return JWTMiddleware(s, next, s.Secret)
	})
	s.registerGetterPublicRoutes(publicGroup)

	// Create and register resource public routes.
	resource.NewResourceService(s.Profile, s.Store).RegisterRoutes(publicGroup)

	// Create and register rss public routes.
	rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup)

	// programmatically set API version same as the server version
	SwaggerInfo.Version = s.Profile.Version
}
		Storage: convertStorageFromStore(storage),
		response.Storages = append(response.Storages, convertStorageFromStore(storage))
		Storage: convertStorageFromStore(storage),
		Storage: convertStorageFromStore(storage),
func convertStorageFromStore(storage *storepb.Storage) *apiv2pb.Storage {
	apiv1 "github.com/usememos/memos/server/route/api/v1"
	// Register API v1 endpoints.
	rootGroup := e.Group("")
	apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store, s.telegramBot)
	apiV1Service.Register(rootGroup)

Fixed code: 	apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
	storepb "github.com/usememos/memos/proto/gen/store"
	apiv2 "github.com/usememos/memos/server/route/api/v2"
	workspaceStorageSetting, err := dataStore.GetWorkspaceStorageSetting(ctx)
		return nil, errors.Wrap(err, "Failed to find workspaceStorageSetting")
	if workspaceStorageSetting.StorageType != storepb.WorkspaceStorageSetting_STORAGE_TYPE_EXTERNAL || workspaceStorageSetting.ActivedExternalStorageId == nil {
		return nil, nil
	storage, err := dataStore.GetStorageV1(ctx, &store.FindStorage{ID: workspaceStorageSetting.ActivedExternalStorageId})
		return nil, errors.Wrap(err, "Failed to find storage")
		return nil, nil
	storageMessage := apiv2.ConvertStorageFromStore(storage)
	if storageMessage.Type != apiv2pb.Storage_S3 {
	s3Config := storageMessage.Config.GetS3Config()
		URLPrefix: s3Config.UrlPrefix,
		URLSuffix: s3Config.UrlSuffix,
			Blob:      attachment.Data,
		err := apiv2.SaveResourceBlob(ctx, t.store, &create)
		Storage: ConvertStorageFromStore(storage),
		response.Storages = append(response.Storages, ConvertStorageFromStore(storage))
		Storage: ConvertStorageFromStore(storage),
		Storage: ConvertStorageFromStore(storage),
func ConvertStorageFromStore(storage *storepb.Storage) *apiv2pb.Storage {