memo.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. package server
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/usememos/memos/api"
  11. "github.com/usememos/memos/common"
  12. metric "github.com/usememos/memos/plugin/metrics"
  13. "github.com/labstack/echo/v4"
  14. )
  15. func (s *Server) registerMemoRoutes(g *echo.Group) {
  16. g.POST("/memo", func(c echo.Context) error {
  17. ctx := c.Request().Context()
  18. userID, ok := c.Get(getUserIDContextKey()).(int)
  19. if !ok {
  20. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  21. }
  22. memoCreate := &api.MemoCreate{}
  23. if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
  24. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
  25. }
  26. if memoCreate.Content == "" {
  27. return echo.NewHTTPError(http.StatusBadRequest, "Memo content shouldn't be empty")
  28. }
  29. if memoCreate.Visibility == "" {
  30. userSettingMemoVisibilityKey := api.UserSettingMemoVisibilityKey
  31. userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
  32. UserID: userID,
  33. Key: &userSettingMemoVisibilityKey,
  34. })
  35. if err != nil {
  36. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
  37. }
  38. if userMemoVisibilitySetting != nil {
  39. memoVisibility := api.Private
  40. err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
  41. if err != nil {
  42. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
  43. }
  44. memoCreate.Visibility = memoVisibility
  45. } else {
  46. // Private is the default memo visibility.
  47. memoCreate.Visibility = api.Private
  48. }
  49. }
  50. memoCreate.CreatorID = userID
  51. memo, err := s.Store.CreateMemo(ctx, memoCreate)
  52. if err != nil {
  53. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
  54. }
  55. s.Collector.Collect(ctx, &metric.Metric{
  56. Name: "memo created",
  57. })
  58. for _, resourceID := range memoCreate.ResourceIDList {
  59. if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
  60. MemoID: memo.ID,
  61. ResourceID: resourceID,
  62. }); err != nil {
  63. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  64. }
  65. }
  66. memo, err = s.Store.ComposeMemo(ctx, memo)
  67. if err != nil {
  68. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  69. }
  70. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  71. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
  72. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
  73. }
  74. return nil
  75. })
  76. g.PATCH("/memo/:memoId", func(c echo.Context) error {
  77. ctx := c.Request().Context()
  78. userID, ok := c.Get(getUserIDContextKey()).(int)
  79. if !ok {
  80. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  81. }
  82. memoID, err := strconv.Atoi(c.Param("memoId"))
  83. if err != nil {
  84. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  85. }
  86. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  87. ID: &memoID,
  88. })
  89. if err != nil {
  90. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  91. }
  92. if memo.CreatorID != userID {
  93. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  94. }
  95. currentTs := time.Now().Unix()
  96. memoPatch := &api.MemoPatch{
  97. ID: memoID,
  98. UpdatedTs: &currentTs,
  99. }
  100. if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
  101. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
  102. }
  103. memo, err = s.Store.PatchMemo(ctx, memoPatch)
  104. if err != nil {
  105. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
  106. }
  107. for _, resourceID := range memoPatch.ResourceIDList {
  108. if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
  109. MemoID: memo.ID,
  110. ResourceID: resourceID,
  111. }); err != nil {
  112. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  113. }
  114. }
  115. memo, err = s.Store.ComposeMemo(ctx, memo)
  116. if err != nil {
  117. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
  118. }
  119. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  120. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
  121. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
  122. }
  123. return nil
  124. })
  125. g.GET("/memo", func(c echo.Context) error {
  126. ctx := c.Request().Context()
  127. memoFind := &api.MemoFind{}
  128. if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
  129. memoFind.CreatorID = &userID
  130. }
  131. currentUserID, ok := c.Get(getUserIDContextKey()).(int)
  132. if !ok {
  133. if memoFind.CreatorID == nil {
  134. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  135. }
  136. memoFind.VisibilityList = []api.Visibility{api.Public}
  137. } else {
  138. if memoFind.CreatorID == nil {
  139. memoFind.CreatorID = &currentUserID
  140. } else {
  141. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
  142. }
  143. }
  144. rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
  145. if rowStatus != "" {
  146. memoFind.RowStatus = &rowStatus
  147. }
  148. pinnedStr := c.QueryParam("pinned")
  149. if pinnedStr != "" {
  150. pinned := pinnedStr == "true"
  151. memoFind.Pinned = &pinned
  152. }
  153. tag := c.QueryParam("tag")
  154. if tag != "" {
  155. contentSearch := "#" + tag
  156. memoFind.ContentSearch = &contentSearch
  157. }
  158. visibilityListStr := c.QueryParam("visibility")
  159. if visibilityListStr != "" {
  160. visibilityList := []api.Visibility{}
  161. for _, visibility := range strings.Split(visibilityListStr, ",") {
  162. visibilityList = append(visibilityList, api.Visibility(visibility))
  163. }
  164. memoFind.VisibilityList = visibilityList
  165. }
  166. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  167. memoFind.Limit = limit
  168. }
  169. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  170. memoFind.Offset = offset
  171. }
  172. list, err := s.Store.FindMemoList(ctx, memoFind)
  173. if err != nil {
  174. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
  175. }
  176. var pinnedMemoList []*api.Memo
  177. var unpinnedMemoList []*api.Memo
  178. for _, memo := range list {
  179. if memo.Pinned {
  180. pinnedMemoList = append(pinnedMemoList, memo)
  181. } else {
  182. unpinnedMemoList = append(unpinnedMemoList, memo)
  183. }
  184. }
  185. sort.Slice(pinnedMemoList, func(i, j int) bool {
  186. return pinnedMemoList[i].DisplayTs > pinnedMemoList[j].DisplayTs
  187. })
  188. sort.Slice(unpinnedMemoList, func(i, j int) bool {
  189. return unpinnedMemoList[i].DisplayTs > unpinnedMemoList[j].DisplayTs
  190. })
  191. memoList := []*api.Memo{}
  192. memoList = append(memoList, pinnedMemoList...)
  193. memoList = append(memoList, unpinnedMemoList...)
  194. if memoFind.Limit != 0 {
  195. memoList = memoList[memoFind.Offset:common.Min(len(memoList), memoFind.Offset+memoFind.Limit)]
  196. }
  197. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  198. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memoList)); err != nil {
  199. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo list response").SetInternal(err)
  200. }
  201. return nil
  202. })
  203. g.GET("/memo/:memoId", func(c echo.Context) error {
  204. ctx := c.Request().Context()
  205. memoID, err := strconv.Atoi(c.Param("memoId"))
  206. if err != nil {
  207. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  208. }
  209. memoFind := &api.MemoFind{
  210. ID: &memoID,
  211. }
  212. memo, err := s.Store.FindMemo(ctx, memoFind)
  213. if err != nil {
  214. if common.ErrorCode(err) == common.NotFound {
  215. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
  216. }
  217. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  218. }
  219. userID, ok := c.Get(getUserIDContextKey()).(int)
  220. if memo.Visibility == api.Private {
  221. if !ok || memo.CreatorID != userID {
  222. return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
  223. }
  224. } else if memo.Visibility == api.Protected {
  225. if !ok {
  226. return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
  227. }
  228. }
  229. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  230. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
  231. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
  232. }
  233. return nil
  234. })
  235. g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
  236. ctx := c.Request().Context()
  237. memoID, err := strconv.Atoi(c.Param("memoId"))
  238. if err != nil {
  239. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  240. }
  241. userID, ok := c.Get(getUserIDContextKey()).(int)
  242. if !ok {
  243. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  244. }
  245. memoOrganizerUpsert := &api.MemoOrganizerUpsert{}
  246. if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
  247. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
  248. }
  249. memoOrganizerUpsert.MemoID = memoID
  250. memoOrganizerUpsert.UserID = userID
  251. err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
  252. if err != nil {
  253. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
  254. }
  255. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  256. ID: &memoID,
  257. })
  258. if err != nil {
  259. if common.ErrorCode(err) == common.NotFound {
  260. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
  261. }
  262. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
  263. }
  264. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  265. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
  266. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
  267. }
  268. return nil
  269. })
  270. g.POST("/memo/:memoId/resource", func(c echo.Context) error {
  271. ctx := c.Request().Context()
  272. memoID, err := strconv.Atoi(c.Param("memoId"))
  273. if err != nil {
  274. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  275. }
  276. userID, ok := c.Get(getUserIDContextKey()).(int)
  277. if !ok {
  278. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  279. }
  280. memoResourceUpsert := &api.MemoResourceUpsert{}
  281. if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
  282. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
  283. }
  284. resourceFind := &api.ResourceFind{
  285. ID: &memoResourceUpsert.ResourceID,
  286. }
  287. resource, err := s.Store.FindResource(ctx, resourceFind)
  288. if err != nil {
  289. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
  290. }
  291. if resource == nil {
  292. return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
  293. } else if resource.CreatorID != userID {
  294. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
  295. }
  296. memoResourceUpsert.MemoID = memoID
  297. currentTs := time.Now().Unix()
  298. memoResourceUpsert.UpdatedTs = &currentTs
  299. if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
  300. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
  301. }
  302. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  303. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
  304. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
  305. }
  306. return nil
  307. })
  308. g.GET("/memo/:memoId/resource", func(c echo.Context) error {
  309. ctx := c.Request().Context()
  310. memoID, err := strconv.Atoi(c.Param("memoId"))
  311. if err != nil {
  312. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  313. }
  314. resourceFind := &api.ResourceFind{
  315. MemoID: &memoID,
  316. }
  317. resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
  318. if err != nil {
  319. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
  320. }
  321. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  322. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resourceList)); err != nil {
  323. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
  324. }
  325. return nil
  326. })
  327. g.GET("/memo/amount", func(c echo.Context) error {
  328. ctx := c.Request().Context()
  329. normalRowStatus := api.Normal
  330. memoFind := &api.MemoFind{
  331. RowStatus: &normalRowStatus,
  332. }
  333. if userID, err := strconv.Atoi(c.QueryParam("userId")); err == nil {
  334. memoFind.CreatorID = &userID
  335. }
  336. memoList, err := s.Store.FindMemoList(ctx, memoFind)
  337. if err != nil {
  338. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
  339. }
  340. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  341. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(len(memoList))); err != nil {
  342. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo amount").SetInternal(err)
  343. }
  344. return nil
  345. })
  346. g.GET("/memo/stats", func(c echo.Context) error {
  347. ctx := c.Request().Context()
  348. normalStatus := api.Normal
  349. memoFind := &api.MemoFind{
  350. RowStatus: &normalStatus,
  351. }
  352. if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
  353. memoFind.CreatorID = &creatorID
  354. }
  355. if memoFind.CreatorID == nil {
  356. return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
  357. }
  358. currentUserID, ok := c.Get(getUserIDContextKey()).(int)
  359. if !ok {
  360. memoFind.VisibilityList = []api.Visibility{api.Public}
  361. } else {
  362. if *memoFind.CreatorID != currentUserID {
  363. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
  364. } else {
  365. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Private}
  366. }
  367. }
  368. list, err := s.Store.FindMemoList(ctx, memoFind)
  369. if err != nil {
  370. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
  371. }
  372. displayTsList := []int64{}
  373. for _, memo := range list {
  374. displayTsList = append(displayTsList, memo.DisplayTs)
  375. }
  376. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  377. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(displayTsList)); err != nil {
  378. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo stats response").SetInternal(err)
  379. }
  380. return nil
  381. })
  382. g.GET("/memo/all", func(c echo.Context) error {
  383. ctx := c.Request().Context()
  384. memoFind := &api.MemoFind{}
  385. _, ok := c.Get(getUserIDContextKey()).(int)
  386. if !ok {
  387. memoFind.VisibilityList = []api.Visibility{api.Public}
  388. } else {
  389. memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
  390. }
  391. pinnedStr := c.QueryParam("pinned")
  392. if pinnedStr != "" {
  393. pinned := pinnedStr == "true"
  394. memoFind.Pinned = &pinned
  395. }
  396. tag := c.QueryParam("tag")
  397. if tag != "" {
  398. contentSearch := "#" + tag + " "
  399. memoFind.ContentSearch = &contentSearch
  400. }
  401. visibilityListStr := c.QueryParam("visibility")
  402. if visibilityListStr != "" {
  403. visibilityList := []api.Visibility{}
  404. for _, visibility := range strings.Split(visibilityListStr, ",") {
  405. visibilityList = append(visibilityList, api.Visibility(visibility))
  406. }
  407. memoFind.VisibilityList = visibilityList
  408. }
  409. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  410. memoFind.Limit = limit
  411. }
  412. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  413. memoFind.Offset = offset
  414. }
  415. // Only fetch normal status memos.
  416. normalStatus := api.Normal
  417. memoFind.RowStatus = &normalStatus
  418. list, err := s.Store.FindMemoList(ctx, memoFind)
  419. if err != nil {
  420. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
  421. }
  422. sort.Slice(list, func(i, j int) bool {
  423. return list[i].DisplayTs > list[j].DisplayTs
  424. })
  425. if memoFind.Limit != 0 {
  426. list = list[memoFind.Offset:common.Min(len(list), memoFind.Offset+memoFind.Limit)]
  427. }
  428. c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  429. if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
  430. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
  431. }
  432. return nil
  433. })
  434. g.DELETE("/memo/:memoId", func(c echo.Context) error {
  435. ctx := c.Request().Context()
  436. userID, ok := c.Get(getUserIDContextKey()).(int)
  437. if !ok {
  438. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  439. }
  440. memoID, err := strconv.Atoi(c.Param("memoId"))
  441. if err != nil {
  442. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  443. }
  444. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  445. ID: &memoID,
  446. })
  447. if err != nil {
  448. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  449. }
  450. if memo.CreatorID != userID {
  451. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  452. }
  453. memoDelete := &api.MemoDelete{
  454. ID: memoID,
  455. }
  456. if err := s.Store.DeleteMemo(ctx, memoDelete); err != nil {
  457. if common.ErrorCode(err) == common.NotFound {
  458. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
  459. }
  460. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
  461. }
  462. return c.JSON(http.StatusOK, true)
  463. })
  464. g.DELETE("/memo/:memoId/resource/:resourceId", func(c echo.Context) error {
  465. ctx := c.Request().Context()
  466. userID, ok := c.Get(getUserIDContextKey()).(int)
  467. if !ok {
  468. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  469. }
  470. memoID, err := strconv.Atoi(c.Param("memoId"))
  471. if err != nil {
  472. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
  473. }
  474. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  475. if err != nil {
  476. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  477. }
  478. memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
  479. ID: &memoID,
  480. })
  481. if err != nil {
  482. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
  483. }
  484. if memo.CreatorID != userID {
  485. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  486. }
  487. memoResourceDelete := &api.MemoResourceDelete{
  488. MemoID: &memoID,
  489. ResourceID: &resourceID,
  490. }
  491. if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
  492. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
  493. }
  494. return c.JSON(http.StatusOK, true)
  495. })
  496. }