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