mirror of https://github.com/usememos/memos
refactor: migrate memo to apiv1 (#1907)
* refactor: migrate memo to apiv1 * chore: update * chore: update * chore: update * chore: upate * chore: update * chore: updatepull/1908/head
parent
1fa9f162a5
commit
a7573d5705
@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
// UnknownID is the ID for unknowns.
|
||||
const UnknownID = -1
|
||||
|
||||
// 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 (e RowStatus) String() string {
|
||||
switch e {
|
||||
case Normal:
|
||||
return "NORMAL"
|
||||
case Archived:
|
||||
return "ARCHIVED"
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package api
|
||||
|
||||
// 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 MemoResponse struct {
|
||||
ID int `json:"id"`
|
||||
|
||||
// Standard fields
|
||||
RowStatus RowStatus `json:"rowStatus"`
|
||||
CreatorID int `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"`
|
||||
ResourceList []*Resource `json:"resourceList"`
|
||||
RelationList []*MemoRelation `json:"relationList"`
|
||||
}
|
||||
|
||||
type CreateMemoRequest struct {
|
||||
// Standard fields
|
||||
CreatorID int `json:"-"`
|
||||
CreatedTs *int64 `json:"createdTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Content string `json:"content"`
|
||||
|
||||
// Related fields
|
||||
ResourceIDList []int `json:"resourceIdList"`
|
||||
RelationList []*MemoRelationUpsert `json:"relationList"`
|
||||
}
|
||||
|
||||
type PatchMemoRequest struct {
|
||||
ID int `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 []int `json:"resourceIdList"`
|
||||
RelationList []*MemoRelationUpsert `json:"relationList"`
|
||||
}
|
||||
|
||||
type FindMemoRequest struct {
|
||||
ID *int
|
||||
|
||||
// Standard fields
|
||||
RowStatus *RowStatus
|
||||
CreatorID *int
|
||||
|
||||
// Domain specific fields
|
||||
Pinned *bool
|
||||
ContentSearch []string
|
||||
VisibilityList []Visibility
|
||||
|
||||
// Pagination
|
||||
Limit *int
|
||||
Offset *int
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
type MemoOrganizer struct {
|
||||
// Domain specific fields
|
||||
MemoID int
|
||||
UserID int
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
type MemoOrganizerUpsert struct {
|
||||
MemoID int `json:"-"`
|
||||
UserID int `json:"-"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type MemoOrganizerFind struct {
|
||||
MemoID int
|
||||
UserID int
|
||||
}
|
||||
|
||||
type MemoOrganizerDelete struct {
|
||||
MemoID *int
|
||||
UserID *int
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package api
|
||||
|
||||
type MemoRelationType string
|
||||
|
||||
const (
|
||||
MemoRelationReference MemoRelationType = "REFERENCE"
|
||||
MemoRelationAdditional MemoRelationType = "ADDITIONAL"
|
||||
)
|
||||
|
||||
type MemoRelation struct {
|
||||
MemoID int `json:"memoId"`
|
||||
RelatedMemoID int `json:"relatedMemoId"`
|
||||
Type MemoRelationType `json:"type"`
|
||||
}
|
||||
|
||||
type MemoRelationUpsert struct {
|
||||
RelatedMemoID int `json:"relatedMemoId"`
|
||||
Type MemoRelationType `json:"type"`
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
type MemoResource struct {
|
||||
MemoID int
|
||||
ResourceID int
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
}
|
||||
|
||||
type MemoResourceUpsert struct {
|
||||
MemoID int `json:"-"`
|
||||
ResourceID int
|
||||
UpdatedTs *int64
|
||||
}
|
||||
|
||||
type MemoResourceFind struct {
|
||||
MemoID *int
|
||||
ResourceID *int
|
||||
}
|
||||
|
||||
type MemoResourceDelete struct {
|
||||
MemoID *int
|
||||
ResourceID *int
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package api
|
||||
|
||||
type Resource struct {
|
||||
ID int `json:"id"`
|
||||
|
||||
// Standard fields
|
||||
CreatorID int `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"`
|
||||
PublicID string `json:"publicId"`
|
||||
|
||||
// Related fields
|
||||
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
type MemoOrganizer struct {
|
||||
MemoID int `json:"memoId"`
|
||||
UserID int `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", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoID, err := strconv.Atoi(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(getUserIDContextKey()).(int)
|
||||
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.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.UpsertMemoOrganizerV1(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)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func composeResponse(data any) response {
|
||||
return response{
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultGetRequestSkipper(c echo.Context) bool {
|
||||
return c.Request().Method == http.MethodGet
|
||||
}
|
||||
|
||||
func defaultAPIRequestSkipper(c echo.Context) bool {
|
||||
path := c.Path()
|
||||
return common.HasPrefixes(path, "/api")
|
||||
}
|
||||
|
||||
func (s *Server) defaultAuthSkipper(c echo.Context) bool {
|
||||
ctx := c.Request().Context()
|
||||
path := c.Path()
|
||||
|
||||
// Skip auth.
|
||||
if common.HasPrefixes(path, "/api/v1/auth") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
OpenID: &openID,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return false
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/auth"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
const (
|
||||
// Context section
|
||||
// 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 getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
// Claims creates a struct that will be encoded to a JWT.
|
||||
// We add jwt.RegisteredClaims as an embedded type, to provide fields such as name.
|
||||
type Claims struct {
|
||||
Name string `json:"name"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
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 {
|
||||
accessToken := ""
|
||||
cookie, _ := c.Cookie(auth.AccessTokenCookieName)
|
||||
if cookie != nil {
|
||||
accessToken = cookie.Value
|
||||
}
|
||||
if accessToken == "" {
|
||||
accessToken, _ = extractTokenFromHeader(c)
|
||||
}
|
||||
|
||||
return accessToken
|
||||
}
|
||||
|
||||
func audienceContains(audience jwt.ClaimStrings, token string) bool {
|
||||
for _, v := range audience {
|
||||
if v == token {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// JWTMiddleware validates the access token.
|
||||
// If the access token is about to expire or has expired and the request has a valid refresh token, it
|
||||
// will try to generate new access token and refresh token.
|
||||
func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
path := c.Request().URL.Path
|
||||
method := c.Request().Method
|
||||
|
||||
if server.defaultAuthSkipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
token := findAccessToken(c)
|
||||
if token == "" {
|
||||
// Allow the user to access the public endpoints.
|
||||
if common.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 common.HasPrefixes(path, "/api/memo") && method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
accessToken, err := jwt.ParseWithClaims(token, 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 !accessToken.Valid {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.")
|
||||
}
|
||||
|
||||
if !audienceContains(claims.Audience, auth.AccessTokenAudienceName) {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Invalid access token, audience mismatch, got %q, expected %q.", claims.Audience, auth.AccessTokenAudienceName))
|
||||
}
|
||||
generateToken := time.Until(claims.ExpiresAt.Time) < auth.RefreshThresholdDuration
|
||||
if err != nil {
|
||||
var ve *jwt.ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
// If expiration error is the only error, we will clear the err
|
||||
// and generate new access token and refresh token
|
||||
if ve.Errors == jwt.ValidationErrorExpired {
|
||||
generateToken = true
|
||||
}
|
||||
} else {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, errors.Wrap(err, "Invalid or expired access token"))
|
||||
}
|
||||
}
|
||||
|
||||
// We either have a valid access token or we will attempt to generate new access token and refresh token
|
||||
ctx := c.Request().Context()
|
||||
userID, err := strconv.Atoi(claims.Subject)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Malformed ID in the 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))
|
||||
}
|
||||
|
||||
if generateToken {
|
||||
generateTokenFunc := func() error {
|
||||
rc, err := c.Cookie(auth.RefreshTokenCookieName)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Failed to generate access token. Missing refresh token.")
|
||||
}
|
||||
|
||||
// Parses token and checks if it's valid.
|
||||
refreshTokenClaims := &Claims{}
|
||||
refreshToken, err := jwt.ParseWithClaims(rc.Value, refreshTokenClaims, func(t *jwt.Token) (any, error) {
|
||||
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
|
||||
return nil, errors.Errorf("unexpected refresh token signing method=%v, expected %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 refresh token kid=%v", t.Header["kid"])
|
||||
})
|
||||
if err != nil {
|
||||
if err == jwt.ErrSignatureInvalid {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Failed to generate access token. Invalid refresh token signature.")
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to refresh expired token. User Id %d", userID)).SetInternal(err)
|
||||
}
|
||||
|
||||
if !audienceContains(refreshTokenClaims.Audience, auth.RefreshTokenAudienceName) {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized,
|
||||
fmt.Sprintf("Invalid refresh token, audience mismatch, got %q, expected %q. you may send request to the wrong environment",
|
||||
refreshTokenClaims.Audience,
|
||||
auth.RefreshTokenAudienceName,
|
||||
))
|
||||
}
|
||||
|
||||
// If we have a valid refresh token, we will generate new access token and refresh token
|
||||
if refreshToken != nil && refreshToken.Valid {
|
||||
if err := auth.GenerateTokensAndSetCookies(c, user, secret); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to refresh expired token. User Id %d", userID)).SetInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// It may happen that we still have a valid access token, but we encounter issue when trying to generate new token
|
||||
// In such case, we won't return the error.
|
||||
if err := generateTokenFunc(); err != nil && !accessToken.Valid {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), userID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
@ -1,735 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/api"
|
||||
apiv1 "github.com/usememos/memos/api/v1"
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// maxContentLength means the max memo content bytes is 1MB.
|
||||
const maxContentLength = 1 << 30
|
||||
|
||||
func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
g.POST("/memo", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
createMemoRequest := &api.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: apiv1.UserSettingMemoVisibilityKey.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
|
||||
}
|
||||
|
||||
if userMemoVisibilitySetting != nil {
|
||||
memoVisibility := api.Private
|
||||
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
|
||||
}
|
||||
createMemoRequest.Visibility = memoVisibility
|
||||
} else {
|
||||
// Private is the default memo visibility.
|
||||
createMemoRequest.Visibility = api.Private
|
||||
}
|
||||
}
|
||||
|
||||
// Find disable public memos system setting.
|
||||
disablePublicMemosSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
|
||||
Name: apiv1.SystemSettingDisablePublicMemosName.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
|
||||
}
|
||||
if disablePublicMemosSystemSetting != nil {
|
||||
disablePublicMemos := false
|
||||
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
||||
}
|
||||
if disablePublicMemos {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||
}
|
||||
// Enforce normal user to create private memo if public memos are disabled.
|
||||
if user.Role == store.RoleUser {
|
||||
createMemoRequest.Visibility = api.Private
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createMemoRequest.CreatorID = userID
|
||||
memoMessage, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
|
||||
}
|
||||
if err := s.createMemoCreateActivity(ctx, memoMessage); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
for _, resourceID := range createMemoRequest.ResourceIDList {
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||
MemoID: memoMessage.ID,
|
||||
ResourceID: resourceID,
|
||||
}); 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.MemoRelationMessage{
|
||||
MemoID: memoMessage.ID,
|
||||
RelatedMemoID: memoRelationUpsert.RelatedMemoID,
|
||||
Type: store.MemoRelationType(memoRelationUpsert.Type),
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoMessage.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.PATCH("/memo/:memoId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
memoID, err := strconv.Atoi(c.Param("memoId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memoMessage.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
patchMemoRequest := &api.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.UpdateMemoMessage{
|
||||
ID: memoID,
|
||||
CreatedTs: patchMemoRequest.CreatedTs,
|
||||
UpdatedTs: patchMemoRequest.UpdatedTs,
|
||||
Content: patchMemoRequest.Content,
|
||||
}
|
||||
if patchMemoRequest.RowStatus != nil {
|
||||
rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String())
|
||||
updateMemoMessage.RowStatus = &rowStatus
|
||||
}
|
||||
if patchMemoRequest.Visibility != nil {
|
||||
visibility := store.Visibility(patchMemoRequest.Visibility.String())
|
||||
updateMemoMessage.Visibility = &visibility
|
||||
}
|
||||
|
||||
err = s.Store.UpdateMemo(ctx, updateMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
|
||||
}
|
||||
memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{ID: &memoID})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
|
||||
if patchMemoRequest.ResourceIDList != nil {
|
||||
addedResourceIDList, removedResourceIDList := getIDListDiff(memoMessage.ResourceIDList, patchMemoRequest.ResourceIDList)
|
||||
for _, resourceID := range addedResourceIDList {
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
|
||||
MemoID: memoMessage.ID,
|
||||
ResourceID: resourceID,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
|
||||
}
|
||||
}
|
||||
for _, resourceID := range removedResourceIDList {
|
||||
if err := s.Store.DeleteMemoResource(ctx, &api.MemoResourceDelete{
|
||||
MemoID: &memoMessage.ID,
|
||||
ResourceID: &resourceID,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if patchMemoRequest.RelationList != nil {
|
||||
patchMemoRelationList := make([]*store.MemoRelationMessage, 0)
|
||||
for _, memoRelation := range patchMemoRequest.RelationList {
|
||||
patchMemoRelationList = append(patchMemoRelationList, &store.MemoRelationMessage{
|
||||
MemoID: memoMessage.ID,
|
||||
RelatedMemoID: memoRelation.RelatedMemoID,
|
||||
Type: store.MemoRelationType(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.DeleteMemoRelationMessage{
|
||||
MemoID: &memoMessage.ID,
|
||||
RelatedMemoID: &memoRelation.RelatedMemoID,
|
||||
Type: &memoRelation.Type,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memoMessage, err = s.Store.GetMemo(ctx, &store.FindMemoMessage{ID: &memoID})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.GET("/memo", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
findMemoMessage := &store.FindMemoMessage{}
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
findMemoMessage.CreatorID = &userID
|
||||
}
|
||||
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
if findMemoMessage.CreatorID == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
findMemoMessage.VisibilityList = []store.Visibility{store.Public}
|
||||
} else {
|
||||
if findMemoMessage.CreatorID == nil {
|
||||
findMemoMessage.CreatorID = ¤tUserID
|
||||
} else {
|
||||
findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
|
||||
}
|
||||
}
|
||||
|
||||
rowStatus := store.RowStatus(c.QueryParam("rowStatus"))
|
||||
if rowStatus != "" {
|
||||
findMemoMessage.RowStatus = &rowStatus
|
||||
}
|
||||
pinnedStr := c.QueryParam("pinned")
|
||||
if pinnedStr != "" {
|
||||
pinned := pinnedStr == "true"
|
||||
findMemoMessage.Pinned = &pinned
|
||||
}
|
||||
|
||||
contentSearch := []string{}
|
||||
tag := c.QueryParam("tag")
|
||||
if tag != "" {
|
||||
contentSearch = append(contentSearch, "#"+tag)
|
||||
}
|
||||
contentSlice := c.QueryParams()["content"]
|
||||
if len(contentSlice) > 0 {
|
||||
contentSearch = append(contentSearch, contentSlice...)
|
||||
}
|
||||
findMemoMessage.ContentSearch = contentSearch
|
||||
|
||||
visibilityListStr := c.QueryParam("visibility")
|
||||
if visibilityListStr != "" {
|
||||
visibilityList := []store.Visibility{}
|
||||
for _, visibility := range strings.Split(visibilityListStr, ",") {
|
||||
visibilityList = append(visibilityList, store.Visibility(visibility))
|
||||
}
|
||||
findMemoMessage.VisibilityList = visibilityList
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
findMemoMessage.Limit = &limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
|
||||
findMemoMessage.Offset = &offset
|
||||
}
|
||||
|
||||
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
|
||||
}
|
||||
if memoDisplayWithUpdatedTs {
|
||||
findMemoMessage.OrderByUpdatedTs = true
|
||||
}
|
||||
|
||||
memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
|
||||
}
|
||||
memoResponseList := []*api.MemoResponse{}
|
||||
for _, memoMessage := range memoMessageList {
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
memoResponseList = append(memoResponseList, memoResponse)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponseList))
|
||||
})
|
||||
|
||||
g.GET("/memo/:memoId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoID, err := strconv.Atoi(c.Param("memoId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if memoMessage.Visibility == store.Private {
|
||||
if !ok || memoMessage.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
|
||||
}
|
||||
} else if memoMessage.Visibility == store.Protected {
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
|
||||
}
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoID, err := strconv.Atoi(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(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
memoOrganizerUpsert := &api.MemoOrganizerUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
|
||||
}
|
||||
memoOrganizerUpsert.MemoID = memoID
|
||||
memoOrganizerUpsert.UserID = userID
|
||||
|
||||
err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
|
||||
}
|
||||
|
||||
memoMessage, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponse))
|
||||
})
|
||||
|
||||
g.GET("/memo/stats", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
normalStatus := store.Normal
|
||||
findMemoMessage := &store.FindMemoMessage{
|
||||
RowStatus: &normalStatus,
|
||||
}
|
||||
if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
findMemoMessage.CreatorID = &creatorID
|
||||
}
|
||||
if findMemoMessage.CreatorID == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
|
||||
}
|
||||
if memoDisplayWithUpdatedTs {
|
||||
findMemoMessage.OrderByUpdatedTs = true
|
||||
}
|
||||
|
||||
memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
|
||||
}
|
||||
memoResponseList := []*api.MemoResponse{}
|
||||
for _, memoMessage := range memoMessageList {
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
memoResponseList = append(memoResponseList, memoResponse)
|
||||
}
|
||||
|
||||
displayTsList := []int64{}
|
||||
for _, memo := range memoResponseList {
|
||||
displayTsList = append(displayTsList, memo.DisplayTs)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(displayTsList))
|
||||
})
|
||||
|
||||
g.GET("/memo/all", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
findMemoMessage := &store.FindMemoMessage{}
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
findMemoMessage.VisibilityList = []store.Visibility{store.Public}
|
||||
} else {
|
||||
findMemoMessage.VisibilityList = []store.Visibility{store.Public, store.Protected}
|
||||
}
|
||||
|
||||
pinnedStr := c.QueryParam("pinned")
|
||||
if pinnedStr != "" {
|
||||
pinned := pinnedStr == "true"
|
||||
findMemoMessage.Pinned = &pinned
|
||||
}
|
||||
|
||||
contentSearch := []string{}
|
||||
tag := c.QueryParam("tag")
|
||||
if tag != "" {
|
||||
contentSearch = append(contentSearch, "#"+tag+" ")
|
||||
}
|
||||
contentSlice := c.QueryParams()["content"]
|
||||
if len(contentSlice) > 0 {
|
||||
contentSearch = append(contentSearch, contentSlice...)
|
||||
}
|
||||
findMemoMessage.ContentSearch = contentSearch
|
||||
|
||||
visibilityListStr := c.QueryParam("visibility")
|
||||
if visibilityListStr != "" {
|
||||
visibilityList := []store.Visibility{}
|
||||
for _, visibility := range strings.Split(visibilityListStr, ",") {
|
||||
visibilityList = append(visibilityList, store.Visibility(visibility))
|
||||
}
|
||||
findMemoMessage.VisibilityList = visibilityList
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
findMemoMessage.Limit = &limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
|
||||
findMemoMessage.Offset = &offset
|
||||
}
|
||||
|
||||
// Only fetch normal status memos.
|
||||
normalStatus := store.Normal
|
||||
findMemoMessage.RowStatus = &normalStatus
|
||||
|
||||
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
|
||||
}
|
||||
if memoDisplayWithUpdatedTs {
|
||||
findMemoMessage.OrderByUpdatedTs = true
|
||||
}
|
||||
|
||||
memoMessageList, err := s.Store.ListMemos(ctx, findMemoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
|
||||
}
|
||||
memoResponseList := []*api.MemoResponse{}
|
||||
for _, memoMessage := range memoMessageList {
|
||||
memoResponse, err := s.composeMemoMessageToMemoResponse(ctx, memoMessage)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
|
||||
}
|
||||
memoResponseList = append(memoResponseList, memoResponse)
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(memoResponseList))
|
||||
})
|
||||
|
||||
g.DELETE("/memo/:memoId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memoID, err := strconv.Atoi(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.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
if err := s.Store.DeleteMemo(ctx, &store.DeleteMemoMessage{
|
||||
ID: memoID,
|
||||
}); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createMemoCreateActivity(ctx context.Context, memo *store.MemoMessage) error {
|
||||
payload := apiv1.ActivityMemoCreatePayload{
|
||||
Content: memo.Content,
|
||||
Visibility: memo.Visibility.String(),
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &store.ActivityMessage{
|
||||
CreatorID: memo.CreatorID,
|
||||
Type: apiv1.ActivityMemoCreate.String(),
|
||||
Level: apiv1.ActivityInfo.String(),
|
||||
Payload: string(payloadBytes),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getIDListDiff(oldList, newList []int) (addedList, removedList []int) {
|
||||
oldMap := map[int]bool{}
|
||||
for _, id := range oldList {
|
||||
oldMap[id] = true
|
||||
}
|
||||
newMap := map[int]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
|
||||
}
|
||||
|
||||
func getMemoRelationListDiff(oldList, newList []*store.MemoRelationMessage) (addedList, removedList []*store.MemoRelationMessage) {
|
||||
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, relation)
|
||||
}
|
||||
}
|
||||
for _, relation := range newList {
|
||||
key := fmt.Sprintf("%d-%s", relation.RelatedMemoID, relation.Type)
|
||||
if !oldMap[key] {
|
||||
addedList = append(addedList, relation)
|
||||
}
|
||||
}
|
||||
return addedList, removedList
|
||||
}
|
||||
|
||||
func convertCreateMemoRequestToMemoMessage(memoCreate *api.CreateMemoRequest) *store.MemoMessage {
|
||||
createdTs := time.Now().Unix()
|
||||
if memoCreate.CreatedTs != nil {
|
||||
createdTs = *memoCreate.CreatedTs
|
||||
}
|
||||
return &store.MemoMessage{
|
||||
CreatorID: memoCreate.CreatorID,
|
||||
CreatedTs: createdTs,
|
||||
Content: memoCreate.Content,
|
||||
Visibility: store.Visibility(memoCreate.Visibility),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) composeMemoMessageToMemoResponse(ctx context.Context, memoMessage *store.MemoMessage) (*api.MemoResponse, error) {
|
||||
memoResponse := &api.MemoResponse{
|
||||
ID: memoMessage.ID,
|
||||
RowStatus: api.RowStatus(memoMessage.RowStatus.String()),
|
||||
CreatorID: memoMessage.CreatorID,
|
||||
CreatedTs: memoMessage.CreatedTs,
|
||||
UpdatedTs: memoMessage.UpdatedTs,
|
||||
Content: memoMessage.Content,
|
||||
Visibility: api.Visibility(memoMessage.Visibility.String()),
|
||||
Pinned: memoMessage.Pinned,
|
||||
}
|
||||
|
||||
// Compose creator name.
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &memoResponse.CreatorID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Nickname != "" {
|
||||
memoResponse.CreatorName = user.Nickname
|
||||
} else {
|
||||
memoResponse.CreatorName = user.Username
|
||||
}
|
||||
|
||||
// Compose display ts.
|
||||
memoResponse.DisplayTs = memoResponse.CreatedTs
|
||||
// Find memo display with updated ts setting.
|
||||
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if memoDisplayWithUpdatedTs {
|
||||
memoResponse.DisplayTs = memoResponse.UpdatedTs
|
||||
}
|
||||
|
||||
relationList := []*api.MemoRelation{}
|
||||
for _, relation := range memoMessage.RelationList {
|
||||
relationList = append(relationList, convertMemoRelationMessageToMemoRelation(relation))
|
||||
}
|
||||
memoResponse.RelationList = relationList
|
||||
|
||||
resourceList := []*api.Resource{}
|
||||
for _, resourceID := range memoMessage.ResourceIDList {
|
||||
resource, err := s.Store.GetResource(ctx, &store.FindResource{
|
||||
ID: &resourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resource != nil {
|
||||
resourceList = append(resourceList, convertResourceFromStore(resource))
|
||||
}
|
||||
}
|
||||
memoResponse.ResourceList = resourceList
|
||||
|
||||
return memoResponse, nil
|
||||
}
|
||||
|
||||
func (s *Server) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
|
||||
memoDisplayWithUpdatedTsSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
|
||||
Name: apiv1.SystemSettingMemoDisplayWithUpdatedTsName.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to find system setting")
|
||||
}
|
||||
memoDisplayWithUpdatedTs := false
|
||||
if memoDisplayWithUpdatedTsSetting != nil {
|
||||
err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to unmarshal system setting value")
|
||||
}
|
||||
}
|
||||
return memoDisplayWithUpdatedTs, nil
|
||||
}
|
||||
|
||||
func convertResourceFromStore(resource *store.Resource) *api.Resource {
|
||||
return &api.Resource{
|
||||
ID: resource.ID,
|
||||
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,
|
||||
PublicID: resource.PublicID,
|
||||
LinkedMemoAmount: resource.LinkedMemoAmount,
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (s *Server) registerMemoResourceRoutes(g *echo.Group) {
|
||||
g.POST("/memo/:memoId/resource", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoID, err := strconv.Atoi(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(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memoResourceUpsert := &api.MemoResourceUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
|
||||
}
|
||||
resource, err := s.Store.GetResource(ctx, &store.FindResource{
|
||||
ID: &memoResourceUpsert.ResourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
|
||||
}
|
||||
if resource == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
|
||||
} else if resource.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
|
||||
}
|
||||
|
||||
memoResourceUpsert.MemoID = memoID
|
||||
currentTs := time.Now().Unix()
|
||||
memoResourceUpsert.UpdatedTs = ¤tTs
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
|
||||
g.GET("/memo/:memoId/resource", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoID, err := strconv.Atoi(c.Param("memoId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
list, err := s.Store.ListResources(ctx, &store.FindResource{
|
||||
MemoID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
}
|
||||
resourceList := []*api.Resource{}
|
||||
for _, resource := range list {
|
||||
resourceList = append(resourceList, convertResourceFromStore(resource))
|
||||
}
|
||||
return c.JSON(http.StatusOK, composeResponse(resourceList))
|
||||
})
|
||||
|
||||
g.DELETE("/memo/:memoId/resource/:resourceId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memoID, err := strconv.Atoi(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)
|
||||
}
|
||||
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemoMessage{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
memoResourceDelete := &api.MemoResourceDelete{
|
||||
MemoID: &memoID,
|
||||
ResourceID: &resourceID,
|
||||
}
|
||||
if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue