VYPR
Moderate severityNVD Advisory· Published Apr 19, 2024· Updated Aug 2, 2024

memos vulnerable to an SSRF in /o/get/image

CVE-2024-29029

Description

memos is a privacy-first, lightweight note-taking service. In memos 0.13.2, an SSRF vulnerability exists at the /o/get/image that allows unauthenticated users to enumerate the internal network and retrieve images. The response from the image request is then copied into the response of the current server request, causing a reflected XSS vulnerability. Version 0.22.0 of memos removes the vulnerable file.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/usememos/memosGo
< 0.22.00.22.0

Affected products

1

Patches

1
bbd206e89302

chore: retire legacy api

https://github.com/usememos/memosStevenApr 13, 2024via ghsa
22 files changed · +22 11119
  • internal/jobs/presign_link.go+15 23 modified
    @@ -2,15 +2,16 @@ 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"
     )
     
    @@ -95,44 +96,35 @@ func signExternalLinks(ctx context.Context, dataStore *store.Store) error {
     // 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,
     	})
     }
    
  • server/integration/telegram.go+2 3 modified
    @@ -1,7 +1,6 @@
     package integration
     
     import (
    -	"bytes"
     	"context"
     	"fmt"
     	"path/filepath"
    @@ -19,7 +18,6 @@ import (
     	"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"
     )
    @@ -126,9 +124,10 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
     			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
    
  • server/route/api/v1/auth.go+0 253 removed
    @@ -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)
    -}
    
  • server/route/api/v1/common.go+0 15 removed
    @@ -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)
    -}
    
  • server/route/api/v1/docs.go+0 3393 removed
  • server/route/api/v1/http_getter.go+0 49 removed
    @@ -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
    -}
    
  • server/route/api/v1/jwt.go+0 155 removed
    @@ -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
    -}
    
  • server/route/api/v1/memo.go+0 943 removed
    @@ -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
    -			}(),
    -		},
    -	}
    -}
    
  • server/route/api/v1/memo_organizer.go+0 97 removed
    @@ -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)
    -}
    
  • server/route/api/v1/memo_relation.go+0 156 removed
    @@ -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),
    -	}
    -}
    
  • server/route/api/v1/resource.go+0 505 removed
    @@ -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
    -}
    
  • server/route/api/v1/storage.go+0 316 removed
    @@ -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
    -}
    
  • server/route/api/v1/swagger.md+0 1708 removed
    @@ -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 |  |  |
    
  • server/route/api/v1/swagger.yaml+0 2278 removed
    @@ -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"
    
  • server/route/api/v1/system.go+0 157 removed
    @@ -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)
    -}
    
  • server/route/api/v1/system_setting.go+0 211 removed
    @@ -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,
    -	}
    -}
    
  • server/route/api/v1/tag.go+0 218 removed
    @@ -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
    -}
    
  • server/route/api/v1/tag_test.go+0 47 removed
    @@ -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)
    -		}
    -	}
    -}
    
  • server/route/api/v1/user.go+0 487 removed
    @@ -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,
    -	}
    -}
    
  • server/route/api/v1/v1.go+0 94 removed
    @@ -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
    -}
    
  • server/route/api/v2/storage_service.go+5 5 modified
    @@ -25,7 +25,7 @@ func (s *APIV2Service) CreateStorage(ctx context.Context, request *apiv2pb.Creat
     		return nil, status.Errorf(codes.Internal, "failed to create storage, error: %+v", err)
     	}
     	return &apiv2pb.CreateStorageResponse{
    -		Storage: convertStorageFromStore(storage),
    +		Storage: ConvertStorageFromStore(storage),
     	}, nil
     }
     
    @@ -39,7 +39,7 @@ func (s *APIV2Service) ListStorages(ctx context.Context, _ *apiv2pb.ListStorages
     		Storages: []*apiv2pb.Storage{},
     	}
     	for _, storage := range storages {
    -		response.Storages = append(response.Storages, convertStorageFromStore(storage))
    +		response.Storages = append(response.Storages, ConvertStorageFromStore(storage))
     	}
     	return response, nil
     }
    @@ -55,7 +55,7 @@ func (s *APIV2Service) GetStorage(ctx context.Context, request *apiv2pb.GetStora
     		return nil, status.Errorf(codes.NotFound, "storage not found")
     	}
     	return &apiv2pb.GetStorageResponse{
    -		Storage: convertStorageFromStore(storage),
    +		Storage: ConvertStorageFromStore(storage),
     	}, nil
     }
     
    @@ -82,7 +82,7 @@ func (s *APIV2Service) UpdateStorage(ctx context.Context, request *apiv2pb.Updat
     		return nil, status.Errorf(codes.Internal, "failed to update storage, error: %+v", err)
     	}
     	return &apiv2pb.UpdateStorageResponse{
    -		Storage: convertStorageFromStore(storage),
    +		Storage: ConvertStorageFromStore(storage),
     	}, nil
     }
     
    @@ -96,7 +96,7 @@ func (s *APIV2Service) DeleteStorage(ctx context.Context, request *apiv2pb.Delet
     	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,
    
  • server/server.go+0 6 modified
    @@ -15,7 +15,6 @@ import (
     	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"
    @@ -75,11 +74,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.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 {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.