CVE-2024-29029 (Go, published 2024-04-19T16:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/usememos/memos
Files changed:
internal/jobs/presign_link.go
server/integration/telegram.go
server/route/api/v2/storage_service.go
server/server.go
Commit message: chore: retire legacy api


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

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

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

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

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

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

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

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

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

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

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

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

	user, err := s.Store.GetUser(ctx, &store.FindUser{
		Username: &signin.Username,
	})
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
	}
	if user == nil {
		return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
	} else if user.RowStatus == store.Archived {
		return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
	}

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

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

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

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

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

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

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

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

	userCreate := &store.User{
		Username: signup.Username,
		// The new signup user should be normal user by default.
		Role:     store.RoleUser,
		Nickname: signup.Username,
	}
	if len(existedHostUsers) == 0 {
		// Change the default role to host if there is no host user.
		userCreate.Role = store.RoleHost
	} else {
		workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
		}
		if workspaceGeneralSetting.DisallowSignup {
			return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
		}
		if workspaceGeneralSetting.DisallowPasswordLogin {
			return echo.NewHTTPError(http.StatusUnauthorized, "password login is deactivated").SetInternal(err)
		}
	}

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

	userCreate.PasswordHash = string(passwordHash)
	user, err := s.Store.CreateUser(ctx, userCreate)
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
	}
	accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
	}
	if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
	}
	cookieExp := time.Now().Add(auth.CookieExpDuration)
	setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
	userMessage := convertUserFromStore(user)
	return c.JSON(http.StatusOK, userMessage)
}

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

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

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

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

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

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

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

package v1

import "github.com/swaggo/swag"

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

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

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

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

	"github.com/labstack/echo/v4"

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

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

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

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

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

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

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

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

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

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

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

	return authHeaderParts[1], nil
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Visibility is the type of a visibility.
type Visibility string

const (
	// Public is the PUBLIC visibility.
	Public Visibility = "PUBLIC"
	// Protected is the PROTECTED visibility.
	Protected Visibility = "PROTECTED"
	// Private is the PRIVATE visibility.
	Private Visibility = "PRIVATE"
)

func (v Visibility) String() string {
	switch v {
	case Public:
		return "PUBLIC"
	case Protected:
		return "PROTECTED"
	case Private:
		return "PRIVATE"
	}
	return "PRIVATE"
}

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

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

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

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

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

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

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

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

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

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

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

type FindMemoRequest struct {
	ID *int32

	// Standard fields
	RowStatus *RowStatus
	CreatorID *int32

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

	// Pagination
	Limit  *int
	Offset *int
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	"github.com/labstack/echo/v4"

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

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

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

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

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

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

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

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

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

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

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

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

	"github.com/labstack/echo/v4"

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

type MemoRelationType string

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return nil
	}

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

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

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

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

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

	create.ExternalLink = link
	return nil
}
package v1

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

	"github.com/labstack/echo/v4"

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

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

type StorageType string

const (
	StorageS3 StorageType = "S3"
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Version: 1.0

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

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

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

### /api/v1/auth/signin

#### POST
##### Summary

Sign-in to memos.

##### Parameters

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

##### Responses

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

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

#### POST
##### Summary

Sign-in to memos using SSO.

##### Parameters

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

##### Responses

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

### /api/v1/auth/signout

#### POST
##### Summary

Sign-out from memos.

##### Responses

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

### /api/v1/auth/signup

#### POST
##### Summary

Sign-up to memos.

##### Parameters

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

##### Responses

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

### /api/v1/idp

#### GET
##### Summary

Get a list of identity providers

##### Description

*clientSecret is only available for host user

##### Responses

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

#### POST
##### Summary

Create Identity Provider

##### Parameters

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

##### Responses

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

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

#### DELETE
##### Summary

Delete an identity provider by ID

##### Parameters

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

##### Responses

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

#### GET
##### Summary

Get an identity provider by ID

##### Parameters

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

##### Responses

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

#### PATCH
##### Summary

Update an identity provider by ID

##### Parameters

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

##### Responses

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

### /api/v1/memo

#### GET
##### Summary

Get a list of memos matching optional filters

##### Parameters

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

##### Responses

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

#### POST
##### Summary

Create a memo

##### Description

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

##### Parameters

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

##### Responses

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

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

#### DELETE
##### Summary

Delete memo by ID

##### Parameters

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

##### Responses

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

#### GET
##### Summary

Get memo by ID

##### Parameters

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

##### Responses

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

#### PATCH
##### Summary

Update a memo

##### Description

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

##### Parameters

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

##### Responses

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

### /api/v1/memo/all

#### GET
##### Summary

Get a list of public memos matching optional filters

##### Description

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

##### Parameters

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

##### Responses

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

### /api/v1/memo/stats

#### GET
##### Summary

Get memo stats by creator ID or username

##### Description

Used to generate the heatmap

##### Parameters

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

##### Responses

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

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

#### POST
##### Summary

Organize memo (pin/unpin)

##### Parameters

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

##### Responses

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

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

#### GET
##### Summary

Get a list of Memo Relations

##### Parameters

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

##### Responses

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

#### POST
##### Summary

Create Memo Relation

##### Description

Create a relation between two memos

##### Parameters

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

##### Responses

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

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

#### DELETE
##### Summary

Delete a Memo Relation

##### Description

Removes a relation between two memos

##### Parameters

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

##### Responses

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

### /api/v1/ping

#### GET
##### Summary

Ping the system

##### Responses

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

### /api/v1/status

#### GET
##### Summary

Get system GetSystemStatus

##### Responses

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

### /api/v1/system/vacuum

#### POST
##### Summary

Vacuum the database

##### Responses

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

### /api/v1/resource

#### GET
##### Summary

Get a list of resources

##### Parameters

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

##### Responses

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

#### POST
##### Summary

Create resource

##### Parameters

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

##### Responses

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

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

#### DELETE
##### Summary

Delete a resource

##### Parameters

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

##### Responses

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

#### PATCH
##### Summary

Update a resource

##### Parameters

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

##### Responses

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

### /api/v1/resource/blob

#### POST
##### Summary

Upload resource

##### Parameters

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

##### Responses

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

### /api/v1/storage

#### GET
##### Summary

Get a list of storages

##### Responses

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

#### POST
##### Summary

Create storage

##### Parameters

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

##### Responses

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

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

#### DELETE
##### Summary

Delete a storage

##### Parameters

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

##### Responses

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

#### PATCH
##### Summary

Update a storage

##### Parameters

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

##### Responses

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

### /api/v1/system/setting

#### GET
##### Summary

Get a list of system settings

##### Responses

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

#### POST
##### Summary

Create system setting

##### Parameters

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

##### Responses

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

### /api/v1/tag

#### GET
##### Summary

Get a list of tags

##### Responses

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

#### POST
##### Summary

Create a tag

##### Parameters

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

##### Responses

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

### /api/v1/tag/delete

#### POST
##### Summary

Delete a tag

##### Parameters

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

##### Responses

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

### /api/v1/tag/suggestion

#### GET
##### Summary

Get a list of tags suggested from other memos contents

##### Responses

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

### /api/v1/user

#### GET
##### Summary

Get a list of users

##### Responses

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

#### POST
##### Summary

Create a user

##### Parameters

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

##### Responses

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

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

#### DELETE
##### Summary

Delete a user

##### Parameters

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

##### Responses

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

#### GET
##### Summary

Get user by id

##### Parameters

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

##### Responses

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

#### PATCH
##### Summary

Update a user

##### Parameters

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

##### Responses

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

### /api/v1/user/me

#### GET
##### Summary

Get current user

##### Responses

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

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

#### GET
##### Summary

Get user by username

##### Parameters

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

##### Responses

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

### /o/get/GetImage

#### GET
##### Summary

Get GetImage from URL

##### Parameters

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

##### Responses

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

### Models

#### api_v1.CreateIdentityProviderRequest

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

#### api_v1.CreateMemoRequest

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

#### api_v1.CreateResourceRequest

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

#### api_v1.CreateStorageRequest

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

#### api_v1.CreateUserRequest

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

#### api_v1.CustomizedProfile

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

#### api_v1.DeleteTagRequest

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

#### api_v1.FieldMapping

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

#### api_v1.IdentityProvider

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

#### api_v1.IdentityProviderConfig

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

#### api_v1.IdentityProviderOAuth2Config

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

#### api_v1.IdentityProviderType

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

#### api_v1.MemoRelationType

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

#### api_v1.PatchMemoRequest

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

#### api_v1.Role

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

#### api_v1.RowStatus

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

#### api_v1.SSOSignIn

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

#### api_v1.SignIn

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

#### api_v1.SignUp

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

#### api_v1.StorageConfig

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

#### api_v1.StorageS3Config

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

#### api_v1.StorageType

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

#### api_v1.SystemSetting

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

#### api_v1.SystemSettingName

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

#### api_v1.SystemStatus

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

#### api_v1.UpdateIdentityProviderRequest

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

#### api_v1.UpdateResourceRequest

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

#### api_v1.UpdateStorageRequest

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

#### api_v1.UpdateUserRequest

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

#### api_v1.UpsertMemoOrganizerRequest

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

#### api_v1.UpsertMemoRelationRequest

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

#### api_v1.UpsertSystemSettingRequest

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

#### api_v1.UpsertTagRequest

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

#### api_v1.User

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

#### api_v1.Visibility

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

#### github_com_usememos_memos_api_v1.CreateIdentityProviderRequest

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

#### github_com_usememos_memos_api_v1.CreateMemoRequest

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

#### github_com_usememos_memos_api_v1.CreateResourceRequest

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

#### github_com_usememos_memos_api_v1.CreateStorageRequest

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

#### github_com_usememos_memos_api_v1.CreateUserRequest

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

#### github_com_usememos_memos_api_v1.CustomizedProfile

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

#### github_com_usememos_memos_api_v1.DeleteTagRequest

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

#### github_com_usememos_memos_api_v1.FieldMapping

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

#### github_com_usememos_memos_api_v1.IdentityProvider

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

#### github_com_usememos_memos_api_v1.IdentityProviderConfig

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

#### github_com_usememos_memos_api_v1.IdentityProviderOAuth2Config

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

#### github_com_usememos_memos_api_v1.IdentityProviderType

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

#### github_com_usememos_memos_api_v1.MemoRelationType

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

#### github_com_usememos_memos_api_v1.PatchMemoRequest

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

#### github_com_usememos_memos_api_v1.Role

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

#### github_com_usememos_memos_api_v1.RowStatus

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

#### github_com_usememos_memos_api_v1.SSOSignIn

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

#### github_com_usememos_memos_api_v1.SignIn

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

#### github_com_usememos_memos_api_v1.SignUp

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

#### github_com_usememos_memos_api_v1.StorageConfig

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

#### github_com_usememos_memos_api_v1.StorageS3Config

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

#### github_com_usememos_memos_api_v1.StorageType

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

#### github_com_usememos_memos_api_v1.SystemSetting

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

#### github_com_usememos_memos_api_v1.SystemSettingName

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

#### github_com_usememos_memos_api_v1.SystemStatus

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

#### github_com_usememos_memos_api_v1.UpdateIdentityProviderRequest

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

#### github_com_usememos_memos_api_v1.UpdateResourceRequest

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

#### github_com_usememos_memos_api_v1.UpdateStorageRequest

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

#### github_com_usememos_memos_api_v1.UpdateUserRequest

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

#### github_com_usememos_memos_api_v1.UpsertMemoOrganizerRequest

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

#### github_com_usememos_memos_api_v1.UpsertMemoRelationRequest

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

#### github_com_usememos_memos_api_v1.UpsertSystemSettingRequest

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

#### github_com_usememos_memos_api_v1.UpsertTagRequest

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

#### github_com_usememos_memos_api_v1.User

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

#### github_com_usememos_memos_api_v1.Visibility

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

#### profile.Profile

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

#### store.FieldMapping

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

#### store.IdentityProvider

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

#### store.IdentityProviderConfig

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

#### store.IdentityProviderOAuth2Config

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

#### store.IdentityProviderType

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

#### store.Memo

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

#### store.MemoRelation

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

#### store.MemoRelationType

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

#### store.Resource

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

#### store.Role

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

#### store.RowStatus

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

#### store.Storage

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

#### store.User

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

#### store.Visibility

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

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

	"github.com/labstack/echo/v4"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

type SystemSettingName string

const (
	// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
	SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
	// SystemSettingCustomizedProfileName is the name of customized server profile.
	SystemSettingCustomizedProfileName SystemSettingName = "customized-profile"
	// SystemSettingStorageServiceIDName is the name of storage service ID.
	SystemSettingStorageServiceIDName SystemSettingName = "storage-service-id"
	// SystemSettingLocalStoragePathName is the name of local storage path.
	SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path"
)
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`

// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
type CustomizedProfile struct {
	// Name is the server name, default is `memos`
	Name string `json:"name"`
	// LogoURL is the url of logo image.
	LogoURL string `json:"logoUrl"`
	// Description is the server description.
	Description string `json:"description"`
	// Locale is the server default locale.
	Locale string `json:"locale"`
	// Appearance is the server default appearance.
	Appearance string `json:"appearance"`
}

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

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

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

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

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

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

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

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

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

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

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

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

func (upsert UpsertSystemSettingRequest) Validate() error {
	switch settingName := upsert.Name; settingName {
	case SystemSettingMaxUploadSizeMiBName:
		var value int
		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
			return errors.Errorf(systemSettingUnmarshalError, settingName)
		}
	case SystemSettingCustomizedProfileName:
		customizedProfile := CustomizedProfile{
			Name:        "Memos",
			LogoURL:     "",
			Description: "",
			Locale:      "en",
			Appearance:  "system",
		}
		if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
			return errors.Errorf(systemSettingUnmarshalError, settingName)
		}
	case SystemSettingStorageServiceIDName:
		// Note: 0 is the default value(database) for storage service ID.
		value := 0
		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
			return errors.Errorf(systemSettingUnmarshalError, settingName)
		}
		return nil
	case SystemSettingLocalStoragePathName:
		value := ""
		if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
			return errors.Errorf(systemSettingUnmarshalError, settingName)
		}

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

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

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

	"github.com/labstack/echo/v4"

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

type Tag struct {
	Name      string
	CreatorID int32
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

import (
	"testing"
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	return nil
}

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

	return nil
}

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

import (
	"net/http"
	"time"

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

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

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

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

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

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

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

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

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


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