CVE-2024-29030 (Go, published 2024-04-19T15:15Z)
Description: value: memos is a privacy-first, lightweight note-taking service. In memos 0.13.2, an SSRF vulnerability exists at the /api/resource that allows authenticated users to enumerate the internal network.
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 {