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