Compare commits

...

257 Commits

Author SHA1 Message Date
boojack
cc23d5cafe chore: upgrade version to 0.11.0 (#1143)
* chore: upgrade version to `0.11.0`

* chore: update
2023-02-24 08:31:54 +08:00
boojack
9c5b44d070 feat: update storage schema (#1142) 2023-02-24 00:02:51 +08:00
boojack
84fb8b2288 feat: update storage setting section (#1140) 2023-02-23 23:22:34 +08:00
boojack
6d2d322140 chore: show pinned memos in explore (#1141) 2023-02-23 19:59:18 +08:00
boojack
1517688076 chore: update code structure (#1139)
* chore: update code structure

* chore: update
2023-02-23 00:07:16 +08:00
boojack
29124f56bb chore: update memo service (#1138)
* chore: update memo service

* chore: update
2023-02-22 20:07:55 +08:00
boojack
42d849abfc chore: update explore header style (#1137) 2023-02-22 19:21:08 +08:00
Weblate (bot)
d1b307b18f chore: update translations from Hosted Weblate (#1134)
Translated using Weblate (Polish)

Currently translated at 97.1% (204 of 210 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (210 of 210 strings)



Translate-URL: https://hosted.weblate.org/projects/memos/web/pl/
Translate-URL: https://hosted.weblate.org/projects/memos/web/zh_Hant/
Translation: memos/web

Co-authored-by: Piotr Wik <p_00@o2.pl>
Co-authored-by: SiriYang <www.yangxinruei@qq.com>
2023-02-22 00:58:49 +00:00
Weblate (bot)
f6d347c5e4 chore: update translations from Hosted Weblate (#1132)
Translated using Weblate (Chinese (Traditional))

Currently translated at 94.2% (198 of 210 strings)


Translate-URL: https://hosted.weblate.org/projects/memos/web/zh_Hant/
Translation: memos/web

Co-authored-by: SiriYang <www.yangxinruei@qq.com>
2023-02-21 21:39:57 +08:00
Weblate (bot)
4fe8476169 chore: update translations from Hosted Weblate (#1128)
Translated using Weblate (Polish)

Currently translated at 79.5% (167 of 210 strings)

Translated using Weblate (Polish)

Currently translated at 0.0% (0 of 0 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (210 of 210 strings)

Added translation using Weblate (Polish)

Translated using Weblate (Korean)

Currently translated at 98.5% (210 of 213 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.1% (207 of 213 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 92.0% (196 of 213 strings)

Translated using Weblate (Vietnamese)

Currently translated at 89.2% (190 of 213 strings)

Translated using Weblate (Ukrainian)

Currently translated at 91.5% (195 of 213 strings)

Translated using Weblate (Russian)

Currently translated at 91.5% (195 of 213 strings)

Translated using Weblate (Italian)

Currently translated at 91.5% (195 of 213 strings)

Translated using Weblate (French)

Currently translated at 90.1% (192 of 213 strings)

Translated using Weblate (Spanish)

Currently translated at 91.5% (195 of 213 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (210 of 210 strings)

Deleted translation using Weblate (English (United States))

Translated using Weblate (English (United States))

Currently translated at 0.0% (0 of 0 strings)

Added translation using Weblate (English (United States))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (210 of 210 strings)

Translated using Weblate (Korean)

Currently translated at 98.5% (207 of 210 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (205 of 210 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 93.3% (196 of 210 strings)

Translated using Weblate (Vietnamese)

Currently translated at 90.4% (190 of 210 strings)

Translated using Weblate (Ukrainian)

Currently translated at 92.8% (195 of 210 strings)

Translated using Weblate (Swedish)

Currently translated at 92.8% (195 of 210 strings)

Translated using Weblate (Russian)

Currently translated at 92.8% (195 of 210 strings)

Translated using Weblate (Dutch)

Currently translated at 85.7% (180 of 210 strings)

Translated using Weblate (Italian)

Currently translated at 92.8% (195 of 210 strings)

Translated using Weblate (French)

Currently translated at 91.4% (192 of 210 strings)

Translated using Weblate (Spanish)

Currently translated at 92.8% (195 of 210 strings)

Translated using Weblate (German)

Currently translated at 91.4% (192 of 210 strings)








Translate-URL: https://hosted.weblate.org/projects/memos/web/de/
Translate-URL: https://hosted.weblate.org/projects/memos/web/en_US/
Translate-URL: https://hosted.weblate.org/projects/memos/web/es/
Translate-URL: https://hosted.weblate.org/projects/memos/web/fr/
Translate-URL: https://hosted.weblate.org/projects/memos/web/it/
Translate-URL: https://hosted.weblate.org/projects/memos/web/ko/
Translate-URL: https://hosted.weblate.org/projects/memos/web/nl/
Translate-URL: https://hosted.weblate.org/projects/memos/web/pl/
Translate-URL: https://hosted.weblate.org/projects/memos/web/ru/
Translate-URL: https://hosted.weblate.org/projects/memos/web/sv/
Translate-URL: https://hosted.weblate.org/projects/memos/web/uk/
Translate-URL: https://hosted.weblate.org/projects/memos/web/vi/
Translate-URL: https://hosted.weblate.org/projects/memos/web/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/memos/web/zh_Hant/
Translation: memos/web

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Piotr Wik <p_00@o2.pl>
Co-authored-by: Yoshino-s <cy-cui@outlook.com>
Co-authored-by: boojack <stevenlgtm@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2023-02-21 07:59:02 +08:00
Yoshino-s
bbc5ac9f0e feat: make file uplaod support drag/drop (#1129) 2023-02-20 22:17:01 +08:00
_Jellen
29b5c393d1 feat: add Korean translation (#1127)
added Korean translation
2023-02-20 00:54:12 +00:00
boojack
b8cc0b1270 chore: update readme with weblate badge (#1126) 2023-02-19 21:55:29 +08:00
boojack
b145d8b8a2 chore: update setting dialog style (#1125) 2023-02-19 21:12:16 +08:00
boojack
ffe1073292 fix: schema path for demo mode (#1124) 2023-02-19 16:34:15 +08:00
Yoshino-s
afaaec8492 feat(mode): add demo mode (#1121)
* feat(mode): add demo mode

* chroe: Update store/db/db.go

Co-authored-by: boojack <stevenlgtm@gmail.com>

* chroe: Update store/db/db.go

Co-authored-by: boojack <stevenlgtm@gmail.com>

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-02-19 13:36:45 +08:00
boojack
d0b8b076cf feat: implement sign in with SSO (#1119)
* feat: implement sign in with SSO

* chore: update

* chore: update

* chore: update
2023-02-19 09:50:30 +08:00
boojack
708049bb89 feat: add SSO related UI (#1118)
* feat: add SSO related UI

* chore: update
2023-02-18 22:57:45 +08:00
Stephen Zhou
65aa51d525 feat: generate RSS item title and limit item count (#1117)
* feat: generate RSS item title and limit item count

* Update server/rss.go

Co-authored-by: boojack <stevenlgtm@gmail.com>

* Update server/rss.go

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-02-18 22:20:28 +08:00
boojack
cbbd284e7a feat: add store cache for idp (#1116)
feat: add cache for idp
2023-02-18 18:41:52 +08:00
boojack
852903bdbd fix: idp config definition (#1115)
fix: idp definition
2023-02-18 18:31:03 +08:00
Zeng1998
19efacef9c chore: add desc for storage form (#1112) 2023-02-18 18:08:35 +08:00
boojack
0f57629d25 feat: implement idp server (#1111)
* feat: implement idp server

* chore: update
2023-02-18 11:29:12 +08:00
boojack
69726c3925 feat: implement oauth2 plugin (#1110) 2023-02-18 10:50:13 +08:00
boojack
37f9c7c8d6 chore: update avatar max size (#1109) 2023-02-18 10:48:31 +08:00
boojack
bcee0bbf3a feat: add avatar to user in frontend (#1108) 2023-02-18 10:00:46 +08:00
boojack
096a71c58b feat: add avatar_url field to user table (#1106)
refactor: add `avatar_url` field to user table
2023-02-17 23:55:56 +08:00
boojack
a538b9789b feat: introduce idp table (#1105)
* feat: introduce idp table

* chore: update
2023-02-17 13:06:41 +00:00
boojack
c6e525b06f chore: remove unused fields of storage table (#1104) 2023-02-17 20:12:08 +08:00
boojack
d29c40dc71 chore: update router loader (#1102) 2023-02-17 08:26:40 +08:00
Jake
4f5f541efe docs: add raycast extension (#1100) 2023-02-16 23:38:30 +08:00
boojack
caf054bae7 chore: add beta badge to storage (#1099)
* chore: add beta badge to storage

* chore: update
2023-02-16 21:21:39 +08:00
boojack
7e8011ba34 chore: support deleting storage (#1095) 2023-02-15 22:54:46 +08:00
Ruihang Xia
e46f77681d chore(build): anchor setup-buildx-action@v2 to version v0.9.1 (#1089)
Signed-off-by: Ruihang Xia <waynestxia@gmail.com>
2023-02-15 22:16:45 +08:00
Ryan McPherson
0de0a5aa87 chore: make README more comprehensive (#1065)
* Update README.md

* fix typos in readme

* Update README.md

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-02-15 20:52:17 +08:00
boojack
3394380ffa chore: update storage components (#1091) 2023-02-14 22:45:22 +08:00
Zeng1998
2493bb0fb7 feat: storage service frontend (#1088) 2023-02-14 09:56:04 +08:00
Christopher
4641e89c17 feat(system): support for disabling public memos (#1003)
* feat(system): support for disabling public memos

* fix(web/editor): set visibility to private on disabled public memos

* feat(server/memo): find/check if public memos are disabled

* fix(server/memo): handle error for finding system error

* fix(server/memo): unmarshal visiblity when getting system settings

* chore(web): move side effect imports to end

* Update memo.go

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-02-13 16:07:31 +00:00
Xi
28405f6d24 feat: not found page (#1081)
* feat: style for not found page (#1078)

* chore: translation for not found page (#1078)

* feat: add not found page (#1078)

* chore: router for not found page (#1078)

* fix: typo
2023-02-13 23:28:46 +08:00
boojack
5455cb3164 chore: simplify editor (#1087) 2023-02-13 23:27:45 +08:00
Zeng1998
1e4a81dea9 feat: storage service backend (#1086)
* feat: storage service backend

* update go.mod

* update the column name (urlPrefix -> url_prefix)

* update

* update
2023-02-13 19:36:48 +08:00
Jake
cbc3373e8e docs: add shortcut for ios (#1083) 2023-02-13 12:41:15 +08:00
boojack
870559046f chore: update skipper name (#1080) 2023-02-12 17:29:23 +08:00
boojack
a997e1d10d chore: simplify memo editor component (#1079) 2023-02-12 16:34:42 +08:00
Jake
c28d35d8f7 docs: memos-import-from-flomo support wechat reading (#1077) 2023-02-12 07:32:03 +08:00
boojack
b92da8f123 fix: check localsetting exists (#1076) 2023-02-11 22:54:13 +08:00
boojack
bdf0c44246 chore: add CreatedTs field to MemoCreate (#1073) 2023-02-11 21:32:42 +08:00
Jake
799fb058b4 docs: add memos-import-from-flomo (#1072) 2023-02-11 21:31:38 +08:00
Zeng1998
11924ad4c5 feat: add storage service table (#1070)
* feat: add storage service table

* update json field name

* update table name

* add updated_ts
2023-02-11 20:31:39 +08:00
boojack
b11d2130a0 chore: validate external link (#1069) 2023-02-11 17:34:29 +08:00
boojack
e0f4cb06b3 chore: update tags order (#1068) 2023-02-11 16:05:52 +08:00
boojack
aad97c4c54 chore: update signup api (#1067) 2023-02-11 15:15:56 +08:00
boojack
3590d3f8b6 feat: update store cache (#1066)
* feat: update store cache

* chore: update
2023-02-11 14:19:26 +08:00
Shruti Chaturvedi
6e5be6ba75 chore: add text for Uffizzi Previews in README (#1061) 2023-02-11 08:35:53 +08:00
boojack
b366ce7594 fix: delete tag (#1062) 2023-02-10 23:57:02 +08:00
Zeng1998
1eacf5367d chore: upgrade version to 0.10.3 (#1060) 2023-02-10 12:03:18 +08:00
boojack
f74d1b7bf8 chore: remove resource cache (#1059) 2023-02-10 08:43:39 +08:00
boojack
a004dcf320 fix: pass empty condition in rss (#1058)
fix: handle empty condition in rss
2023-02-10 08:28:14 +08:00
boojack
5df59a48b7 chore: update rss icon style (#1056) 2023-02-09 23:45:48 +08:00
boojack
989208eb45 chore: update resource select dialog (#999)
* chore: update resource select dialog

* chore: update
2023-02-09 23:24:51 +08:00
boojack
bec1558488 fix: patch resource id (#1055) 2023-02-09 23:20:36 +08:00
Stephen Zhou
6ff79c5d5c fix: can not input chinese (#1053) 2023-02-09 21:50:51 +08:00
Stephen Zhou
168c4f6950 feat: more rss info (#1052)
* feat: more rss info

* fix: ci
2023-02-09 21:17:15 +08:00
boojack
3e40b9df66 chore: update readme with dark mode demo (#1049) 2023-02-08 20:21:33 +08:00
Stephen Zhou
94f97208e3 chore: setup project workspace for better DX (#1048)
* chore: setup project workspace for better DX

* chore: remove prettier ext
2023-02-08 18:43:13 +08:00
boojack
bd9003c24b chore: update readme (#1047) 2023-02-08 08:51:36 +08:00
Nitin Khanna
26700a1ff0 fix: DatePicker should say Wed instead of Web (#1046)
DatePicker should say Wed instead of Web
2023-02-08 08:37:02 +08:00
Stephen Zhou
8b92021b1a fix: editor cursor not in view after smart editing (#1043) 2023-02-07 23:31:43 +08:00
Zeng1998
7cd474dbb7 feat: add setting for double-click of memos (#1036)
* feat: add setting for double-click of memos

* update

* update
2023-02-07 20:35:41 +08:00
boojack
9bf869767d chore: update seed data (#1042) 2023-02-07 20:35:32 +08:00
Zeng1998
9e818cddce feat: tag filter in explore (#1032)
* temp

* Revert "temp"

This reverts commit d2d14b4c57.

* Revert "Revert "temp""

This reverts commit c50be22cb4.

* feat: tag filter in explore page

* update
2023-02-07 20:11:22 +08:00
Stephen Zhou
d6fe180ca1 fix: parse chrome or edge urls in plain link (#1034)
fix: parse chrome or urls in plain link
2023-02-07 20:10:13 +08:00
Stephen Zhou
99cac7cac0 fix: scroll when clicking expand button (#1035) 2023-02-07 20:09:30 +08:00
boojack
81f2166912 chore: update code owners (#1041) 2023-02-07 20:08:51 +08:00
boojack
4de65ab55d fix: url encode for tag name (#1031) 2023-02-06 20:28:19 +08:00
Zeng1998
771ef44d82 feat: support enter to signin (#1014) 2023-02-06 20:03:33 +08:00
-Shiken-
76c42c6c9f chore: more translation correction to traditional Chinese (#1028)
* more translation correction to traditional Chinese

To be in line with the language habits of traditional Chinese users

* Update zh-Hant.json
2023-02-06 19:59:33 +08:00
Shruti Chaturvedi
003887d4e0 Use official Uffizzi reusable action (#1027)
* Use official Uffizzi reusable action

* Run preview if build passed successfully
2023-02-06 19:59:05 +08:00
-Shiken-
89743bd1e6 chore: update zh-Hant.json (#1023) 2023-02-05 17:45:50 +08:00
boojack
1ace332152 feat: graceful shutdown server (#1016) 2023-02-03 10:30:18 +08:00
Zeng1998
2d14047c73 fix: pdf resource preview (#1008) 2023-02-02 20:34:24 +08:00
Stephen Zhou
42cd93cf33 fix: show copy button on hover (#1002) 2023-01-31 18:38:58 +08:00
boojack
4a7b764ab3 chore: remove unused flags for sqlite (#997) 2023-01-30 00:03:21 +08:00
Shruti Chaturvedi
1bdb0d465c chore: update Uffizzi GHA for better error-handling (#996)
Update Uffizzi GHA for better error-handling
2023-01-29 18:16:44 +08:00
WY-WY-W
930b54fabd feat: update Traditional Chinese translation (#994) 2023-01-29 09:41:56 +08:00
boojack
5b0a54bfb7 chore: clean package.json (#993)
* chore: clean `package.json`

* chore: update
2023-01-26 00:35:50 +08:00
boojack
6c3ff6de63 chore: get resource blob optional (#991) 2023-01-25 16:11:02 +08:00
boojack
dd5a23e36e feat: support creating resource with external link (#988) 2023-01-22 21:16:28 +08:00
boojack
848ecd99ee chore: format SQL (#987)
chore: format sql
2023-01-22 21:16:03 +08:00
boojack
82f61f2a0e chore: upgrade version to 0.10.2 (#983) 2023-01-21 08:56:26 +08:00
boojack
c5368fe8d3 chore: update resource dialog style (#982) 2023-01-21 08:46:49 +08:00
Wujiao233
0aaf153717 fix: video and audio can't play on safari (#980)
* fix: video can't play on safari

* fix: audio can't play on safari
2023-01-20 16:52:38 +08:00
Stephen Zhou
942e1f887b feat: scrool to memo after editing (#907) 2023-01-19 20:57:45 +08:00
Wujiao233
b8ab43aa25 feat: support swipe to switch img on touchscreen (#970)
* feat: support swipe to switch img on touchscreen

* fix: fix two or more fingers touch

* fix lint
2023-01-19 20:57:03 +08:00
Wujiao233
a5f3b051f2 fix: round corner issue in resource blocks (#979) 2023-01-19 17:59:37 +08:00
boojack
4ba9767b94 fix: use input instead of textfield (#973) 2023-01-19 09:16:22 +08:00
Zeng1998
12fda38520 feat: add customized logo in share dialog (#969)
* feat: add qrcode in share dialog

* update: change the color

* feat: add customized logo in share dialog

* update: import order

Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-01-18 10:52:25 +00:00
Zeng1998
9ed503fd6d feat: add qrcode in share dialog (#964)
* feat: add qrcode in share dialog

* update: change the color

* update: import order
2023-01-18 18:49:48 +08:00
Viet-Anh, Nguyen
a8976de634 feat: update Vietnamese translation (#965)
refactor: Update Vietnamese translation
2023-01-18 09:01:53 +08:00
Zeng1998
f8855ddb56 feat: support empty content memo (#963)
feat: support empty-text memo
2023-01-17 20:56:57 +08:00
Jasper Platenburg
288ecc617d feat: issue translator workflow (#956)
Create issue-translator.yml
2023-01-15 21:33:19 +08:00
Ángel Fernández Sánchez
14ec81b65c feat: update Spanish translation (#954) 2023-01-15 08:35:20 +08:00
boojack
fae0b64a08 fix: delete tag api (#950)
* fix: delete tag api

* chore: update
2023-01-14 12:08:31 +08:00
boojack
677750ef51 chore: upgrade version to 0.10.1 (#949) 2023-01-14 08:00:07 +08:00
boojack
4cfd000b92 feat: support audio player (#948) 2023-01-14 07:41:17 +08:00
boojack
aacaf3f37c chore: remove sponsor with open collective (#947)
chore: update github sponsors
2023-01-14 06:56:17 +08:00
boojack
ad4a79a510 chore: update GitHub sponsor (#946)
chore: update github sponsor
2023-01-14 06:51:21 +08:00
boojack
219d2754a0 chore: remove existed tags in suggestion (#944) 2023-01-13 22:37:30 +08:00
boojack
10430a66c3 chore: debounce search text input (#943)
* chore: debounce search text input

* chore: update
2023-01-13 22:33:52 +08:00
Jasper Platenburg
c167c21e4e chore: added translation for copy memo link (#942) 2023-01-13 21:22:20 +08:00
boojack
40d25f7dca fix: skip api error for static middleware (#941) 2023-01-13 07:06:15 +08:00
boojack
1441a1df1f chore: update funding with open collective (#940) 2023-01-12 23:48:51 +08:00
boojack
b19c3c6db3 feat: update renderer in list (#935) 2023-01-12 08:52:57 +08:00
boojack
8c146aed68 feat: update memo resources style (#933)
* feat: update memo resources style

* chore: update
2023-01-12 00:00:44 +08:00
boojack
805122f45c chore: add User stories section to readme (#932) 2023-01-11 09:21:52 +08:00
sfan5
7d5de1a07e feat: update German translation (#926) 2023-01-08 23:41:48 +08:00
boojack
4b860777cf fix: tag generate in code block (#925) 2023-01-08 13:49:26 +08:00
boojack
e29924c8a1 fix: codeblock renderer (#924) 2023-01-08 11:24:28 +08:00
boojack
1847756ade chore: remove escape (#918) 2023-01-07 14:52:47 +08:00
boojack
771c56f485 chore: fix renderer (#917) 2023-01-07 14:07:17 +08:00
boojack
0f057e81e9 fix: version compare (#916)
* fix: version compare

* chore: update
2023-01-07 13:58:42 +08:00
Stephen Zhou
529c9b34a7 fix: missing creator id in shortcut cache (#915)
fix: missing creatot id in shortcut cache
2023-01-07 12:45:55 +08:00
boojack
e2e8130f4c fix: sort version (#914) 2023-01-07 11:49:58 +08:00
boojack
46c13a4b7f chore: add skipper for secure (#913) 2023-01-07 10:51:34 +08:00
boojack
96798e10b4 feat: support embed memo with iframe (#912) 2023-01-07 01:56:02 +08:00
boojack
0f8ce3dd16 refactor: return jsx element instead of string in marked (#910)
* refactor: return jsx element instead of string in marked

* chore: update
2023-01-07 00:13:49 +08:00
boojack
491859bbf6 chore: update activity metrics (#908) 2023-01-05 20:56:50 +08:00
boojack
f16123a624 chore: update memo create activity (#903) 2023-01-03 23:49:11 +08:00
boojack
d50ad9433f feat: persistent session name (#902)
* feat: persistent session name

* chore: update
2023-01-03 23:05:42 +08:00
Zeng1998
92a8a4ac0c feat: support code copy (#901)
* feat: support code copy

* update
2023-01-03 23:05:00 +08:00
helaxious
62f53888ba feat: update docker command description (#893) (#900)
* Add a line on README to help prevent a mistake (#893)

* Update README.md

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-01-03 22:40:53 +08:00
boojack
79180928d4 chore: update server activity (#898) 2023-01-03 20:05:37 +08:00
boojack
e5550828a0 chore: update activity payload (#891) 2023-01-02 23:18:12 +08:00
Vincenzo Cardone
2e95f6824f feat: add Italian Translation (#890) 2023-01-02 09:41:39 +08:00
boojack
5195012217 feat: add activity table (#888)
feat: introduce activity
2023-01-01 23:55:02 +08:00
boojack
a797280e3f chore: update middleware skipper (#887)
* chore: update middleware skipper

* chore: update
2023-01-01 23:26:21 +08:00
boojack
293f88e40c chore: update GitHub action name (#886)
* chore: update github actions name

* chore: update
2023-01-01 22:01:16 +08:00
boojack
861eeb7b0f chore: add skipper in CSRF (#885) 2023-01-01 21:32:17 +08:00
boojack
24b21aa9d7 chore: update version to 0.9.1 (#882) 2022-12-31 15:40:53 +08:00
boojack
51eac649c5 chore: update create tag dialog (#881) 2022-12-31 15:13:25 +08:00
boojack
7670c95360 chore: fix XSS in renderer (#880) 2022-12-31 11:52:57 +08:00
Ivan
65e9fdead1 feat: add russian locale (#879) 2022-12-31 09:02:14 +08:00
Zeng1998
2b2792de73 fix: logic of email validation (#877)
* fix: fix logic of email validation

* update
2022-12-30 13:10:52 +08:00
boojack
c9bb2b785d chore: fix CSRF (#876) 2022-12-30 00:17:19 +08:00
boojack
64e5c343c5 chore: fix XSS in renderer (#875)
chore: fix xss in renderer
2022-12-29 23:27:56 +08:00
boojack
9169b3f2cd chore: update tip text for empty tag list (#872) 2022-12-29 09:14:24 +08:00
boojack
b6f7a85a2a fix: reload page when sign out (#871) 2022-12-28 20:58:59 +08:00
boojack
3556ae4e65 fix: access control (#870) 2022-12-28 20:22:52 +08:00
boojack
f888c62840 chore: update userinfo validator (#868)
* chore: update userinfo validator

* chore: update actions

* chore: update
2022-12-27 21:51:43 +08:00
Taras
c160bed403 fead: add ukrainian locale (#864) 2022-12-26 19:10:47 +08:00
boojack
afc9709484 chore: update dev config (#857) 2022-12-25 10:39:45 +08:00
boojack
05b41804e3 chore: hide host user email (#856) 2022-12-25 10:28:51 +08:00
Zeng1998
2e2657b39d feat: add setting for power editor (#851) 2022-12-24 16:18:13 +08:00
Zeng1998
60ee602639 feat: enable word break (#849)
* feat: enable word break

* Update web/src/less/editor.less

Co-authored-by: boojack <stevenlgtm@gmail.com>

* Update web/src/less/memo-content.less

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-24 14:50:27 +08:00
Zeng1998
cac04e4406 feat: enable word break (#849)
* feat: enable word break

* Update web/src/less/editor.less

Co-authored-by: boojack <stevenlgtm@gmail.com>

* Update web/src/less/memo-content.less

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-24 14:50:10 +08:00
M. Gschwandtner
278b4d21b4 fix: prioritize user css by moving it to the body end (#847)
Co-authored-by: M. Gschwandtner <84477901+OnlyPain-ctrl@users.noreply.github.com>
2022-12-24 09:35:30 +08:00
boojack
27fd1e2880 chore: remove secure flag (#848) 2022-12-24 08:31:37 +08:00
boojack
fae9b3db46 chore: revert add linux/arm/v7 to platforms (#843)
Revert "chore: add `linux/arm/v7` to platforms (#842)"

This reverts commit 49c7f49820.
2022-12-23 22:39:04 +08:00
boojack
49c7f49820 chore: add linux/arm/v7 to platforms (#842) 2022-12-23 22:25:24 +08:00
boojack
ef8981794e chore: restore dockerfile (#841) 2022-12-23 22:12:41 +08:00
boojack
e52d77b2c4 chore: restore lockfile (#840) 2022-12-23 22:08:54 +08:00
boojack
1d2953b1b1 chore: downgrade joy-ui version (#839) 2022-12-23 21:53:13 +08:00
boojack
d702eaa625 chore: update dockerfile (#838) 2022-12-23 20:48:51 +08:00
boojack
50811c3064 chore: update yarn.lock (#837) 2022-12-23 20:33:53 +08:00
boojack
99d9cc9168 fix: set csp header only for resource (#836) 2022-12-23 20:02:42 +08:00
boojack
119603da5d chore: upgrade version to 0.9.0 (#835) 2022-12-23 19:49:55 +08:00
boojack
f6039f2eb9 chore: update dialog title (#834) 2022-12-23 19:49:44 +08:00
boojack
65cc19c12e chore: add escape to prevent XSS (#833) 2022-12-23 19:17:33 +08:00
boojack
c07b4a57ca feat: add secure middleware (#832) 2022-12-23 18:58:55 +08:00
boojack
dca35bde87 fix: disable decode patch id (#831) 2022-12-23 18:38:24 +08:00
boojack
9f25badde3 chore: update logo format to png (#830) 2022-12-23 00:21:53 +08:00
boojack
7efa749c66 feat: customize system profile (#828) 2022-12-22 19:48:44 +08:00
boojack
72daa4e1d6 feat: support heading syntax (#827) 2022-12-22 19:48:19 +08:00
ChasLui
54702db9ba feat: prevent page jitter caused by the presence of scroll bars (#808) 2022-12-22 17:46:09 +08:00
Zeng1998
41ad084489 fix: fix css of input placeholder in auth page (#824) 2022-12-22 17:45:12 +08:00
boojack
2fb171e069 chore: update create tag dialog style (#818)
* chore: update create tag dialog

* chore: update
2022-12-22 09:29:22 +08:00
boojack
201c0b020d chore: update seed data for tag (#817)
* chore: update seed data

* chore: add `_journal_mode` for SQLite

* chore: update create tag dialog
2022-12-22 08:34:05 +08:00
boojack
b6f19ca093 feat: upsert tag based content (#816)
* feat: upsert tag based content

* chore: update
2022-12-22 00:35:47 +08:00
boojack
68a77b6e1f feat: create tag dialog (#814) 2022-12-21 23:59:03 +08:00
boojack
e4a8a4d708 feat: tag table (#811)
* feat: tag table

* chore: update

* chore: update
2022-12-21 19:22:32 +08:00
boojack
ab07c91d42 feat: update marked (#810) 2022-12-21 18:36:26 +08:00
ChasLui
1838e616fd feat: show active panel when searchBar is in focus (#806)
* feat: Show active panel when searchBar is in focus

* refactor: rename
2022-12-21 18:36:08 +08:00
M. Gschwandtner
90d0ccc2e8 feat: add arm/v7 to buildx platforms (#802)
added arm/v7 to buildx platforms

Co-authored-by: M. Gschwandtner <84477901+OnlyPain-ctrl@users.noreply.github.com>
2022-12-21 09:08:08 +08:00
ChasLui
358a5c0ed9 feat: press cmd+f to focus on the search bar (#800) 2022-12-21 00:46:22 +08:00
ChasLui
40f39fd66c feat: use shift+tab to unindent (#799)
* feat: Use shift+tab to unindent, just like vscode

* fix: shit+tab return
2022-12-20 23:03:25 +08:00
Jasper Platenburg
3b41976866 feat: implement plurals for stats (#783)
* implement plurals for stats

* renamed variables

* modified according to 18n guide
2022-12-20 21:29:10 +08:00
PublicHer0
a23de50bb8 feat: update spanish locale (#786)
* Adding spanish version

* update spanish locale

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-20 13:18:21 +00:00
Jasper Platenburg
6596e6893e feat: implement translation for days (#784)
implement trranslation for weekdays

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-20 13:10:25 +00:00
ChasLui
b6fe4d914e fix: incorrect cursor when text is selected in range (#797) 2022-12-20 20:51:32 +08:00
ChasLui
3c2cd43d28 fix: shortcuts should exclude the shift judgment (#796)
* fix: Shortcuts should exclude the shift judgment

* fix: eslint
2022-12-20 19:47:35 +08:00
ChasLui
2658b1fd09 feat: support command + k shortcuts insert []() (#793)
* feat: support `command + k` shortcuts insert []()

* fix: eslint

* fix: clear code

* fix: eslint

* feat: insert [](url)

* refactor: rename param

* fix: eslint
2022-12-20 18:51:22 +08:00
Zeng1998
b7df1f5bbf feat: matching punctuation (#791) 2022-12-20 18:10:02 +08:00
ChasLui
a0face6695 feat: update i18n (#790)
* feat: tag type i18n

* feat: custom server dialog i18n

* feat: i18n resources name

* feat: i18n toast

* fix: eslint

* eslint: fix

* fix: eslint

* fix: eslint
2022-12-20 17:47:02 +08:00
boojack
c177db69d5 chore: update tag regexp (#785) 2022-12-20 09:44:41 +08:00
boojack
b704c20809 chore: return raw text for html (#782) 2022-12-19 18:45:17 +08:00
boojack
6c17f94ef6 fix: max open conns for SQLite (#781) 2022-12-19 18:28:15 +08:00
lujiefsi
726285e634 chore: restrict the html file (#749)
* restrict the html file

* replace spaces with table

* remove space
2022-12-19 18:26:50 +08:00
Zeng1998
bd6ab71d41 chore: remove unused state (#780) 2022-12-19 18:03:39 +08:00
boojack
b67ed1ee13 feat: customize system profile (#774)
* feat: system setting for customized profile

* chore: update

* feat: update frontend

* chore: update
2022-12-18 21:18:30 +08:00
Zeng1998
55695f2189 feat: esc key to exit multiple dialogs (#692)
* fix: `esc` key to exit multiple dialogs

* update

* update

* update

* Update web/src/components/Dialog/BaseDialog.tsx

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-18 10:09:12 +00:00
boojack
e79d67d127 chore: update readme with deploy guides (#771) 2022-12-18 17:59:29 +08:00
ajstephens1995
1d9ef9813a docs: guide to deploy memos with render for beginners (#769) 2022-12-18 16:35:09 +08:00
boojack
ef621a444f refactor: introducing use{Module}Store instead of service (#768)
* refactor: introducing `useEditorStore`

* refactor: update

* chore: update
2022-12-18 15:25:18 +08:00
boojack
bd00fa798d chore: simplify ordered list in editor (#767)
chore: simplify editor
2022-12-18 12:44:46 +08:00
Zeng1998
a41745c9ae feat: editor enhancement for order list (#763) 2022-12-18 11:02:42 +08:00
M. Gschwandtner
1eec474007 fix: heatmap popup showing after logging out (#761)
* fix for heatmap popup showing after logging out

* moved node.remove to component unmount

* Update web/src/components/UsageHeatMap.tsx

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: M. Gschwandtner <84477901+OnlyPain-ctrl@users.noreply.github.com>
Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-17 03:10:06 +00:00
Stephen Zhou
83e5278b51 fix: dialog close when draging from in to out (#760) 2022-12-17 10:53:55 +08:00
boojack
a8751af6b5 fix: memo list padding bottom (#759) 2022-12-17 10:19:15 +08:00
boojack
6b24f52cd1 fix: watermark container width (#758) 2022-12-17 10:15:54 +08:00
boojack
7ec22482c1 chore: upgrade version to 0.8.3 (#755) 2022-12-16 23:12:07 +08:00
boojack
ee89dc00c0 chore: update list style (#754) 2022-12-16 23:09:01 +08:00
boojack
bbd5fe4eb2 feat: remove sticky style for memo editor (#752) 2022-12-16 22:41:12 +08:00
boojack
575a0610a3 chore: revert "feat: add visibility field to resource (#743)" (#751)
Revert "feat: add `visibility` field to resource (#743)"

This reverts commit b68cc08592.
2022-12-16 22:20:17 +08:00
boojack
b68cc08592 feat: add visibility field to resource (#743) 2022-12-15 21:15:16 +08:00
Stephen Zhou
d51af7e98a fix: hr in edited content do not trigger folding (#748) 2022-12-15 18:38:35 +08:00
M. Gschwandtner
334da5e903 fix: add a span as wrapper to fix whitespace (#747) 2022-12-15 08:45:20 +08:00
Jasper Platenburg
35fed76d1a feat: add 'theme' to translation (#746) 2022-12-15 08:43:32 +08:00
boojack
c77d49259a chore: update light bg color (#744) 2022-12-13 23:34:38 +08:00
PublicHer0
5520605ccc feat: add spanish locale (#741) 2022-12-13 09:12:22 +08:00
Zeng1998
1dee8ae49f fix: url resource filename decode (#738)
* fix: url resource filename decode

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-12 20:00:21 +08:00
Zeng1998
3fd4ee83ac fix: checklist auto continuation (#737) 2022-12-12 18:30:04 +08:00
boojack
5e978e2cfc chore: update color scheme listener (#735) 2022-12-11 23:18:25 +08:00
boojack
37b7b983d2 chore: add vite plugin legacy (#734) 2022-12-11 22:15:52 +08:00
Zeng1998
c4278ef55a fix: fix order in resource dialog (#733) 2022-12-11 21:36:17 +08:00
Zeng1998
91220ea4a6 fix: reset image state in gallery (#730) 2022-12-11 20:14:55 +08:00
Zeng1998
4bebbf3e1d feat: enable paste multiple resources (#729)
* feat: enable paste multiple resources

* update

* update
2022-12-11 19:17:39 +08:00
boojack
5d8b8c37a5 chore: upgrade vite (#728)
* chore: upgrade vite

* Revert "chore: remove lazy import component (#724)"

This reverts commit 688dc2304c.
2022-12-11 18:38:29 +08:00
boojack
564f20d13a chore: remove ESC to close edit (#726) 2022-12-11 14:18:04 +08:00
boojack
c3adb1b152 fix: set resource list in memo editor (#725) 2022-12-11 14:04:22 +08:00
boojack
688dc2304c chore: remove lazy import component (#724) 2022-12-10 19:39:43 +08:00
boojack
dd6e2337e6 chore: update version to 0.8.2 (#722) 2022-12-10 13:23:38 +08:00
boojack
66418d4210 feat: get image only when cors error (#721) 2022-12-10 13:20:48 +08:00
boojack
ab8c7b9d8a fix: auto complete in memo editor (#720) 2022-12-10 12:44:45 +08:00
M. Gschwandtner
387799b31c fix: added dark theme bg color to buttons (#719) 2022-12-10 12:14:02 +08:00
boojack
4a64a4dea8 fix: html lang attr (#718) 2022-12-10 10:42:10 +08:00
M. Gschwandtner
964c58ac01 feat: responsive layout for create shortcut dialog (#717) 2022-12-10 10:17:47 +08:00
boojack
56716cdad4 fix: break word (#708)
* fix: break word

* chore: update
2022-12-09 08:31:45 +08:00
Shruti Chaturvedi
a2ee750d1e fix: bump Version of reusable.yaml (#707)
Bump version

Bump up the version for reusable.yaml to v2
2022-12-09 00:31:00 +08:00
Shruti Chaturvedi
3f0601f651 feat: add Uffizzi Integration (#655)
* Integrate Uffizzi

* Update docker-compose.uffizzi.yml

Start memos in dev mode

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-08 23:45:23 +08:00
Zeng1998
6f8e3432e9 fix: correct priority of keys in editor (#703) 2022-12-08 18:46:43 +08:00
Stephen Zhou
b7ab6f8e7e fix: code highlight in dark mode (#702) 2022-12-08 18:30:46 +08:00
Zeng1998
36b92ad884 feat: auto continuation list in editor (#689)
* feat: auto continuation list in editor

* update

* update
2022-12-08 10:01:01 +08:00
apixandru
4d9857ce18 fix: update UsageHeatMap.tsx to account for daylight savings (#696) 2022-12-08 08:24:36 +08:00
boojack
43b22ce55f chore: fix typo (#695) 2022-12-07 22:49:05 +08:00
Zeng1998
147185309c feat: vacuum database in setting (#694)
* feat: vacuum database in setting

* update

* update

* update

* update
2022-12-07 22:45:47 +08:00
Maurice Bauer
f48226d4f2 chore: update German translation (#691)
Switched to plural to make the difference between TAG(S) and TAG(E) visible...
Refers to #545
2022-12-07 20:18:35 +08:00
Zeng1998
e92407d9ec feat: image preview enhancement (#682) 2022-12-06 09:38:01 +08:00
Jasper Platenburg
79bf365d78 Dutch locale (#687) 2022-12-06 08:11:21 +08:00
Maurice Bauer
492a1370ab feat: add German i18n item (#686) 2022-12-06 07:42:17 +08:00
boojack
e3ddf93c4d chore: update demo image (#672) 2022-12-04 20:43:46 +08:00
boojack
4a9314c476 chore: rename enableFoldMemo (#671)
* chore: rename `enableFoldMemo`

* chore: update
2022-12-04 15:34:03 +08:00
boojack
4767ee3293 feat: support follow system appearance (#670) 2022-12-04 12:23:29 +08:00
boojack
1ea74dfd0d chore: remove table syntax (#669) 2022-12-04 10:52:11 +08:00
Andreas Backström
53cf6ebb79 feat: add swedish/svenska translation (#668)
Add swedish / svenska translation
2022-12-03 21:38:25 +08:00
boojack
d1007950e0 chore: remove emoji picker (#667) 2022-12-03 15:28:37 +08:00
Zeng1998
331226ec68 chore: fix some typos of README (#666) 2022-12-03 15:01:27 +08:00
boojack
a7374cf998 fix: generate sharing memo image (#663) 2022-12-03 09:41:52 +08:00
332 changed files with 15123 additions and 7245 deletions

View File

@@ -1,2 +1 @@
web/node_modules
web/yarn.lock

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
ko_fi: stevenlgtm
github: usememos

41
.github/workflows/backend-tests.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Backend Test
on:
pull_request:
branches:
- main
- "release/*.*.*"
jobs:
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.19
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: -v
skip-cache: true
go-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Run all tests
run: go test -v ./... | tee test.log; exit ${PIPESTATUS[0]}
- name: Pretty print tests running time
run: grep --color=never -e '--- PASS:' -e '--- FAIL:' test.log | sed 's/[:()]//g' | awk '{print $2,$3,$4}' | sort -t' ' -nk3 -r | awk '{sum += $3; print $1,$2,$3,sum"s"}'

View File

@@ -32,6 +32,7 @@ jobs:
uses: docker/setup-buildx-action@v2
with:
install: true
version: v0.9.1
- name: Build and Push
id: docker_build

View File

@@ -24,6 +24,7 @@ jobs:
uses: docker/setup-buildx-action@v2
with:
install: true
version: v0.9.1
- name: Build and Push
id: docker_build

38
.github/workflows/frontend-tests.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Frontend Test
on:
pull_request:
branches:
- main
- "release/*.*.*"
jobs:
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run eslint check
run: yarn lint
working-directory: web
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run frontend build
run: yarn build
working-directory: web

18
.github/workflows/issue-translator.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: 'issue-translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
# not require, default false, . Decide whether to modify the issue title
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
CUSTOM_BOT_NOTE: Issue is not in English. It has been translated automatically.
# not require. Customize the translation robot prefix message.

View File

@@ -1,88 +0,0 @@
name: Test
on:
push:
branches:
- main
- "release/v*.*.*"
pull_request:
branches: [main]
jobs:
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run eslint check
run: yarn lint
working-directory: web
jest-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run jest
run: yarn test
working-directory: web
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run frontend build
run: yarn build
working-directory: web
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.19
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: -v
skip-cache: true
go-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Run all tests
run: go test -v ./... | tee test.log; exit ${PIPESTATUS[0]}
- name: Pretty print tests running time
run: grep --color=never -e '--- PASS:' -e '--- FAIL:' test.log | sed 's/[:()]//g' | awk '{print $2,$3,$4}' | sort -t' ' -nk3 -r | awk '{sum += $3; print $1,$2,$3,sum"s"}'

97
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: Build PR Image
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
build-memos:
name: Build and push `Memos`
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
if: ${{ github.event.action != 'closed' }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_WORKER }}
tags: |
type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry
uses: docker/build-push-action@v3
with:
context: ./
file: Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
cache-from: type=gha
cache-to: type=gha, mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-memos
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
MEMOS_IMAGE=${{ needs.build-memos.outputs.tags }}
export MEMOS_IMAGE
# Render simple template from environment variables.
envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

88
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ env.HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
if: ${{ github.event.workflow_run.conclusion == 'success' }}
needs:
- cache-compose-file
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write

3
.gitignore vendored
View File

@@ -15,7 +15,4 @@ build
# Jetbrains
.idea
# vscode
.vscode
bin/air

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["golang.go"]
}

12
.vscode/project.code-workspace vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"folders": [
{
"name": "server",
"path": "../"
},
{
"name": "web",
"path": "../web"
}
]
}

View File

@@ -1,4 +1,5 @@
{
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint"
"go.lintTool": "golangci-lint",
"go.inferGopath": false
}

View File

@@ -1,2 +1,2 @@
# These owners will be the default owners for everything in the repo.
* @boojack @lqwakeup
* @boojack

View File

@@ -4,20 +4,18 @@ WORKDIR /frontend-build
COPY ./web/ .
RUN yarn
RUN yarn build
RUN yarn && yarn build
# Build backend exec file.
FROM golang:1.19.3-alpine3.16 AS backend
WORKDIR /backend-build
RUN apk update
RUN apk --no-cache add gcc musl-dev
RUN apk update && apk add --no-cache gcc musl-dev
COPY . .
COPY --from=frontend /frontend-build/dist ./server/dist
RUN go build -o memos ./bin/server/main.go
RUN go build -o memos ./main.go
# Make workspace with above generated files.
FROM alpine:3.16 AS monolithic

View File

@@ -1,27 +1,32 @@
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
<p align="center">An open-source, self-hosted memo hub with knowledge management and socialization.</p>
<p align="center">
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
<img alt="Go report" src="https://goreportcard.com/badge/github.com/usememos/memos" />
<a href="https://hosted.weblate.org/engage/memos/"><img src="https://hosted.weblate.org/widgets/memos/-/svg-badge.svg" alt="Translation status" /></a>
<a href="https://discord.gg/tfPJa4UmAv"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
</p>
<p align="center">
<a href="https://demo.usememos.com/">Live Demo</a> •
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <b><a href="https://discord.gg/tfPJa4UmAv">Discord 🏂</a></b>
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <a href="https://discord.gg/tfPJa4UmAv">Discord</a>
</p>
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.webp)
![demo](./resources/demo.webp#gh-light-mode-only)
![demo-dark](./resources/demo-dark.webp#gh-dark-mode-only)
## Features
- 🦄 Open source and free forever;
- 🚀 Support for self-hosting with `Docker` in seconds;
- 📜 Plain textarea first and support some useful markdown syntax;
- 👥 Set memo private or public to others;
- 🧑‍💻 RESTful API for self-service.
- 🦄 Open source and free forever
- 🚀 Support for self-hosting with `Docker` in seconds
- 📜 Plain textarea first and support some useful Markdown syntax
- 👥 Set memo private or public to others
- 🧑‍💻 RESTful API for self-service
- 📋 Embed memos on other sites using iframe
- #⃣ Hashtags for organizing memos
- 📆 Interactive calendar view
- 💾 Easy data migration and backups
## Deploy with Docker in seconds
@@ -31,32 +36,44 @@
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
```
If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it. Memos will be running at [http://localhost:5230](http://localhost:5230).
> `~/.memos/` will be used as the data directory in your machine and `/var/opt/memos` is the directory of the volume in Docker and should not be modified.
### Docker Compose
Example Compose YAML file: [`docker-compose.yaml`](./docker-compose.yaml).
- Provided docker compose YAML file: [`docker-compose.yaml`](./docker-compose.yaml).
If you want to upgrade the version of memos, use the following command.
- You can upgrade to the latest version memos with:
```sh
docker-compose down && docker image rm neosmemo/memos:latest && docker-compose up -d
```
### Other installation methods
- [Deploy on render.com](./docs/deploy-with-render.md)
- [Deploy on fly.io](https://github.com/hu3rror/memos-on-fly)
## Contribute
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
See more in [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
Learn more about contributing in [development guide](./docs/development.md).
## Products made by Community
### Products made by our Community
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - Wechat miniprogram
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - WeChat MiniProgram
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
- [eallion/memos.top](https://github.com/eallion/memos.top) - A static page rendered with the Memos API
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - A logseq Plugin
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - A Logseq plugin
- [JakeLaoyu/memos-import-from-flomo](https://github.com/JakeLaoyu/memos-import-from-flomo) - Import data. Support from flomo, wechat reading.
- [Send to memos](https://sharecuts.cn/shortcut/12640) - A shortcut for iOS.
- [Memos Raycast Extension](https://www.raycast.com/JakeYu/memos) - Raycast extension, [source code](https://github.com/JakeLaoyu/memos-raycast).
### User stories
- [Memos - A Twitter Like Notes App You can Self Host](https://noted.lol/memos/)
### Join the community to build memos together!
@@ -64,9 +81,13 @@ See more in [development guide](https://github.com/usememos/memos/tree/main/docs
<img src="https://contrib.rocks/image?repo=usememos/memos" />
</a>
## Acknowledgements
- Thanks [Uffizzi](https://www.uffizzi.com/) for sponsoring preview environments for PRs.
## License
This project is open source and available under the [MIT License](https://github.com/usememos/memos/blob/main/LICENSE).
[MIT License](https://github.com/usememos/memos/blob/main/LICENSE)
## Star history

137
api/activity.go Normal file
View File

@@ -0,0 +1,137 @@
package api
import "github.com/usememos/memos/server/profile"
// ActivityType is the type for an activity.
type ActivityType string
const (
// User related.
// ActivityUserCreate is the type for creating users.
ActivityUserCreate ActivityType = "user.create"
// ActivityUserUpdate is the type for updating users.
ActivityUserUpdate ActivityType = "user.update"
// ActivityUserDelete is the type for deleting users.
ActivityUserDelete ActivityType = "user.delete"
// ActivityUserAuthSignIn is the type for user signin.
ActivityUserAuthSignIn ActivityType = "user.auth.signin"
// ActivityUserAuthSignUp is the type for user signup.
ActivityUserAuthSignUp ActivityType = "user.auth.signup"
// ActivityUserSettingUpdate is the type for updating user settings.
ActivityUserSettingUpdate ActivityType = "user.setting.update"
// Memo related.
// ActivityMemoCreate is the type for creating memos.
ActivityMemoCreate ActivityType = "memo.create"
// ActivityMemoUpdate is the type for updating memos.
ActivityMemoUpdate ActivityType = "memo.update"
// ActivityMemoDelete is the type for deleting memos.
ActivityMemoDelete ActivityType = "memo.delete"
// Shortcut related.
// ActivityShortcutCreate is the type for creating shortcuts.
ActivityShortcutCreate ActivityType = "shortcut.create"
// ActivityShortcutUpdate is the type for updating shortcuts.
ActivityShortcutUpdate ActivityType = "shortcut.update"
// ActivityShortcutDelete is the type for deleting shortcuts.
ActivityShortcutDelete ActivityType = "shortcut.delete"
// Resource related.
// ActivityResourceCreate is the type for creating resources.
ActivityResourceCreate ActivityType = "resource.create"
// ActivityResourceDelete is the type for deleting resources.
ActivityResourceDelete ActivityType = "resource.delete"
// Tag related.
// ActivityTagCreate is the type for creating tags.
ActivityTagCreate ActivityType = "tag.create"
// ActivityTagDelete is the type for deleting tags.
ActivityTagDelete ActivityType = "tag.delete"
// Server related.
// ActivityServerStart is the type for starting server.
ActivityServerStart ActivityType = "server.start"
)
// ActivityLevel is the level of activities.
type ActivityLevel string
const (
// ActivityInfo is the INFO level of activities.
ActivityInfo ActivityLevel = "INFO"
// ActivityWarn is the WARN level of activities.
ActivityWarn ActivityLevel = "WARN"
// ActivityError is the ERROR level of activities.
ActivityError ActivityLevel = "ERROR"
)
type ActivityUserCreatePayload struct {
UserID int `json:"userId"`
Username string `json:"username"`
Role Role `json:"role"`
}
type ActivityUserAuthSignInPayload struct {
UserID int `json:"userId"`
IP string `json:"ip"`
}
type ActivityUserAuthSignUpPayload struct {
Username string `json:"username"`
IP string `json:"ip"`
}
type ActivityMemoCreatePayload struct {
Content string `json:"content"`
Visibility string `json:"visibility"`
}
type ActivityShortcutCreatePayload struct {
Title string `json:"title"`
Payload string `json:"payload"`
}
type ActivityResourceCreatePayload struct {
Filename string `json:"filename"`
Type string `json:"type"`
Size int64 `json:"size"`
}
type ActivityTagCreatePayload struct {
TagName string `json:"tagName"`
}
type ActivityServerStartPayload struct {
ServerID string `json:"serverId"`
Profile *profile.Profile `json:"profile"`
}
type Activity struct {
ID int `json:"id"`
// Standard fields
CreatorID int `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
// Domain specific fields
Type ActivityType `json:"type"`
Level ActivityLevel `json:"level"`
Payload string `json:"payload"`
}
// ActivityCreate is the API message for creating an activity.
type ActivityCreate struct {
// Standard fields
CreatorID int
// Domain specific fields
Type ActivityType `json:"type"`
Level ActivityLevel
Payload string `json:"payload"`
}

View File

@@ -1,12 +1,17 @@
package api
type Signin struct {
type SignIn struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Signup struct {
type SSOSignIn struct {
IdentityProviderID int `json:"identityProviderId"`
Code string `json:"code"`
RedirectURI string `json:"redirectUri"`
}
type SignUp struct {
Username string `json:"username"`
Password string `json:"password"`
Role Role `json:"role"`
}

View File

@@ -1,22 +0,0 @@
package api
// CacheNamespace is the type of a cache.
type CacheNamespace string
const (
// UserCache is the cache type of users.
UserCache CacheNamespace = "u"
// MemoCache is the cache type of memos.
MemoCache CacheNamespace = "m"
// ShortcutCache is the cache type of shortcuts.
ShortcutCache CacheNamespace = "s"
// ResourceCache is the cache type of resources.
ResourceCache CacheNamespace = "r"
)
// CacheService is the service for caches.
type CacheService interface {
FindCache(namespace CacheNamespace, id int, entry interface{}) (bool, error)
UpsertCache(namespace CacheNamespace, id int, entry interface{}) error
DeleteCache(namespace CacheNamespace, id int)
}

View File

@@ -1,5 +1,8 @@
package api
// UnknownID is the ID for unknowns.
const UnknownID = -1
// RowStatus is the status for a row.
type RowStatus string

58
api/idp.go Normal file
View File

@@ -0,0 +1,58 @@
package api
type IdentityProviderType string
const (
IdentityProviderOAuth2 IdentityProviderType = "OAUTH2"
)
type IdentityProviderConfig struct {
OAuth2Config *IdentityProviderOAuth2Config `json:"oauth2Config"`
}
type IdentityProviderOAuth2Config struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
AuthURL string `json:"authUrl"`
TokenURL string `json:"tokenUrl"`
UserInfoURL string `json:"userInfoUrl"`
Scopes []string `json:"scopes"`
FieldMapping *FieldMapping `json:"fieldMapping"`
}
type FieldMapping struct {
Identifier string `json:"identifier"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
}
type IdentityProvider struct {
ID int `json:"id"`
Name string `json:"name"`
Type IdentityProviderType `json:"type"`
IdentifierFilter string `json:"identifierFilter"`
Config *IdentityProviderConfig `json:"config"`
}
type IdentityProviderCreate struct {
Name string `json:"name"`
Type IdentityProviderType `json:"type"`
IdentifierFilter string `json:"identifierFilter"`
Config *IdentityProviderConfig `json:"config"`
}
type IdentityProviderFind struct {
ID *int
}
type IdentityProviderPatch struct {
ID int
Type IdentityProviderType `json:"type"`
Name *string `json:"name"`
IdentifierFilter *string `json:"identifierFilter"`
Config *IdentityProviderConfig `json:"config"`
}
type IdentityProviderDelete struct {
ID int
}

View File

@@ -1,5 +1,8 @@
package api
// MaxContentLength means the max memo content bytes is 1MB.
const MaxContentLength = 1 << 30
// Visibility is the type of a visibility.
type Visibility string
@@ -37,7 +40,6 @@ type Memo struct {
Content string `json:"content"`
Visibility Visibility `json:"visibility"`
Pinned bool `json:"pinned"`
DisplayTs int64 `json:"displayTs"`
// Related fields
Creator *User `json:"creator"`
@@ -46,7 +48,8 @@ type Memo struct {
type MemoCreate struct {
// Standard fields
CreatorID int
CreatorID int `json:"-"`
CreatedTs *int64 `json:"createdTs"`
// Domain specific fields
Visibility Visibility `json:"visibility"`
@@ -57,7 +60,7 @@ type MemoCreate struct {
}
type MemoPatch struct {
ID int
ID int `json:"-"`
// Standard fields
CreatedTs *int64 `json:"createdTs"`
@@ -73,11 +76,11 @@ type MemoPatch struct {
}
type MemoFind struct {
ID *int `json:"id"`
ID *int
// Standard fields
RowStatus *RowStatus `json:"rowStatus"`
CreatorID *int `json:"creatorId"`
RowStatus *RowStatus
CreatorID *int
// Domain specific fields
Pinned *bool
@@ -85,8 +88,8 @@ type MemoFind struct {
VisibilityList []Visibility
// Pagination
Limit int
Offset int
Limit *int
Offset *int
}
type MemoDelete struct {

View File

@@ -9,17 +9,17 @@ type MemoOrganizer struct {
Pinned bool
}
type MemoOrganizerUpsert struct {
MemoID int `json:"-"`
UserID int `json:"-"`
Pinned bool `json:"pinned"`
}
type MemoOrganizerFind struct {
MemoID int
UserID int
}
type MemoOrganizerUpsert struct {
MemoID int
UserID int
Pinned bool `json:"pinned"`
}
type MemoOrganizerDelete struct {
MemoID *int
UserID *int

View File

@@ -8,7 +8,7 @@ type MemoResource struct {
}
type MemoResourceUpsert struct {
MemoID int
MemoID int `json:"-"`
ResourceID int
UpdatedTs *int64
}

View File

@@ -9,10 +9,11 @@ type Resource struct {
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"-"`
Type string `json:"type"`
Size int64 `json:"size"`
Filename string `json:"filename"`
Blob []byte `json:"-"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"size"`
// Related fields
LinkedMemoAmount int `json:"linkedMemoAmount"`
@@ -20,13 +21,14 @@ type Resource struct {
type ResourceCreate struct {
// Standard fields
CreatorID int
CreatorID int `json:"-"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"blob"`
Type string `json:"type"`
Size int64 `json:"size"`
Filename string `json:"filename"`
Blob []byte `json:"-"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"-"`
}
type ResourceFind struct {
@@ -38,10 +40,11 @@ type ResourceFind struct {
// Domain specific fields
Filename *string `json:"filename"`
MemoID *int
GetBlob bool
}
type ResourcePatch struct {
ID int
ID int `json:"-"`
// Standard fields
UpdatedTs *int64

View File

@@ -16,7 +16,7 @@ type Shortcut struct {
type ShortcutCreate struct {
// Standard fields
CreatorID int
CreatorID int `json:"-"`
// Domain specific fields
Title string `json:"title"`
@@ -24,7 +24,7 @@ type ShortcutCreate struct {
}
type ShortcutPatch struct {
ID int
ID int `json:"-"`
// Standard fields
UpdatedTs *int64

48
api/storage.go Normal file
View File

@@ -0,0 +1,48 @@
package api
type StorageType string
const (
StorageS3 StorageType = "S3"
)
type StorageConfig struct {
S3Config *StorageS3Config `json:"s3Config"`
}
type StorageS3Config struct {
EndPoint string `json:"endPoint"`
Region string `json:"region"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
URLPrefix string `json:"urlPrefix"`
}
type Storage struct {
ID int `json:"id"`
Name string `json:"name"`
Type StorageType `json:"type"`
Config *StorageConfig `json:"config"`
}
type StorageCreate struct {
Name string `json:"name"`
Type StorageType `json:"type"`
Config *StorageConfig `json:"config"`
}
type StoragePatch struct {
ID int `json:"id"`
Type StorageType `json:"type"`
Name *string `json:"name"`
Config *StorageConfig `json:"config"`
}
type StorageFind struct {
ID *int `json:"id"`
}
type StorageDelete struct {
ID int `json:"id"`
}

View File

@@ -10,8 +10,13 @@ type SystemStatus struct {
// System settings
// Allow sign up.
AllowSignUp bool `json:"allowSignUp"`
// Disable public memos.
DisablePublicMemos bool `json:"disablePublicMemos"`
// Additional style.
AdditionalStyle string `json:"additionalStyle"`
// Additional script.
AdditionalScript string `json:"additionalScript"`
// Customized server profile, including server name and external url.
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
StorageServiceID int `json:"storageServiceId"`
}

View File

@@ -2,39 +2,79 @@ package api
import (
"encoding/json"
"errors"
"fmt"
"golang.org/x/exp/slices"
)
type SystemSettingName string
const (
// SystemSettingServerID is the key type of server id.
SystemSettingServerID SystemSettingName = "serverId"
// SystemSettingSecretSessionName is the key type of secret session name.
SystemSettingSecretSessionName SystemSettingName = "secretSessionName"
// SystemSettingAllowSignUpName is the key type of allow signup setting.
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
// SystemSettingsDisablePublicMemos is the key type of disable public memos setting.
SystemSettingDisablePublicMemosName SystemSettingName = "disablePublicMemos"
// SystemSettingAdditionalStyleName is the key type of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
// SystemSettingAdditionalScriptName is the key type of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
// SystemSettingCustomizedProfileName is the key type of customized server profile.
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
// SystemSettingStorageServiceIDName is the key type of storage service ID.
SystemSettingStorageServiceIDName SystemSettingName = "storageServiceId"
)
// 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"`
// ExternalURL is the external url of server. e.g. https://usermemos.com
ExternalURL string `json:"externalUrl"`
}
func (key SystemSettingName) String() string {
switch key {
case SystemSettingServerID:
return "serverId"
case SystemSettingSecretSessionName:
return "secretSessionName"
case SystemSettingAllowSignUpName:
return "allowSignUp"
case SystemSettingDisablePublicMemosName:
return "disablePublicMemos"
case SystemSettingAdditionalStyleName:
return "additionalStyle"
case SystemSettingAdditionalScriptName:
return "additionalScript"
case SystemSettingCustomizedProfileName:
return "customizedProfile"
case SystemSettingStorageServiceIDName:
return "storageServiceId"
}
return ""
}
var (
SystemSettingAllowSignUpValue = []bool{true, false}
SystemSettingAllowSignUpValue = []bool{true, false}
SystemSettingDisbalePublicMemosValue = []bool{true, false}
)
type SystemSetting struct {
Name SystemSettingName
// Value is a JSON string with basic value
// Value is a JSON string with basic value.
Value string
Description string
}
@@ -46,7 +86,9 @@ type SystemSettingUpsert struct {
}
func (upsert SystemSettingUpsert) Validate() error {
if upsert.Name == SystemSettingAllowSignUpName {
if upsert.Name == SystemSettingServerID {
return errors.New("update server id is not allowed")
} else if upsert.Name == SystemSettingAllowSignUpName {
value := false
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
@@ -63,6 +105,24 @@ func (upsert SystemSettingUpsert) Validate() error {
if invalid {
return fmt.Errorf("invalid system setting allow signup value")
}
} else if upsert.Name == SystemSettingDisablePublicMemosName {
value := false
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting disable public memos value")
}
invalid := true
for _, v := range SystemSettingDisbalePublicMemosValue {
if value == v {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid system setting disable public memos value")
}
} else if upsert.Name == SystemSettingAdditionalStyleName {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
@@ -75,6 +135,32 @@ func (upsert SystemSettingUpsert) Validate() error {
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional script value")
}
} else if upsert.Name == SystemSettingCustomizedProfileName {
customizedProfile := CustomizedProfile{
Name: "memos",
LogoURL: "",
Description: "",
Locale: "en",
Appearance: "system",
ExternalURL: "",
}
err := json.Unmarshal([]byte(upsert.Value), &customizedProfile)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting customized profile value")
}
if !slices.Contains(UserSettingLocaleValue, customizedProfile.Locale) {
return fmt.Errorf("invalid locale value")
}
if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) {
return fmt.Errorf("invalid appearance value")
}
} else if upsert.Name == SystemSettingStorageServiceIDName {
value := 0
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting storage service id value")
}
return nil
} else {
return fmt.Errorf("invalid system setting name")
}
@@ -83,5 +169,5 @@ func (upsert SystemSettingUpsert) Validate() error {
}
type SystemSettingFind struct {
Name *SystemSettingName `json:"name"`
Name SystemSettingName `json:"name"`
}

20
api/tag.go Normal file
View File

@@ -0,0 +1,20 @@
package api
type Tag struct {
Name string
CreatorID int
}
type TagUpsert struct {
Name string
CreatorID int `json:"-"`
}
type TagFind struct {
CreatorID int
}
type TagDelete struct {
Name string `json:"name"`
CreatorID int
}

View File

@@ -2,6 +2,8 @@ package api
import (
"fmt"
"github.com/usememos/memos/common"
)
// Role is the type of a role.
@@ -43,6 +45,7 @@ type User struct {
Nickname string `json:"nickname"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
AvatarURL string `json:"avatarUrl"`
UserSettingList []*UserSetting `json:"userSettingList"`
}
@@ -58,18 +61,29 @@ type UserCreate struct {
}
func (create UserCreate) Validate() error {
if len(create.Username) < 4 {
return fmt.Errorf("username is too short, minimum length is 4")
if len(create.Username) < 3 {
return fmt.Errorf("username is too short, minimum length is 3")
}
if len(create.Password) < 4 {
return fmt.Errorf("password is too short, minimum length is 4")
if len(create.Username) > 32 {
return fmt.Errorf("username is too long, maximum length is 32")
}
if len(create.Nickname) > 64 {
return fmt.Errorf("nickname is too long, maximum length is 64")
}
if create.Email != "" {
if len(create.Email) > 256 {
return fmt.Errorf("email is too long, maximum length is 256")
}
if !common.ValidateEmail(create.Email) {
return fmt.Errorf("invalid email format")
}
}
return nil
}
type UserPatch struct {
ID int
ID int `json:"-"`
// Standard fields
UpdatedTs *int64
@@ -81,10 +95,38 @@ type UserPatch struct {
Nickname *string `json:"nickname"`
Password *string `json:"password"`
ResetOpenID *bool `json:"resetOpenId"`
AvatarURL *string `json:"avatarUrl"`
PasswordHash *string
OpenID *string
}
func (patch UserPatch) Validate() error {
if patch.Username != nil && len(*patch.Username) < 3 {
return fmt.Errorf("username is too short, minimum length is 3")
}
if patch.Username != nil && len(*patch.Username) > 32 {
return fmt.Errorf("username is too long, maximum length is 32")
}
if patch.Nickname != nil && len(*patch.Nickname) > 64 {
return fmt.Errorf("nickname is too long, maximum length is 64")
}
if patch.AvatarURL != nil {
if len(*patch.AvatarURL) > 2<<20 {
return fmt.Errorf("avatar is too large, maximum is 2MB")
}
}
if patch.Email != nil && *patch.Email != "" {
if len(*patch.Email) > 256 {
return fmt.Errorf("email is too long, maximum length is 256")
}
if !common.ValidateEmail(*patch.Email) {
return fmt.Errorf("invalid email format")
}
}
return nil
}
type UserFind struct {
ID *int `json:"id"`

View File

@@ -3,6 +3,8 @@ package api
import (
"encoding/json"
"fmt"
"golang.org/x/exp/slices"
)
type UserSettingKey string
@@ -14,8 +16,6 @@ const (
UserSettingAppearanceKey UserSettingKey = "appearance"
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
// UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option.
UserSettingMemoDisplayTsOptionKey UserSettingKey = "memoDisplayTsOption"
)
// String returns the string format of UserSettingKey type.
@@ -27,17 +27,14 @@ func (key UserSettingKey) String() string {
return "appearance"
case UserSettingMemoVisibilityKey:
return "memoVisibility"
case UserSettingMemoDisplayTsOptionKey:
return "memoDisplayTsOption"
}
return ""
}
var (
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr"}
UserSettingAppearanceValue = []string{"light", "dark"}
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"}
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr", "nl", "sv", "de", "es", "uk", "ru", "it", "hant", "ko"}
UserSettingAppearanceValue = []string{"system", "light", "dark"}
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
)
type UserSetting struct {
@@ -48,7 +45,7 @@ type UserSetting struct {
}
type UserSettingUpsert struct {
UserID int
UserID int `json:"-"`
Key UserSettingKey `json:"key"`
Value string `json:"value"`
}
@@ -60,32 +57,16 @@ func (upsert UserSettingUpsert) Validate() error {
if err != nil {
return fmt.Errorf("failed to unmarshal user setting locale value")
}
invalid := true
for _, value := range UserSettingLocaleValue {
if localeValue == value {
invalid = false
break
}
}
if invalid {
if !slices.Contains(UserSettingLocaleValue, localeValue) {
return fmt.Errorf("invalid user setting locale value")
}
} else if upsert.Key == UserSettingAppearanceKey {
appearanceValue := "light"
appearanceValue := "system"
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting appearance value")
}
invalid := true
for _, value := range UserSettingAppearanceValue {
if appearanceValue == value {
invalid = false
break
}
}
if invalid {
if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
return fmt.Errorf("invalid user setting appearance value")
}
} else if upsert.Key == UserSettingMemoVisibilityKey {
@@ -94,34 +75,9 @@ func (upsert UserSettingUpsert) Validate() error {
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
}
invalid := true
for _, value := range UserSettingMemoVisibilityValue {
if memoVisibilityValue == value {
invalid = false
break
}
}
if invalid {
if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
return fmt.Errorf("invalid user setting memo visibility value")
}
} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
memoDisplayTsOption := "created_ts"
err := json.Unmarshal([]byte(upsert.Value), &memoDisplayTsOption)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo display ts option")
}
invalid := true
for _, value := range UserSettingMemoDisplayTsOptionKeyValue {
if memoDisplayTsOption == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting memo display ts option value")
}
} else {
return fmt.Errorf("invalid user setting key")
}

View File

@@ -1,82 +0,0 @@
package main
import (
"os"
_ "github.com/mattn/go-sqlite3"
"context"
"fmt"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/server"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
DB "github.com/usememos/memos/store/db"
)
const (
greetingBanner = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
`
)
func run(profile *profile.Profile) error {
ctx := context.Background()
db := DB.NewDB(profile)
if err := db.Open(ctx); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
serverInstance := server.NewServer(profile)
storeInstance := store.New(db.Db, profile)
serverInstance.Store = storeInstance
metricCollector := server.NewMetricCollector(profile, storeInstance)
// Disable metrics collector.
metricCollector.Enabled = false
serverInstance.Collector = &metricCollector
println(greetingBanner)
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
metricCollector.Collect(ctx, &metric.Metric{
Name: "service started",
})
return serverInstance.Run()
}
func execute() error {
profile, err := profile.GetProfile()
if err != nil {
return err
}
println("---")
println("profile")
println("mode:", profile.Mode)
println("port:", profile.Port)
println("dsn:", profile.DSN)
println("version:", profile.Version)
println("---")
if err := run(profile); err != nil {
fmt.Printf("error: %+v\n", err)
return err
}
return nil
}
func main() {
if err := execute(); err != nil {
os.Exit(1)
}
}

119
cmd/server.go Normal file
View File

@@ -0,0 +1,119 @@
package cmd
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/usememos/memos/server"
_profile "github.com/usememos/memos/server/profile"
)
const (
greetingBanner = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
`
)
var (
profile *_profile.Profile
mode string
port int
data string
rootCmd = &cobra.Command{
Use: "memos",
Short: `An open-source, self-hosted memo hub with knowledge management and social networking.`,
Run: func(_cmd *cobra.Command, _args []string) {
ctx, cancel := context.WithCancel(context.Background())
s, err := server.NewServer(ctx, profile)
if err != nil {
cancel()
fmt.Printf("failed to create server, error: %+v\n", err)
return
}
c := make(chan os.Signal, 1)
// Trigger graceful shutdown on SIGINT or SIGTERM.
// The default signal sent by the `kill` command is SIGTERM,
// which is taken as the graceful shutdown signal for many systems, eg., Kubernetes, Gunicorn.
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-c
fmt.Printf("%s received.\n", sig.String())
s.Shutdown(ctx)
cancel()
}()
println(greetingBanner)
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
if err := s.Start(ctx); err != nil {
if err != http.ErrServerClosed {
fmt.Printf("failed to start server, error: %+v\n", err)
cancel()
}
}
// Wait for CTRL-C.
<-ctx.Done()
},
}
)
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&mode, "mode", "m", "demo", `mode of server, can be "prod" or "dev" or "demo"`)
rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8081, "port of server")
rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "", "data directory")
err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode"))
if err != nil {
panic(err)
}
err = viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
if err != nil {
panic(err)
}
err = viper.BindPFlag("data", rootCmd.PersistentFlags().Lookup("data"))
if err != nil {
panic(err)
}
viper.SetDefault("mode", "demo")
viper.SetDefault("port", 8081)
viper.SetEnvPrefix("memos")
}
func initConfig() {
viper.AutomaticEnv()
var err error
profile, err = _profile.GetProfile()
if err != nil {
fmt.Printf("failed to get profile, error: %+v\n", err)
return
}
println("---")
println("Server profile")
println("dsn:", profile.DSN)
println("port:", profile.Port)
println("mode:", profile.Mode)
println("version:", profile.Version)
println("---")
}

67
common/log/logger.go Normal file
View File

@@ -0,0 +1,67 @@
// Package log implements a simple logging package.
package log
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var (
// `gl` is the global logger.
// Other packages should use public methods such as Info/Error to do the logging.
// For other types of logging, e.g. logging to a separate file, they should use their own loggers.
gl *zap.Logger
gLevel zap.AtomicLevel
)
// Initializes the global console logger.
func init() {
gLevel = zap.NewAtomicLevelAt(zap.InfoLevel)
gl, _ = zap.Config{
Level: gLevel,
Development: true,
// Use "console" to print readable stacktrace.
Encoding: "console",
EncoderConfig: zap.NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}.Build(
// Skip one caller stack to locate the correct caller.
zap.AddCallerSkip(1),
)
}
// SetLevel wraps the zap Level's SetLevel method.
func SetLevel(level zapcore.Level) {
gLevel.SetLevel(level)
}
// EnabledLevel wraps the zap Level's Enabled method.
func EnabledLevel(level zapcore.Level) bool {
return gLevel.Enabled(level)
}
// Debug wraps the zap Logger's Debug method.
func Debug(msg string, fields ...zap.Field) {
gl.Debug(msg, fields...)
}
// Info wraps the zap Logger's Info method.
func Info(msg string, fields ...zap.Field) {
gl.Info(msg, fields...)
}
// Warn wraps the zap Logger's Warn method.
func Warn(msg string, fields ...zap.Field) {
gl.Warn(msg, fields...)
}
// Error wraps the zap Logger's Error method.
func Error(msg string, fields ...zap.Field) {
gl.Error(msg, fields...)
}
// Sync wraps the zap Logger's Sync method.
func Sync() {
_ = gl.Sync()
}

View File

@@ -1,6 +1,8 @@
package common
import (
"crypto/rand"
"math/big"
"net/mail"
"strings"
@@ -35,3 +37,24 @@ func Min(x, y int) int {
}
return y
}
var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
// RandomString returns a random string with length n.
func RandomString(n int) (string, error) {
var sb strings.Builder
sb.Grow(n)
for i := 0; i < n; i++ {
// The reason for using crypto/rand instead of math/rand is that
// the former relies on hardware to generate random numbers and
// thus has a stronger source of random numbers.
randNum, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
if _, err := sb.WriteRune(letters[randNum.Uint64()]); err != nil {
return "", err
}
}
return sb.String(), nil
}

View File

@@ -0,0 +1,17 @@
version: "3.0"
# uffizzi integration
x-uffizzi:
ingress:
service: memos
port: 5230
services:
memos:
image: "${MEMOS_IMAGE}"
volumes:
- memos_volume:/var/opt/memos
command: ["--mode", "demo"]
volumes:
memos_volume:

133
docs/deploy-with-render.md Normal file
View File

@@ -0,0 +1,133 @@
# A Beginner's Guide to Deploying Memos on Render.com
written by [AJ](https://memos.ajstephens.website/) (also a noob)
<img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" />
[Live Demo](https://demo.usememos.com) • [Official Website](https://usememos.com) • [Source Code](https://github.com/usememos/memos)
## Who is this guide for?
Someone who...
- doesn't have much experience with self hosting
- has a minimal understanding of docker
Someone who wants...
- to use memos
- to support the memos project
- a cost effective and simple way to host it on the cloud with reliablity and persistance
- to share memos with friends
## Requirements
- Can follow instructions
- Have 7ish USD a month on a debit/credit card
## Guide
Create an account at [Render](https://dashboard.render.com/register)
![ss1](https://i.imgur.com/l3K7aqC.png)
1. Go to your dashboard
[https://dashboard.render.com/](https://dashboard.render.com/)
2. Select New Web Service
![ss2](https://i.imgur.com/IIDdK2y.png)
3. Scroll down to "Public Git repository"
4. Paste in the link for the public git repository for memos (https://github.com/usememos/memos) and press continue
![ss3](https://i.imgur.com/OXoCWoJ.png)
5. Render will pre-fill most of the fields but you will need to create a unique name for your web service
6. Adjust region if you want to
7. Don't touch the "branch", "root directory", and "environment" fields
![ss4](https://i.imgur.com/v7Sw3fp.png)
8. Click "enter your payment information" and do so
![ss5](https://i.imgur.com/paKcQFl.png)
![ss6](https://i.imgur.com/JdcO1HC.png)
9. Select the starter plan ($7 a month - a requirement for persistant data - render's free instances spin down when inactive and lose all data)
10. Click "Create Web Service"
![ss7](https://i.imgur.com/MHe45J4.png)
11. Wait patiently while the _magic_ happens 🤷‍♂️
![ss8](https://i.imgur.com/h1PXHHJ.png)
12. After some time (~ 6 min for me) the build will finish and you will see the web service is live
![ss9](https://i.imgur.com/msapkRw.png)
13. Now it's time to add the disk so your data won't dissappear when the webservice redeploys (redeploys happen automatically when the public repo is updated)
14. Select the "Disks" tab on the left menu and then click "Add Disk"
![ss10](https://i.imgur.com/rGeI0bv.png)
15. Name your disk (can be whatever)
16. Set the "Mount Path" to `/var/opt/memos`
17. Set the disk size (default is 10GB but 1GB is plenty and can be increased at any time)
18. Click "Save"
![ss11](https://i.imgur.com/Jbg7O6q.png)
19. Wait...again...while the webservice redeploys with the persistant disk
![ss12](https://i.imgur.com/pTzpE34.png)
20. aaaand....we're back online!
![ss13](https://i.imgur.com/qdsFfSa.png)
21. Time to test! We're going to make sure everything is working correctly.
22. Click the link in the top left, it should look like `https://the-name-you-chose.onrender.com` - this is your self hosted memos link!
![ss14](https://i.imgur.com/cgzFSIn.png)
23. Create a Username and Password (remember these) then click "Sign up as Host"
![ss15](https://i.imgur.com/kuRStAj.png)
24. Create a test memo then click save
![ss16](https://i.imgur.com/Eh2AB44.png)
25. Sign out of your self-hosted memos
![ss17](https://i.imgur.com/0mMb88G.png)
26. Return to your Render dashboard, click the "Manual Deploy" dropdown button and click "Deploy latest commit" and wait until the webservice is live again (This is to test that your data is persistant)
![ss18](https://i.imgur.com/w1N7VTb.png)
27. Once the webservice is live go back to your self-hosted memos page and sign in! (If your memos screen looks different then something went wrong)
28. Once you're logged in, verify your test memo is still there after the redeploy
![ss19](https://i.imgur.com/dTcEQZS.png)
![ss20](https://i.imgur.com/VE2lu8H.png)
## 🎉Celebrate!🎉
You did it! Enjoy using memos!
Want to learn more or need more guidance? Join the community on [telegram](https://t.me/+-_tNF1k70UU4ZTc9) and [discord](https://discord.gg/tfPJa4UmAv).

View File

@@ -37,4 +37,4 @@ Memos is built with a curated tech stack. It is optimized for developer experien
cd web && yarn && yarn dev
```
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
Memos should now be running at [http://localhost:3001](http://localhost:3001) and change either frontend or backend code would trigger live reload.

65
go.mod
View File

@@ -7,42 +7,85 @@ require github.com/mattn/go-sqlite3 v1.14.9
require github.com/google/uuid v1.3.0
require (
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220728030405-41545e8bf201
golang.org/x/crypto v0.1.0
golang.org/x/net v0.6.0
)
require github.com/labstack/echo/v4 v4.9.0
require (
github.com/VictoriaMetrics/fastcache v1.10.0
github.com/gorilla/feeds v1.1.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo-contrib v0.13.0
github.com/stretchr/testify v1.8.1
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.1.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require github.com/segmentio/analytics-go v3.1.0+incompatible
require (
github.com/aws/aws-sdk-go-v2 v1.17.4
github.com/aws/aws-sdk-go-v2/config v1.18.12
github.com/aws/aws-sdk-go-v2/credentials v1.13.12
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3
github.com/pkg/errors v0.9.1
github.com/segmentio/analytics-go v3.1.0+incompatible
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a
golang.org/x/mod v0.6.0
golang.org/x/oauth2 v0.5.0
)

557
go.sum
View File

@@ -1,21 +1,175 @@
github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY=
github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY=
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.12 h1:fKs/I4wccmfrNRO9rdrbMO1NgLxct6H9rNMiPdBxHWw=
github.com/aws/aws-sdk-go-v2/config v1.18.12/go.mod h1:J36fOhj1LQBr+O4hJCiT8FwVvieeoSGOtPuvhKlsNu8=
github.com/aws/aws-sdk-go-v2/credentials v1.13.12 h1:Cb+HhuEnV19zHRaYYVglwvdHGMJWbdsyP4oHhw04xws=
github.com/aws/aws-sdk-go-v2/credentials v1.13.12/go.mod h1:37HG2MBroXK3jXfxVGtbM2J48ra2+Ltu+tmwr/jO0KA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22 h1:3aMfcTmoXtTZnaT86QlVaYh+BRMbvrrmZwIQ5jWqCZQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22/go.mod h1:YGSIJyQ6D6FjKMQh16hVFSIUD54L4F7zTGePqYMYYJU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51 h1:iTFYCAdKzSAjGnVIUe88Hxvix0uaBqr0Rv7qJEOX5hE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51/go.mod h1:7Grl2gV+dx9SWrUIgwwlUvU40t7+lOSbx34XwfmsTkY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29 h1:J4xhFd6zHhdF9jPP0FQJ6WknzBboGMBNjKOv4iTuw4A=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.29/go.mod h1:TwuqRBGzxjQJIwH16/fOZodwXt2Zxa9/cwJC5ke4j7s=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.19/go.mod h1:8W88sW3PjamQpKFUQvHWWKay6ARsNvZnzU7+a4apubw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20 h1:YIvKIfPXQVp0EhXUV644kmQo6cQPPSRmC44A1HSoJeg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.20/go.mod h1:8W88sW3PjamQpKFUQvHWWKay6ARsNvZnzU7+a4apubw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23 h1:c5+bNdV8E4fIPteWx4HZSkqI07oY9exbfQ7JH7Yx4PI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.23/go.mod h1:1jcUfF+FAOEwtIcNiHPaV4TSoZqkUIPzrohmD7fb95c=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22 h1:LjFQf8hFuMO22HkV5VWGLBvmCLBCLPivUAmpdpnp4Vs=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.22/go.mod h1:xt0Au8yPIwYXf/GYPy/vl4K3CgwhfQMYbrH7DlUUIws=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22 h1:ISLJ2BKXe4zzyZ7mp5ewKECiw0U7KpLgS3S6OxY9Cm0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.22/go.mod h1:QFVbqK54XArazLvn2wvWMRBi/jGrWii46qbr5DyPGjc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.2/go.mod h1:SXDHd6fI2RhqB7vmAzyYQCTQnpZrIprVJvYxpzW3JAM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3 h1:PVieHTwugdlHedlxLpYLQsOZAq736RScuEb/m4zhzc4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.3/go.mod h1:XN3YcdmnWYZ3Hrnojvo5p2mc/wfF973nkq3ClXPDMHk=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.1 h1:lQKN/LNa3qqu2cDOQZybP7oL4nMGGiFqob0jZJaR8/4=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.1/go.mod h1:IgV8l3sj22nQDd5qcAGY0WenwCzCphqdbFOpfktZPrI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 h1:0bLhH6DRAqox+g0LatcjGKjjhU6Eudyys6HB6DJVPj8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1/go.mod h1:O1YSOg3aekZibh2SngvCRRG+cRHKKlYgxf/JBF/Kr/k=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 h1:s49mSnsBZEXjfGBkRfmK+nPqzT7Lt3+t2SmAKNyHblw=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3/go.mod h1:b+psTJn33Q4qGoDaM7ZiOVVG8uVjGI6HaZ8WBHdgDgU=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
@@ -24,6 +178,23 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -37,6 +208,8 @@ github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITV
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -44,48 +217,394 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a h1:/YWeLOBWYV5WAQORVPkZF3Pq9IppkcT72GKnWjNf5W8=
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

14
main.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
_ "github.com/mattn/go-sqlite3"
"github.com/usememos/memos/cmd"
)
func main() {
err := cmd.Execute()
if err != nil {
panic(err)
}
}

7
plugin/idp/idp.go Normal file
View File

@@ -0,0 +1,7 @@
package idp
type IdentityProviderUserInfo struct {
Identifier string
DisplayName string
Email string
}

115
plugin/idp/oauth2/oauth2.go Normal file
View File

@@ -0,0 +1,115 @@
// Package oauth2 is the plugin for OAuth2 Identity Provider.
package oauth2
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/pkg/errors"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/store"
"golang.org/x/oauth2"
)
// IdentityProvider represents an OAuth2 Identity Provider.
type IdentityProvider struct {
config *store.IdentityProviderOAuth2Config
}
// NewIdentityProvider initializes a new OAuth2 Identity Provider with the given configuration.
func NewIdentityProvider(config *store.IdentityProviderOAuth2Config) (*IdentityProvider, error) {
for v, field := range map[string]string{
config.ClientID: "clientId",
config.ClientSecret: "clientSecret",
config.TokenURL: "tokenUrl",
config.UserInfoURL: "userInfoUrl",
config.FieldMapping.Identifier: "fieldMapping.identifier",
} {
if v == "" {
return nil, errors.Errorf(`the field "%s" is empty but required`, field)
}
}
return &IdentityProvider{
config: config,
}, nil
}
// ExchangeToken returns the exchanged OAuth2 token using the given authorization code.
func (p *IdentityProvider) ExchangeToken(ctx context.Context, redirectURL, code string) (string, error) {
conf := &oauth2.Config{
ClientID: p.config.ClientID,
ClientSecret: p.config.ClientSecret,
RedirectURL: redirectURL,
Scopes: p.config.Scopes,
Endpoint: oauth2.Endpoint{
AuthURL: p.config.AuthURL,
TokenURL: p.config.TokenURL,
AuthStyle: oauth2.AuthStyleInParams,
},
}
token, err := conf.Exchange(ctx, code)
if err != nil {
return "", errors.Wrap(err, "failed to exchange access token")
}
accessToken, ok := token.Extra("access_token").(string)
if !ok {
return "", errors.New(`missing "access_token" from authorization response`)
}
return accessToken, nil
}
// UserInfo returns the parsed user information using the given OAuth2 token.
func (p *IdentityProvider) UserInfo(token string) (*idp.IdentityProviderUserInfo, error) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, p.config.UserInfoURL, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to new http request")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "failed to get user information")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read response body")
}
var claims map[string]any
err = json.Unmarshal(body, &claims)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal response body")
}
userInfo := &idp.IdentityProviderUserInfo{}
if v, ok := claims[p.config.FieldMapping.Identifier].(string); ok {
userInfo.Identifier = v
}
if userInfo.Identifier == "" {
return nil, errors.Errorf("the field %q is not found in claims or has empty value", p.config.FieldMapping.Identifier)
}
// Best effort to map optional fields
if p.config.FieldMapping.DisplayName != "" {
if v, ok := claims[p.config.FieldMapping.DisplayName].(string); ok {
userInfo.DisplayName = v
}
}
if userInfo.DisplayName == "" {
userInfo.DisplayName = userInfo.Identifier
}
if p.config.FieldMapping.Email != "" {
if v, ok := claims[p.config.FieldMapping.Email].(string); ok {
userInfo.Email = v
}
}
return userInfo, nil
}

View File

@@ -0,0 +1,163 @@
package oauth2
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/store"
)
func TestNewIdentityProvider(t *testing.T) {
tests := []struct {
name string
config *store.IdentityProviderOAuth2Config
containsErr string
}{
{
name: "no tokenUrl",
config: &store.IdentityProviderOAuth2Config{
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
AuthURL: "",
TokenURL: "",
UserInfoURL: "https://example.com/api/user",
FieldMapping: &store.FieldMapping{
Identifier: "login",
},
},
containsErr: `the field "tokenUrl" is empty but required`,
},
{
name: "no userInfoUrl",
config: &store.IdentityProviderOAuth2Config{
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
AuthURL: "",
TokenURL: "https://example.com/token",
UserInfoURL: "",
FieldMapping: &store.FieldMapping{
Identifier: "login",
},
},
containsErr: `the field "userInfoUrl" is empty but required`,
},
{
name: "no field mapping identifier",
config: &store.IdentityProviderOAuth2Config{
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
AuthURL: "",
TokenURL: "https://example.com/token",
UserInfoURL: "https://example.com/api/user",
FieldMapping: &store.FieldMapping{
Identifier: "",
},
},
containsErr: `the field "fieldMapping.identifier" is empty but required`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := NewIdentityProvider(test.config)
assert.ErrorContains(t, err, test.containsErr)
})
}
}
func newMockServer(t *testing.T, code, accessToken string, userinfo []byte) *httptest.Server {
mux := http.NewServeMux()
var rawIDToken string
mux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
vals, err := url.ParseQuery(string(body))
require.NoError(t, err)
require.Equal(t, code, vals.Get("code"))
require.Equal(t, "authorization_code", vals.Get("grant_type"))
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(map[string]any{
"access_token": accessToken,
"token_type": "Bearer",
"refresh_token": "test-refresh-token",
"expires_in": 3600,
"id_token": rawIDToken,
})
require.NoError(t, err)
})
mux.HandleFunc("/oauth2/userinfo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write(userinfo)
require.NoError(t, err)
})
s := httptest.NewServer(mux)
return s
}
func TestIdentityProvider(t *testing.T) {
ctx := context.Background()
const (
testClientID = "test-client-id"
testCode = "test-code"
testAccessToken = "test-access-token"
testSubject = "123456789"
testName = "John Doe"
testEmail = "john.doe@example.com"
)
userInfo, err := json.Marshal(
map[string]any{
"sub": testSubject,
"name": testName,
"email": testEmail,
},
)
require.NoError(t, err)
s := newMockServer(t, testCode, testAccessToken, userInfo)
oauth2, err := NewIdentityProvider(
&store.IdentityProviderOAuth2Config{
ClientID: testClientID,
ClientSecret: "test-client-secret",
TokenURL: fmt.Sprintf("%s/oauth2/token", s.URL),
UserInfoURL: fmt.Sprintf("%s/oauth2/userinfo", s.URL),
FieldMapping: &store.FieldMapping{
Identifier: "sub",
DisplayName: "name",
Email: "email",
},
},
)
require.NoError(t, err)
redirectURL := "https://example.com/oauth/callback"
oauthToken, err := oauth2.ExchangeToken(ctx, redirectURL, testCode)
require.NoError(t, err)
require.Equal(t, testAccessToken, oauthToken)
userInfoResult, err := oauth2.UserInfo(oauthToken)
require.NoError(t, err)
wantUserInfo := &idp.IdentityProviderUserInfo{
Identifier: testSubject,
DisplayName: testName,
Email: testEmail,
}
assert.Equal(t, wantUserInfo, userInfoResult)
}

View File

@@ -1,6 +1,14 @@
package metric
// Metric is the API message for metric.
type Metric struct {
ID string
Name string
Labels map[string]string
}
// Collector is the interface definition for metric collector.
type Collector interface {
Identify(id string) error
Collect(metric *Metric) error
}

View File

@@ -1,7 +0,0 @@
package metric
// Metric is the API message for metric.
type Metric struct {
Name string
Labels map[string]string
}

View File

@@ -3,15 +3,10 @@ package segment
import (
"time"
"github.com/google/uuid"
"github.com/segmentio/analytics-go"
metric "github.com/usememos/memos/plugin/metrics"
)
var (
sessionUUID = uuid.NewString()
)
// collector is the metrics collector https://segment.com/.
type collector struct {
client analytics.Client
@@ -26,6 +21,14 @@ func NewCollector(key string) metric.Collector {
}
}
// Identify will identify the server caller.
func (c *collector) Identify(id string) error {
return c.client.Enqueue(analytics.Identify{
UserId: id,
Timestamp: time.Now().UTC(),
})
}
// Collect will exec all the segment collector.
func (c *collector) Collect(metric *metric.Metric) error {
properties := analytics.NewProperties()
@@ -34,9 +37,9 @@ func (c *collector) Collect(metric *metric.Metric) error {
}
return c.client.Enqueue(analytics.Track{
Event: string(metric.Name),
AnonymousId: sessionUUID,
Properties: properties,
Timestamp: time.Now().UTC(),
UserId: metric.ID,
Timestamp: time.Now().UTC(),
Event: metric.Name,
Properties: properties,
})
}

73
plugin/storage/s3/s3.go Normal file
View File

@@ -0,0 +1,73 @@
package s3
import (
"context"
"fmt"
"io"
"github.com/aws/aws-sdk-go-v2/aws"
s3config "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
type Config struct {
AccessKey string
SecretKey string
Bucket string
EndPoint string
Region string
URLPrefix string
}
type Client struct {
Client *awss3.Client
Config *Config
}
func NewClient(ctx context.Context, config *Config) (*Client, error) {
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: config.EndPoint,
SigningRegion: config.Region,
}, nil
})
cfg, err := s3config.LoadDefaultConfig(ctx,
s3config.WithEndpointResolverWithOptions(resolver),
s3config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKey, config.SecretKey, "")),
)
if err != nil {
return nil, err
}
client := awss3.NewFromConfig(cfg)
return &Client{
Client: client,
Config: config,
}, nil
}
func (client *Client) UploadFile(ctx context.Context, filename string, fileType string, src io.Reader) (string, error) {
uploader := manager.NewUploader(client.Client)
resp, err := uploader.Upload(ctx, &awss3.PutObjectInput{
Bucket: aws.String(client.Config.Bucket),
Key: aws.String(filename),
Body: src,
ContentType: aws.String(fileType),
ACL: types.ObjectCannedACL(*aws.String("public-read")),
})
if err != nil {
return "", err
}
var link string
if client.Config.URLPrefix == "" {
link = resp.Location
} else {
link = fmt.Sprintf("%s/%s", client.Config.URLPrefix, filename)
}
return link, nil
}

BIN
resources/demo-dark.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -3,7 +3,7 @@ tmp_dir = ".air"
[build]
bin = "./.air/memos"
cmd = "go build -o ./.air/memos ./bin/server/main.go"
cmd = "go build -o ./.air/memos ./main.go"
delay = 1000
exclude_dir = [".air", "web", "build"]
exclude_file = []
@@ -11,3 +11,5 @@ tmp_dir = ".air"
exclude_unchanged = false
follow_symlink = false
full_bin = ""
send_interrupt = true
kill_delay = 2000

View File

@@ -8,6 +8,6 @@ cd "$(dirname "$0")/../"
echo "Start building backend..."
go build -o ./build/memos ./bin/server/main.go
go build -o ./build/memos ./main.go
echo "Backend built!"

View File

@@ -15,6 +15,7 @@ import (
var (
userIDContextKey = "user-id"
sessionName = "memos_session"
)
func getUserIDContextKey() string {
@@ -22,11 +23,12 @@ func getUserIDContextKey() string {
}
func setUserSession(ctx echo.Context, user *api.User) error {
sess, _ := session.Get("memos_session", ctx)
sess, _ := session.Get(sessionName, ctx)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 30,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
sess.Values[userIDContextKey] = user.ID
err := sess.Save(ctx.Request(), ctx.Response())
@@ -37,7 +39,7 @@ func setUserSession(ctx echo.Context, user *api.User) error {
}
func removeUserSession(ctx echo.Context) error {
sess, _ := session.Get("memos_session", ctx)
sess, _ := session.Get(sessionName, ctx)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 0,
@@ -56,61 +58,33 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
ctx := c.Request().Context()
path := c.Path()
// Skip auth.
if common.HasPrefixes(path, "/api/auth") {
if s.defaultAuthSkipper(c) {
return next(c)
}
{
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
if openID != "" {
userFind := &api.UserFind{
OpenID: &openID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
}
if user != nil {
// Stores userID into context.
c.Set(getUserIDContextKey(), user.ID)
return next(c)
sess, _ := session.Get(sessionName, c)
userIDValue := sess.Values[userIDContextKey]
if userIDValue != nil {
userID, _ := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
}
if user != nil {
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", user.Username))
}
c.Set(getUserIDContextKey(), userID)
}
}
{
sess, _ := session.Get("memos_session", c)
userIDValue := sess.Values[userIDContextKey]
if userIDValue != nil {
userID, _ := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
}
if user != nil {
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", user.Username))
}
c.Set(getUserIDContextKey(), userID)
}
}
}
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id", "/api/memo/all", "/api/memo/:memoId", "/api/memo/amount") && c.Request().Method == http.MethodGet {
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/idp", "/api/user/:id", "/api/memo") && c.Request().Method == http.MethodGet {
return next(c)
}
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
return next(c)
}
}
userID := c.Get(getUserIDContextKey())
if userID == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")

View File

@@ -4,10 +4,15 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/store"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@@ -16,7 +21,7 @@ import (
func (s *Server) registerAuthRoutes(g *echo.Group) {
g.POST("/auth/signin", func(c echo.Context) error {
ctx := c.Request().Context()
signin := &api.Signin{}
signin := &api.SignIn{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
@@ -43,78 +48,143 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
if err = setUserSession(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user signed in",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
if err := s.createUserAuthSignInActivity(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(user))
})
g.POST("/auth/logout", func(c echo.Context) error {
g.POST("/auth/signin/sso", func(c echo.Context) error {
ctx := c.Request().Context()
err := removeUserSession(c)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set logout session").SetInternal(err)
signin := &api.SSOSignIn{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user logout",
})
c.Response().WriteHeader(http.StatusOK)
return nil
identityProviderMessage, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProviderMessage{
ID: &signin.IdentityProviderID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
}
var userInfo *idp.IdentityProviderUserInfo
if identityProviderMessage.Type == store.IdentityProviderOAuth2 {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProviderMessage.Config.OAuth2Config)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider instance").SetInternal(err)
}
token, err := oauth2IdentityProvider.ExchangeToken(ctx, signin.RedirectURI, signin.Code)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to exchange token").SetInternal(err)
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user info").SetInternal(err)
}
}
identifierFilter := identityProviderMessage.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile identifier filter").SetInternal(err)
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return echo.NewHTTPError(http.StatusUnauthorized, "Access denied, identifier does not match the filter.").SetInternal(err)
}
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
Username: &userInfo.Identifier,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", userInfo.Identifier)).SetInternal(err)
}
if user == nil {
userCreate := &api.UserCreate{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: api.NormalUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
Password: userInfo.Email,
OpenID: common.GenUUID(),
}
password, err := common.RandomString(20)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate random password").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(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)
}
}
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
}
if err = setUserSession(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
}
if err := s.createUserAuthSignInActivity(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(user))
})
g.POST("/auth/signup", func(c echo.Context) error {
ctx := c.Request().Context()
signup := &api.Signup{}
signup := &api.SignUp{}
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
}
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
}
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
if signup.Role == api.Host && hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
systemSettingAllowSignUpName := api.SystemSettingAllowSignUpName
allowSignUpSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: &systemSettingAllowSignUpName,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue && hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
userCreate := &api.UserCreate{
Username: signup.Username,
Role: api.Role(signup.Role),
// The new signup user should be normal user by default.
Role: api.NormalUser,
Nickname: signup.Username,
Password: signup.Password,
OpenID: common.GenUUID(),
}
hostUserType := api.Host
existedHostUsers, err := s.Store.FindUserList(ctx, &api.UserFind{
Role: &hostUserType,
})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
}
if len(existedHostUsers) == 0 {
// Change the default role to host if there is no host user.
userCreate.Role = api.Host
} else {
allowSignUpSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingAllowSignUpName,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue {
return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
}
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
@@ -123,24 +193,77 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
}
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)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user signed up",
})
if err := s.createUserAuthSignUpActivity(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
err = setUserSession(c, user)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signup session").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(user))
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode created user response").SetInternal(err)
g.POST("/auth/signout", func(c echo.Context) error {
err := removeUserSession(c)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set sign out session").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, true)
})
}
func (s *Server) createUserAuthSignInActivity(c echo.Context, user *api.User) error {
ctx := c.Request().Context()
payload := api.ActivityUserAuthSignInPayload{
UserID: user.ID,
IP: echo.ExtractIPFromRealIPHeader()(c.Request()),
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: user.ID,
Type: api.ActivityUserAuthSignIn,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Collect(ctx, &metric.Metric{
Name: string(activity.Type),
})
return err
}
func (s *Server) createUserAuthSignUpActivity(c echo.Context, user *api.User) error {
ctx := c.Request().Context()
payload := api.ActivityUserAuthSignUpPayload{
Username: user.Username,
IP: echo.ExtractIPFromRealIPHeader()(c.Request()),
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: user.ID,
Type: api.ActivityUserAuthSignUp,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Collect(ctx, &metric.Metric{
Name: string(activity.Type),
})
return err
}

View File

@@ -1,11 +1,57 @@
package server
func composeResponse(data interface{}) interface{} {
type R struct {
Data interface{} `json:"data"`
}
import (
"net/http"
return R{
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
type response struct {
Data interface{} `json:"data"`
}
func composeResponse(data interface{}) 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 (server *Server) defaultAuthSkipper(c echo.Context) bool {
ctx := c.Request().Context()
path := c.Path()
// Skip auth.
if common.HasPrefixes(path, "/api/auth") {
return true
}
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
if openID != "" {
userFind := &api.UserFind{
OpenID: &openID,
}
user, err := server.Store.FindUser(ctx, userFind)
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
}

View File

@@ -25,18 +25,20 @@ func embedFrontend(e *echo.Echo) {
// Use echo static middleware to serve the built dist folder
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: defaultAPIRequestSkipper,
HTML5: true,
Filesystem: getFileSystem("dist"),
}))
g := e.Group("assets")
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
assetsGroup := e.Group("assets")
assetsGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
return next(c)
}
})
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
assetsGroup.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: defaultAPIRequestSkipper,
HTML5: true,
Filesystem: getFileSystem("dist/assets"),
}))

View File

@@ -1,19 +1,16 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/labstack/echo/v4"
getter "github.com/usememos/memos/plugin/http_getter"
metric "github.com/usememos/memos/plugin/metrics"
)
func (s *Server) registerGetterPublicRoutes(g *echo.Group) {
func registerGetterPublicRoutes(g *echo.Group) {
g.GET("/get/httpmeta", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
@@ -26,22 +23,10 @@ func (s *Server) registerGetterPublicRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "httpmeta",
},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(htmlMeta)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode website HTML meta").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(htmlMeta))
})
g.GET("/get/image", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
@@ -54,12 +39,6 @@ func (s *Server) registerGetterPublicRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "image",
},
})
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", image.Mediatype)

232
server/idp.go Normal file
View File

@@ -0,0 +1,232 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/usememos/memos/store"
)
func (s *Server) registerIdentityProviderRoutes(g *echo.Group) {
g.POST("/idp", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderCreate := &api.IdentityProviderCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(identityProviderCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post identity provider request").SetInternal(err)
}
identityProviderMessage, err := s.Store.CreateIdentityProvider(ctx, &store.IdentityProviderMessage{
Name: identityProviderCreate.Name,
Type: store.IdentityProviderType(identityProviderCreate.Type),
IdentifierFilter: identityProviderCreate.IdentifierFilter,
Config: convertIdentityProviderConfigToStore(identityProviderCreate.Config),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create identity provider").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(convertIdentityProviderFromStore(identityProviderMessage)))
})
g.PATCH("/idp/:idpId", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderID, err := strconv.Atoi(c.Param("idpId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("idpId"))).SetInternal(err)
}
identityProviderPatch := &api.IdentityProviderPatch{
ID: identityProviderID,
}
if err := json.NewDecoder(c.Request().Body).Decode(identityProviderPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch identity provider request").SetInternal(err)
}
identityProviderMessage, err := s.Store.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderMessage{
ID: identityProviderPatch.ID,
Type: store.IdentityProviderType(identityProviderPatch.Type),
Name: identityProviderPatch.Name,
IdentifierFilter: identityProviderPatch.IdentifierFilter,
Config: convertIdentityProviderConfigToStore(identityProviderPatch.Config),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch identity provider").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(convertIdentityProviderFromStore(identityProviderMessage)))
})
g.GET("/idp", func(c echo.Context) error {
ctx := c.Request().Context()
identityProviderMessageList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProviderMessage{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider list").SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
isHostUser := false
if ok {
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user != nil && user.Role == api.Host {
isHostUser = true
}
}
identityProviderList := []*api.IdentityProvider{}
for _, identityProviderMessage := range identityProviderMessageList {
identityProvider := convertIdentityProviderFromStore(identityProviderMessage)
// data desensitize
if !isHostUser {
identityProvider.Config.OAuth2Config.ClientSecret = ""
}
identityProviderList = append(identityProviderList, identityProvider)
}
return c.JSON(http.StatusOK, composeResponse(identityProviderList))
})
g.GET("/idp/:idpId", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
// We should only show identity provider list to host user.
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderID, err := strconv.Atoi(c.Param("idpId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("idpId"))).SetInternal(err)
}
identityProviderMessage, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProviderMessage{
ID: &identityProviderID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get identity provider").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(convertIdentityProviderFromStore(identityProviderMessage)))
})
g.DELETE("/idp/:idpId", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderID, err := strconv.Atoi(c.Param("idpId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("idpId"))).SetInternal(err)
}
if err = s.Store.DeleteIdentityProvider(ctx, &store.DeleteIdentityProviderMessage{ID: identityProviderID}); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Identity provider ID not found: %d", identityProviderID))
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete identity provider").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}
func convertIdentityProviderFromStore(identityProviderMessage *store.IdentityProviderMessage) *api.IdentityProvider {
return &api.IdentityProvider{
ID: identityProviderMessage.ID,
Name: identityProviderMessage.Name,
Type: api.IdentityProviderType(identityProviderMessage.Type),
IdentifierFilter: identityProviderMessage.IdentifierFilter,
Config: convertIdentityProviderConfigFromStore(identityProviderMessage.Config),
}
}
func convertIdentityProviderConfigFromStore(config *store.IdentityProviderConfig) *api.IdentityProviderConfig {
return &api.IdentityProviderConfig{
OAuth2Config: &api.IdentityProviderOAuth2Config{
ClientID: config.OAuth2Config.ClientID,
ClientSecret: config.OAuth2Config.ClientSecret,
AuthURL: config.OAuth2Config.AuthURL,
TokenURL: config.OAuth2Config.TokenURL,
UserInfoURL: config.OAuth2Config.UserInfoURL,
Scopes: config.OAuth2Config.Scopes,
FieldMapping: &api.FieldMapping{
Identifier: config.OAuth2Config.FieldMapping.Identifier,
DisplayName: config.OAuth2Config.FieldMapping.DisplayName,
Email: config.OAuth2Config.FieldMapping.Email,
},
},
}
}
func convertIdentityProviderConfigToStore(config *api.IdentityProviderConfig) *store.IdentityProviderConfig {
return &store.IdentityProviderConfig{
OAuth2Config: &store.IdentityProviderOAuth2Config{
ClientID: config.OAuth2Config.ClientID,
ClientSecret: config.OAuth2Config.ClientSecret,
AuthURL: config.OAuth2Config.AuthURL,
TokenURL: config.OAuth2Config.TokenURL,
UserInfoURL: config.OAuth2Config.UserInfoURL,
Scopes: config.OAuth2Config.Scopes,
FieldMapping: &store.FieldMapping{
Identifier: config.OAuth2Config.FieldMapping.Identifier,
DisplayName: config.OAuth2Config.FieldMapping.DisplayName,
Email: config.OAuth2Config.FieldMapping.Email,
},
},
}
}

View File

@@ -4,11 +4,11 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
@@ -24,15 +24,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoCreate := &api.MemoCreate{
CreatorID: userID,
}
memoCreate := &api.MemoCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
}
if memoCreate.Content == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Memo content shouldn't be empty")
}
if memoCreate.Visibility == "" {
userSettingMemoVisibilityKey := api.UserSettingMemoVisibilityKey
@@ -57,13 +52,36 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
}
// Find system settings
disablePublicMemosSystemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingDisablePublicMemosName,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
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 {
memoCreate.Visibility = api.Private
}
}
if len(memoCreate.Content) > api.MaxContentLength {
return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
}
memoCreate.CreatorID = userID
memo, err := s.Store.CreateMemo(ctx, memoCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "memo created",
})
if err := s.createMemoCreateActivity(c, memo); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
for _, resourceID := range memoCreate.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
@@ -78,12 +96,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(memo))
})
g.PATCH("/memo/:memoId", func(c echo.Context) error {
@@ -98,13 +111,15 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
CreatorID: &userID,
}
if _, err := s.Store.FindMemo(ctx, memoFind); err != nil {
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
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")
}
currentTs := time.Now().Unix()
memoPatch := &api.MemoPatch{
@@ -115,7 +130,11 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
}
memo, err := s.Store.PatchMemo(ctx, memoPatch)
if memoPatch.Content != nil && len(*memoPatch.Content) > api.MaxContentLength {
return echo.NewHTTPError(http.StatusBadRequest, "Content size overflow, up to 1MB").SetInternal(err)
}
memo, err = s.Store.PatchMemo(ctx, memoPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
}
@@ -133,12 +152,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(memo))
})
g.GET("/memo", func(c echo.Context) error {
@@ -173,7 +187,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
tag := c.QueryParam("tag")
if tag != "" {
contentSearch := "#" + tag + " "
contentSearch := "#" + tag
memoFind.ContentSearch = &contentSearch
}
visibilityListStr := c.QueryParam("visibility")
@@ -185,48 +199,139 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
memoFind.VisibilityList = visibilityList
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
memoFind.Limit = limit
memoFind.Limit = &limit
}
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
memoFind.Offset = offset
memoFind.Offset = &offset
}
list, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(list))
})
var pinnedMemoList []*api.Memo
var unpinnedMemoList []*api.Memo
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)
}
for _, memo := range list {
if memo.Pinned {
pinnedMemoList = append(pinnedMemoList, memo)
} else {
unpinnedMemoList = append(unpinnedMemoList, memo)
memoFind := &api.MemoFind{
ID: &memoID,
}
memo, err := s.Store.FindMemo(ctx, memoFind)
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 memo.Visibility == api.Private {
if !ok || memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
}
} else if memo.Visibility == api.Protected {
if !ok {
return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
}
}
return c.JSON(http.StatusOK, composeResponse(memo))
})
sort.Slice(pinnedMemoList, func(i, j int) bool {
return pinnedMemoList[i].DisplayTs > pinnedMemoList[j].DisplayTs
})
sort.Slice(unpinnedMemoList, func(i, j int) bool {
return unpinnedMemoList[i].DisplayTs > unpinnedMemoList[j].DisplayTs
})
memoList := []*api.Memo{}
memoList = append(memoList, pinnedMemoList...)
memoList = append(memoList, unpinnedMemoList...)
if memoFind.Limit != 0 {
memoList = memoList[memoFind.Offset:common.Min(len(memoList), memoFind.Offset+memoFind.Limit)]
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)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memoList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo list response").SetInternal(err)
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
return nil
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)
}
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
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)
}
return c.JSON(http.StatusOK, composeResponse(memo))
})
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)
}
resourceFind := &api.ResourceFind{
ID: &memoResourceUpsert.ResourceID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
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 = &currentTs
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, composeResponse(resource))
})
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)
}
resourceFind := &api.ResourceFind{
MemoID: &memoID,
}
resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(resourceList))
})
g.GET("/memo/amount", func(c echo.Context) error {
@@ -243,12 +348,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(len(memoList))); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo amount").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(memoList))
})
g.GET("/memo/stats", func(c echo.Context) error {
@@ -280,16 +380,11 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
displayTsList := []int64{}
createdTsList := []int64{}
for _, memo := range list {
displayTsList = append(displayTsList, memo.DisplayTs)
createdTsList = append(createdTsList, memo.CreatedTs)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(displayTsList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo stats response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(createdTsList))
})
g.GET("/memo/all", func(c echo.Context) error {
@@ -322,10 +417,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
memoFind.VisibilityList = visibilityList
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
memoFind.Limit = limit
memoFind.Limit = &limit
}
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
memoFind.Offset = offset
memoFind.Offset = &offset
}
// Only fetch normal status memos.
@@ -336,178 +431,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
}
sort.Slice(list, func(i, j int) bool {
return list[i].DisplayTs > list[j].DisplayTs
})
if memoFind.Limit != 0 {
list = list[memoFind.Offset:common.Min(len(list), memoFind.Offset+memoFind.Limit)]
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
}
return nil
})
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)
}
memoFind := &api.MemoFind{
ID: &memoID,
}
memo, err := s.Store.FindMemo(ctx, memoFind)
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 memo.Visibility == api.Private {
if !ok || memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
}
} else if memo.Visibility == api.Protected {
if !ok {
return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
}
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
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")
}
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
MemoID: memoID,
UserID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
}
err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
}
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
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)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
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)
}
currentTs := time.Now().Unix()
memoResourceUpsert := &api.MemoResourceUpsert{
MemoID: memoID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
}
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &memoResourceUpsert.ResourceID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
})
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)
}
resourceFind := &api.ResourceFind{
MemoID: &memoID,
}
resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resourceList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
}
return nil
})
g.DELETE("/memo/:memoId/resource/:resourceId", 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("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)
}
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)
return c.JSON(http.StatusOK, composeResponse(list))
})
g.DELETE("/memo/:memoId", func(c echo.Context) error {
@@ -516,19 +440,20 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
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)
}
memoFind := &api.MemoFind{
ID: &memoID,
CreatorID: &userID,
}
if _, err := s.Store.FindMemo(ctx, memoFind); err != nil {
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
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")
}
memoDelete := &api.MemoDelete{
ID: memoID,
@@ -539,7 +464,66 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
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.FindMemo(ctx, &api.MemoFind{
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)
})
}
func (s *Server) createMemoCreateActivity(c echo.Context, memo *api.Memo) error {
ctx := c.Request().Context()
payload := api.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, &api.ActivityCreate{
CreatorID: memo.CreatorID,
Type: api.ActivityMemoCreate,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Collect(ctx, &metric.Metric{
Name: string(activity.Type),
})
return err
}

View File

@@ -8,29 +8,39 @@ import (
"github.com/usememos/memos/plugin/metrics/segment"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/store"
)
// MetricCollector is the metric collector.
type MetricCollector struct {
Collector metric.Collector
collector metric.Collector
ID string
Enabled bool
Profile *profile.Profile
Store *store.Store
}
const (
segmentMetricWriteKey = "fTn5BumOkj352n3TGw9tu0ARH2dOkcoQ"
segmentMetricWriteKey = "NbPruMMmfqfD2AMCw3pkxZTsszVS3hKq"
)
func NewMetricCollector(profile *profile.Profile, store *store.Store) MetricCollector {
func (s *Server) registerMetricCollector() {
c := segment.NewCollector(segmentMetricWriteKey)
mc := &MetricCollector{
collector: c,
ID: s.ID,
Enabled: false,
Profile: s.Profile,
}
s.Collector = mc
}
return MetricCollector{
Collector: c,
Enabled: true,
Profile: profile,
Store: store,
func (mc *MetricCollector) Identify(_ context.Context) {
if !mc.Enabled {
return
}
err := mc.collector.Identify(mc.ID)
if err != nil {
fmt.Printf("Failed to request segment, error: %+v\n", err)
}
}
@@ -39,16 +49,13 @@ func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
return
}
if mc.Profile.Mode == "dev" {
return
}
if metric.Labels == nil {
metric.Labels = map[string]string{}
}
metric.Labels["mode"] = mc.Profile.Mode
metric.Labels["version"] = version.GetCurrentVersion(mc.Profile.Mode)
err := mc.Collector.Collect(metric)
metric.ID = mc.ID
err := mc.collector.Collect(metric)
if err != nil {
fmt.Printf("Failed to request segment, error: %+v\n", err)
}

View File

@@ -1,25 +1,25 @@
package profile
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/viper"
"github.com/usememos/memos/server/version"
)
// Profile is the configuration to start main server.
type Profile struct {
// Mode can be "prod" or "dev"
// Mode can be "prod" or "dev" or "demo"
Mode string `json:"mode"`
// Port is the binding port for server
Port int `json:"port"`
Port int `json:"-"`
// Data is the data directory
Data string `json:"data"`
Data string `json:"-"`
// DSN points to where Memos stores its own data
DSN string `json:"dsn"`
DSN string `json:"-"`
// Version is the current version of server
Version string `json:"version"`
}
@@ -47,13 +47,13 @@ func checkDSN(dataDir string) (string, error) {
// GetDevProfile will return a profile for dev or prod.
func GetProfile() (*Profile, error) {
profile := Profile{}
flag.StringVar(&profile.Mode, "mode", "dev", "mode of server")
flag.IntVar(&profile.Port, "port", 8080, "port of server")
flag.StringVar(&profile.Data, "data", "", "data directory")
flag.Parse()
err := viper.Unmarshal(&profile)
if err != nil {
return nil, err
}
if profile.Mode != "dev" && profile.Mode != "prod" {
profile.Mode = "dev"
if profile.Mode != "demo" && profile.Mode != "dev" && profile.Mode != "prod" {
profile.Mode = "demo"
}
if profile.Mode == "prod" && profile.Data == "" {

View File

@@ -1,24 +1,27 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"html"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/plugin/storage/s3"
)
const (
// The max file size is 32MB.
maxFileSize = (32 * 8) << 20
maxFileSize = 32 << 20
)
func (s *Server) registerResourceRoutes(g *echo.Group) {
@@ -29,6 +32,33 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceCreate := &api.ResourceCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(resourceCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
}
resourceCreate.CreatorID = userID
// Only allow those external links with http prefix.
if resourceCreate.ExternalLink != "" && !strings.HasPrefix(resourceCreate.ExternalLink, "http") {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link")
}
resource, err := s.Store.CreateResource(ctx, resourceCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
if err := s.createResourceCreateActivity(c, resource); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(resource))
})
g.POST("/resource/blob", 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")
}
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
}
@@ -50,32 +80,74 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
defer src.Close()
fileBytes, err := io.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
systemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingStorageServiceIDName})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
}
storageServiceID := 0
if systemSetting != nil {
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
}
}
resourceCreate := &api.ResourceCreate{
Filename: filename,
Type: filetype,
Size: size,
Blob: fileBytes,
CreatorID: userID,
var resourceCreate *api.ResourceCreate
if storageServiceID == 0 {
fileBytes, err := io.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
}
resourceCreate = &api.ResourceCreate{
CreatorID: userID,
Filename: filename,
Type: filetype,
Size: size,
Blob: fileBytes,
}
} else {
storage, err := s.Store.FindStorage(ctx, &api.StorageFind{ID: &storageServiceID})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
}
if storage.Type == api.StorageS3 {
s3Config := storage.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,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to new s3 client").SetInternal(err)
}
link, err := s3client.UploadFile(ctx, filename, filetype, src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload via s3 client").SetInternal(err)
}
resourceCreate = &api.ResourceCreate{
CreatorID: userID,
Filename: filename,
Type: filetype,
ExternalLink: link,
}
} else {
return echo.NewHTTPError(http.StatusInternalServerError, "Unsupported storage type")
}
}
resource, err := s.Store.CreateResource(ctx, resourceCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "resource created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
if err := s.createResourceCreateActivity(c, resource); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(resource))
})
g.GET("/resource", func(c echo.Context) error {
@@ -101,12 +173,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
resource.LinkedMemoAmount = len(memoResourceList)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(list))
})
g.GET("/resource/:resourceId", func(c echo.Context) error {
@@ -123,17 +190,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
GetBlob: true,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(resource))
})
g.GET("/resource/:resourceId/blob", func(c echo.Context) error {
@@ -150,18 +213,52 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
GetBlob: true,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
return c.Stream(http.StatusOK, resource.Type, bytes.NewReader(resource.Blob))
})
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write resource blob").SetInternal(err)
g.PATCH("/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")
}
return nil
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
currentTs := time.Now().Unix()
resourcePatch := &api.ResourcePatch{
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
}
resourcePatch.ID = resourceID
resource, err = s.Store.PatchResource(ctx, resourcePatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(resource))
})
g.DELETE("/resource/:resourceId", func(c echo.Context) error {
@@ -183,8 +280,8 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, "Not find resource").SetInternal(err)
if resource.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
resourceDelete := &api.ResourceDelete{
@@ -196,50 +293,8 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
g.PATCH("/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")
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
if _, err := s.Store.FindResource(ctx, resourceFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
currentTs := time.Now().Unix()
resourcePatch := &api.ResourcePatch{
ID: resourceID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
}
resource, err := s.Store.PatchResource(ctx, resourcePatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
})
}
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
@@ -249,23 +304,55 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
filename := html.UnescapeString(c.Param("filename"))
filename, err := url.QueryUnescape(c.Param("filename"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("filename is invalid: %s", c.Param("filename"))).SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
Filename: &filename,
GetBlob: true,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
resourceType := strings.ToLower(resource.Type)
if strings.HasPrefix(resourceType, "text") {
resourceType = echo.MIMETextPlainCharsetUTF8
} else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(resource.Blob))
return nil
}
return nil
return c.Stream(http.StatusOK, resourceType, bytes.NewReader(resource.Blob))
})
}
func (s *Server) createResourceCreateActivity(c echo.Context, resource *api.Resource) error {
ctx := c.Request().Context()
payload := api.ActivityResourceCreatePayload{
Filename: resource.Filename,
Type: resource.Type,
Size: resource.Size,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: resource.CreatorID,
Type: api.ActivityResourceCreate,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Collect(ctx, &metric.Metric{
Name: string(activity.Type),
})
return err
}

View File

@@ -1,8 +1,11 @@
package server
import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/feeds"
@@ -11,9 +14,44 @@ import (
)
func (s *Server) registerRSSRoutes(g *echo.Group) {
g.GET("/explore/rss.xml", func(c echo.Context) error {
ctx := c.Request().Context()
systemCustomizedProfile, err := getSystemCustomizedProfile(ctx, s)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
}
normalStatus := api.Normal
memoFind := api.MemoFind{
RowStatus: &normalStatus,
VisibilityList: []api.Visibility{
api.Public,
},
}
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
rss, err := generateRSSFromMemoList(memoList, baseURL, &systemCustomizedProfile)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss)
})
g.GET("/u/:id/rss.xml", func(c echo.Context) error {
ctx := c.Request().Context()
systemCustomizedProfile, err := getSystemCustomizedProfile(ctx, s)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
}
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
@@ -32,41 +70,135 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
userFind := api.UserFind{
ID: &id,
}
user, err := s.Store.FindUser(ctx, &userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
feed := &feeds.Feed{
Title: "Memos",
Link: &feeds.Link{Href: baseURL},
Description: "Memos",
Author: &feeds.Author{Name: user.Username},
Created: time.Now(),
}
feed.Items = make([]*feeds.Item, len(memoList))
for i, memo := range memoList {
feed.Items[i] = &feeds.Item{
Title: user.Username + "-memos-" + strconv.Itoa(memo.ID),
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
Description: memo.Content,
Created: time.Unix(memo.CreatedTs, 0),
}
}
rss, err := feed.ToRss()
rss, err := generateRSSFromMemoList(memoList, baseURL, &systemCustomizedProfile)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
rssPrefix := `<?xml version="1.0" encoding="UTF-8"?>`
return c.XMLBlob(http.StatusOK, []byte(rss[len(rssPrefix):]))
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss)
})
}
const MaxRSSItemCount = 100
const MaxRSSItemTitleLength = 100
func generateRSSFromMemoList(memoList []*api.Memo, baseURL string, profile *api.CustomizedProfile) (string, error) {
feed := &feeds.Feed{
Title: profile.Name,
Link: &feeds.Link{Href: baseURL},
Description: profile.Description,
Created: time.Now(),
}
var itemCountLimit = min(len(memoList), MaxRSSItemCount)
feed.Items = make([]*feeds.Item, itemCountLimit)
for i := 0; i < itemCountLimit; i++ {
memo := memoList[i]
feed.Items[i] = &feeds.Item{
Title: getRSSItemTitle(memo.Content),
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
Description: getRSSItemDescription(memo.Content),
Created: time.Unix(memo.CreatedTs, 0),
}
}
rss, err := feed.ToRss()
if err != nil {
return "", err
}
return rss, nil
}
func getSystemCustomizedProfile(ctx context.Context, s *Server) (api.CustomizedProfile, error) {
systemStatus := api.SystemStatus{
CustomizedProfile: api.CustomizedProfile{
Name: "memos",
LogoURL: "",
Description: "",
Locale: "en",
Appearance: "system",
ExternalURL: "",
},
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
if err != nil {
return api.CustomizedProfile{}, err
}
for _, systemSetting := range systemSettingList {
if systemSetting.Name == api.SystemSettingServerID || systemSetting.Name == api.SystemSettingSecretSessionName {
continue
}
var value interface{}
err := json.Unmarshal([]byte(systemSetting.Value), &value)
if err != nil {
return api.CustomizedProfile{}, err
}
if systemSetting.Name == api.SystemSettingCustomizedProfileName {
valueMap := value.(map[string]interface{})
systemStatus.CustomizedProfile = api.CustomizedProfile{}
if v := valueMap["name"]; v != nil {
systemStatus.CustomizedProfile.Name = v.(string)
}
if v := valueMap["logoUrl"]; v != nil {
systemStatus.CustomizedProfile.LogoURL = v.(string)
}
if v := valueMap["description"]; v != nil {
systemStatus.CustomizedProfile.Description = v.(string)
}
if v := valueMap["locale"]; v != nil {
systemStatus.CustomizedProfile.Locale = v.(string)
}
if v := valueMap["appearance"]; v != nil {
systemStatus.CustomizedProfile.Appearance = v.(string)
}
if v := valueMap["externalUrl"]; v != nil {
systemStatus.CustomizedProfile.ExternalURL = v.(string)
}
}
}
return systemStatus.CustomizedProfile, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func getRSSItemTitle(content string) string {
var title string
if isTitleDefined(content) {
title = strings.Split(content, "\n")[0][2:]
} else {
title = strings.Split(content, "\n")[0]
var titleLengthLimit = min(len(title), MaxRSSItemTitleLength)
if titleLengthLimit < len(title) {
title = title[:titleLengthLimit] + "..."
}
}
return title
}
func getRSSItemDescription(content string) string {
var description string
if isTitleDefined(content) {
var firstLineEnd = strings.Index(content, "\n")
description = strings.Trim(content[firstLineEnd+1:], " ")
} else {
description = content
}
return description
}
func isTitleDefined(content string) bool {
return strings.HasPrefix(content, "# ")
}

View File

@@ -1,13 +1,19 @@
package server
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
@@ -15,58 +21,88 @@ import (
)
type Server struct {
e *echo.Echo
e *echo.Echo
db *sql.DB
ID string
Profile *profile.Profile
Store *store.Store
Collector *MetricCollector
Profile *profile.Profile
Store *store.Store
}
func NewServer(profile *profile.Profile) *Server {
func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) {
e := echo.New()
e.Debug = true
e.HideBanner = true
e.HidePort = true
db := db.NewDB(profile)
if err := db.Open(ctx); err != nil {
return nil, errors.Wrap(err, "cannot open db")
}
s := &Server{
e: e,
db: db.DBInstance,
Profile: profile,
}
storeInstance := store.New(db.DBInstance, profile)
s.Store = storeInstance
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339}",` +
`"method":"${method}","uri":"${uri}",` +
`"status":${status},"error":"${error}"}` + "\n",
}))
e.Use(middleware.Gzip())
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
Skipper: s.defaultAuthSkipper,
TokenLookup: "cookie:_csrf",
}))
e.Use(middleware.CORS())
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
Skipper: defaultGetRequestSkipper,
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSPreloadEnabled: false,
}))
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Skipper: middleware.DefaultSkipper,
ErrorMessage: "Request timeout",
Timeout: 30 * time.Second,
}))
serverID, err := s.getSystemServerID(ctx)
if err != nil {
return nil, err
}
s.ID = serverID
secretSessionName := "usememos"
if profile.Mode == "prod" {
secretSessionName, err = s.getSystemSecretSessionName(ctx)
if err != nil {
return nil, err
}
}
e.Use(session.Middleware(sessions.NewCookieStore([]byte(secretSessionName))))
embedFrontend(e)
// In dev mode, set the const secret key to make signin session persistence.
secret := []byte("usememos")
if profile.Mode == "prod" {
secret = securecookie.GenerateRandomKey(16)
}
e.Use(session.Middleware(sessions.NewCookieStore(secret)))
s := &Server{
e: e,
Profile: profile,
}
// Register MetricCollector to server.
s.registerMetricCollector()
rootGroup := e.Group("")
s.registerRSSRoutes(rootGroup)
webhookGroup := e.Group("/h")
s.registerResourcePublicRoutes(webhookGroup)
publicGroup := e.Group("/o")
s.registerResourcePublicRoutes(publicGroup)
s.registerGetterPublicRoutes(publicGroup)
registerGetterPublicRoutes(publicGroup)
apiGroup := e.Group("/api")
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -79,10 +115,57 @@ func NewServer(profile *profile.Profile) *Server {
s.registerShortcutRoutes(apiGroup)
s.registerResourceRoutes(apiGroup)
s.registerTagRoutes(apiGroup)
s.registerStorageRoutes(apiGroup)
s.registerIdentityProviderRoutes(apiGroup)
return s
return s, nil
}
func (server *Server) Run() error {
return server.e.Start(fmt.Sprintf(":%d", server.Profile.Port))
func (s *Server) Start(ctx context.Context) error {
if err := s.createServerStartActivity(ctx); err != nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Identify(ctx)
return s.e.Start(fmt.Sprintf(":%d", s.Profile.Port))
}
func (s *Server) Shutdown(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Shutdown echo server
if err := s.e.Shutdown(ctx); err != nil {
fmt.Printf("failed to shutdown server, error: %v\n", err)
}
// Close database connection
if err := s.db.Close(); err != nil {
fmt.Printf("failed to close database, error: %v\n", err)
}
fmt.Printf("memos stopped properly\n")
}
func (s *Server) createServerStartActivity(ctx context.Context) error {
payload := api.ActivityServerStartPayload{
ServerID: s.ID,
Profile: s.Profile,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: api.UnknownID,
Type: api.ActivityServerStart,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Collect(ctx, &metric.Metric{
Name: string(activity.Type),
})
return err
}

View File

@@ -7,9 +7,9 @@ import (
"strconv"
"time"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
@@ -21,81 +21,75 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
shortcutCreate := &api.ShortcutCreate{
CreatorID: userID,
}
shortcutCreate := &api.ShortcutCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(shortcutCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
}
shortcutCreate.CreatorID = userID
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "shortcut created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode shortcut response").SetInternal(err)
if err := s.createShortcutCreateActivity(c, shortcut); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(shortcut))
})
g.PATCH("/shortcut/:shortcutId", 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")
}
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutFind := &api.ShortcutFind{
ID: &shortcutID,
}
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
}
if shortcut.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
currentTs := time.Now().Unix()
shortcutPatch := &api.ShortcutPatch{
ID: shortcutID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(shortcutPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
}
shortcut, err := s.Store.PatchShortcut(ctx, shortcutPatch)
shortcutPatch.ID = shortcutID
shortcut, err = s.Store.PatchShortcut(ctx, shortcutPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode shortcut response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(shortcut))
})
g.GET("/shortcut", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutFind := &api.ShortcutFind{}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
shortcutFind.CreatorID = &userID
} else {
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find shortcut")
}
shortcutFind.CreatorID = &userID
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find shortcut")
}
shortcutFind := &api.ShortcutFind{
CreatorID: &userID,
}
list, err := s.Store.FindShortcutList(ctx, shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode shortcut list response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(list))
})
g.GET("/shortcut/:shortcutId", func(c echo.Context) error {
@@ -112,21 +106,31 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch shortcut by ID %d", *shortcutFind.ID)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode shortcut response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(shortcut))
})
g.DELETE("/shortcut/:shortcutId", 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")
}
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutFind := &api.ShortcutFind{
ID: &shortcutID,
}
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
}
if shortcut.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
shortcutDelete := &api.ShortcutDelete{
ID: &shortcutID,
}
@@ -136,7 +140,28 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}
func (s *Server) createShortcutCreateActivity(c echo.Context, shortcut *api.Shortcut) error {
ctx := c.Request().Context()
payload := api.ActivityShortcutCreatePayload{
Title: shortcut.Title,
Payload: shortcut.Payload,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: shortcut.CreatorID,
Type: api.ActivityShortcutCreate,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
return err
}

150
server/storage.go Normal file
View File

@@ -0,0 +1,150 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
func (s *Server) registerStorageRoutes(g *echo.Group) {
g.POST("/storage", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
storageCreate := &api.StorageCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(storageCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
}
storage, err := s.Store.CreateStorage(ctx, storageCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(storage))
})
g.PATCH("/storage/:storageId", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
storageID, err := strconv.Atoi(c.Param("storageId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
}
storagePatch := &api.StoragePatch{
ID: storageID,
}
if err := json.NewDecoder(c.Request().Body).Decode(storagePatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
}
storage, err := s.Store.PatchStorage(ctx, storagePatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(storage))
})
g.GET("/storage", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
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 != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
storageList, err := s.Store.FindStorageList(ctx, &api.StorageFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(storageList))
})
g.DELETE("/storage/:storageId", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
storageID, err := strconv.Atoi(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.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingStorageServiceIDName})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
}
if systemSetting != nil {
storageServiceID := 0
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, &api.StorageDelete{ID: storageID}); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Storage ID not found: %d", storageID))
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}

View File

@@ -1,26 +1,21 @@
package server
import (
"context"
"encoding/json"
"net/http"
"os"
"github.com/google/uuid"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
func (s *Server) registerSystemRoutes(g *echo.Group) {
g.GET("/ping", func(c echo.Context) error {
data := s.Profile
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(data)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose system profile").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(s.Profile))
})
g.GET("/status", func(c echo.Context) error {
@@ -37,15 +32,26 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
if hostUser != nil {
// data desensitize
hostUser.OpenID = ""
hostUser.Email = ""
}
systemStatus := api.SystemStatus{
Host: hostUser,
Profile: *s.Profile,
DBSize: 0,
AllowSignUp: false,
AdditionalStyle: "",
AdditionalScript: "",
Host: hostUser,
Profile: *s.Profile,
DBSize: 0,
AllowSignUp: false,
DisablePublicMemos: false,
AdditionalStyle: "",
AdditionalScript: "",
CustomizedProfile: api.CustomizedProfile{
Name: "memos",
LogoURL: "",
Description: "",
Locale: "en",
Appearance: "system",
ExternalURL: "",
},
StorageServiceID: 0,
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
@@ -53,22 +59,52 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
for _, systemSetting := range systemSettingList {
if systemSetting.Name == api.SystemSettingServerID || systemSetting.Name == api.SystemSettingSecretSessionName {
continue
}
var value interface{}
err = json.Unmarshal([]byte(systemSetting.Value), &value)
err := json.Unmarshal([]byte(systemSetting.Value), &value)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting value").SetInternal(err)
}
if systemSetting.Name == api.SystemSettingAllowSignUpName {
systemStatus.AllowSignUp = value.(bool)
} else if systemSetting.Name == api.SystemSettingDisablePublicMemosName {
systemStatus.DisablePublicMemos = value.(bool)
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
systemStatus.AdditionalStyle = value.(string)
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
systemStatus.AdditionalScript = value.(string)
} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
valueMap := value.(map[string]interface{})
systemStatus.CustomizedProfile = api.CustomizedProfile{}
if v := valueMap["name"]; v != nil {
systemStatus.CustomizedProfile.Name = v.(string)
}
if v := valueMap["logoUrl"]; v != nil {
systemStatus.CustomizedProfile.LogoURL = v.(string)
}
if v := valueMap["description"]; v != nil {
systemStatus.CustomizedProfile.Description = v.(string)
}
if v := valueMap["locale"]; v != nil {
systemStatus.CustomizedProfile.Locale = v.(string)
}
if v := valueMap["appearance"]; v != nil {
systemStatus.CustomizedProfile.Appearance = v.(string)
}
if v := valueMap["externalUrl"]; v != nil {
systemStatus.CustomizedProfile.ExternalURL = v.(string)
}
} else if systemSetting.Name == api.SystemSettingStorageServiceIDName {
systemStatus.StorageServiceID = int(value.(float64))
}
}
userID, ok := c.Get(getUserIDContextKey()).(int)
// Get database size for host user.
if ok {
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
@@ -84,12 +120,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
systemStatus.DBSize = fi.Size()
}
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemStatus)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system status response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(systemStatus))
})
g.POST("/system/setting", func(c echo.Context) error {
@@ -105,9 +136,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "Current signin user not found")
} else if user.Role != api.Host {
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
@@ -123,29 +152,91 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "systemSetting updated",
Labels: map[string]string{"field": string(systemSettingUpsert.Name)},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system setting response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(systemSetting))
})
g.GET("/system/setting", 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")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(systemSettingList))
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSettingList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system setting list response").SetInternal(err)
g.POST("/system/vacuum", 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")
}
return nil
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
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)
})
}
func (s *Server) getSystemServerID(ctx context.Context) (string, error) {
serverIDValue, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingServerID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return "", err
}
if serverIDValue == nil || serverIDValue.Value == "" {
serverIDValue, err = s.Store.UpsertSystemSetting(ctx, &api.SystemSettingUpsert{
Name: api.SystemSettingServerID,
Value: uuid.NewString(),
})
if err != nil {
return "", err
}
}
return serverIDValue.Value, nil
}
func (s *Server) getSystemSecretSessionName(ctx context.Context) (string, error) {
secretSessionNameValue, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingSecretSessionName,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return "", err
}
if secretSessionNameValue == nil || secretSessionNameValue.Value == "" {
secretSessionNameValue, err = s.Store.UpsertSystemSetting(ctx, &api.SystemSettingUpsert{
Name: api.SystemSettingSecretSessionName,
Value: uuid.NewString(),
})
if err != nil {
return "", err
}
}
return secretSessionNameValue.Value, nil
}

View File

@@ -2,55 +2,105 @@ package server
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"sort"
"strconv"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"golang.org/x/exp/slices"
"github.com/labstack/echo/v4"
)
var tagRegexpList = []*regexp.Regexp{regexp.MustCompile(`^#([^\s#]+?) `), regexp.MustCompile(`[^\S]#([^\s#]+?) `), regexp.MustCompile(` #([^\s#]+?) `)}
func (s *Server) registerTagRoutes(g *echo.Group) {
g.POST("/tag", 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")
}
tagUpsert := &api.TagUpsert{}
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")
}
tagUpsert.CreatorID = userID
tag, err := s.Store.UpsertTag(ctx, tagUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
}
if err := s.createTagCreateActivity(c, tag); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(tag.Name))
})
g.GET("/tag", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
}
tagFind := &api.TagFind{
CreatorID: userID,
}
tagList, err := s.Store.FindTagList(ctx, tagFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
}
tagNameList := []string{}
for _, tag := range tagList {
tagNameList = append(tagNameList, tag.Name)
}
return c.JSON(http.StatusOK, composeResponse(tagNameList))
})
g.GET("/tag/suggestion", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
}
contentSearch := "#"
normalRowStatus := api.Normal
memoFind := api.MemoFind{
CreatorID: &userID,
ContentSearch: &contentSearch,
RowStatus: &normalRowStatus,
}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &userID
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if memoFind.CreatorID == nil {
memoFind.CreatorID = &currentUserID
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
}
}
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
tagFind := &api.TagFind{
CreatorID: userID,
}
existTagList, err := s.Store.FindTagList(ctx, tagFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
}
tagNameList := []string{}
for _, tag := range existTagList {
tagNameList = append(tagNameList, tag.Name)
}
tagMapSet := make(map[string]bool)
for _, memo := range memoList {
for _, tag := range findTagListFromMemoContent(memo.Content) {
tagMapSet[tag] = true
if !slices.Contains(tagNameList, tag) {
tagMapSet[tag] = true
}
}
}
tagList := []string{}
@@ -58,22 +108,43 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return c.JSON(http.StatusOK, composeResponse(tagList))
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tags response").SetInternal(err)
g.POST("/tag/delete", 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")
}
return nil
tagDelete := &api.TagDelete{}
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")
}
tagDelete.CreatorID = userID
if err := s.Store.DeleteTag(ctx, tagDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Tag name not found: %s", tagDelete.Name))
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}
var tagRegexp = regexp.MustCompile(`#([^\s#]+)`)
func findTagListFromMemoContent(memoContent string) []string {
tagMapSet := make(map[string]bool)
for _, tagRegexp := range tagRegexpList {
for _, rawTag := range tagRegexp.FindAllString(memoContent, -1) {
tag := tagRegexp.ReplaceAllString(rawTag, "$1")
tagMapSet[tag] = true
}
matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
for _, v := range matches {
tagName := v[1]
tagMapSet[tagName] = true
}
tagList := []string{}
@@ -83,3 +154,24 @@ func findTagListFromMemoContent(memoContent string) []string {
sort.Strings(tagList)
return tagList
}
func (s *Server) createTagCreateActivity(c echo.Context, tag *api.Tag) error {
ctx := c.Request().Context()
payload := api.ActivityTagCreatePayload{
TagName: tag.Name,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: tag.CreatorID,
Type: api.ActivityTagCreate,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
return err
}

View File

@@ -31,11 +31,11 @@ func TestFindTagListFromMemoContent(t *testing.T) {
},
{
memoContent: "#tag1 123123#tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "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", "tag3", "tag4"},
want: []string{"tag1", "tag2", "tag2)", "tag3", "tag4"},
},
}
for _, test := range tests {

View File

@@ -7,6 +7,7 @@ import (
"strconv"
"time"
"github.com/pkg/errors"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
@@ -29,18 +30,20 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
}
if currentUser.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Only Host user can create member.")
return echo.NewHTTPError(http.StatusUnauthorized, "Only Host user can create member")
}
userCreate := &api.UserCreate{
OpenID: common.GenUUID(),
}
userCreate := &api.UserCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
}
if userCreate.Role == api.Host {
return echo.NewHTTPError(http.StatusForbidden, "Could not create host user")
}
userCreate.OpenID = common.GenUUID()
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
@@ -53,15 +56,10 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
if err := s.createUserCreateActivity(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(user))
})
g.GET("/user", func(c echo.Context) error {
@@ -74,13 +72,32 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
for _, user := range userList {
// data desensitize
user.OpenID = ""
user.Email = ""
}
return c.JSON(http.StatusOK, composeResponse(userList))
})
g.POST("/user/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user list response").SetInternal(err)
userSettingUpsert := &api.UserSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
}
return nil
if err := userSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting format").SetInternal(err)
}
userSettingUpsert.UserID = userID
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
}
return c.JSON(http.StatusOK, composeResponse(userSetting))
})
// GET /api/user/me is used to check if the user is logged in.
@@ -106,40 +123,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
g.POST("/user/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userSettingUpsert := &api.UserSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
}
if err := userSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting format").SetInternal(err)
}
userSettingUpsert.UserID = userID
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user setting response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(user))
})
g.GET("/user/:id", func(c echo.Context) error {
@@ -159,13 +143,9 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
if user != nil {
// data desensitize
user.OpenID = ""
user.Email = ""
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(user))
})
g.PATCH("/user/:id", func(c echo.Context) error {
@@ -192,15 +172,14 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
currentTs := time.Now().Unix()
userPatch := &api.UserPatch{
ID: userID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Email != nil && *userPatch.Email != "" && !common.ValidateEmail(*userPatch.Email) {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format")
userPatch.ID = userID
if err := userPatch.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user patch format").SetInternal(err)
}
if userPatch.Password != nil && *userPatch.Password != "" {
@@ -230,12 +209,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, composeResponse(user))
})
g.DELETE("/user/:id", func(c echo.Context) error {
@@ -274,3 +248,29 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return c.JSON(http.StatusOK, true)
})
}
func (s *Server) createUserCreateActivity(c echo.Context, user *api.User) error {
ctx := c.Request().Context()
payload := api.ActivityUserCreatePayload{
UserID: user.ID,
Username: user.Username,
Role: user.Role,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to marshal activity payload")
}
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
CreatorID: user.ID,
Type: api.ActivityUserCreate,
Level: api.ActivityInfo,
Payload: string(payloadBytes),
})
if err != nil || activity == nil {
return errors.Wrap(err, "failed to create activity")
}
s.Collector.Collect(ctx, &metric.Metric{
Name: string(activity.Type),
})
return err
}

View File

@@ -1,19 +1,21 @@
package version
import (
"strconv"
"fmt"
"strings"
"golang.org/x/mod/semver"
)
// Version is the service current released version.
// Semantic versioning: https://semver.org/
var Version = "0.8.1"
var Version = "0.11.0"
// DevVersion is the service current development version.
var DevVersion = "0.8.1"
var DevVersion = "0.11.0"
func GetCurrentVersion(mode string) string {
if mode == "dev" {
if mode == "dev" || mode == "demo" {
return DevVersion
}
return Version
@@ -29,39 +31,31 @@ func GetMinorVersion(version string) string {
func GetSchemaVersion(version string) string {
minorVersion := GetMinorVersion(version)
return minorVersion + ".0"
}
// convSemanticVersionToInt converts version string to int.
func convSemanticVersionToInt(version string) int {
versionList := strings.Split(version, ".")
if len(versionList) < 3 {
return 0
}
major, err := strconv.Atoi(versionList[0])
if err != nil {
return 0
}
minor, err := strconv.Atoi(versionList[1])
if err != nil {
return 0
}
patch, err := strconv.Atoi(versionList[2])
if err != nil {
return 0
}
return major*10000 + minor*100 + patch
}
// IsVersionGreaterThanOrEqualTo returns true if version is greater than or equal to target.
func IsVersionGreaterOrEqualThan(version, target string) bool {
return convSemanticVersionToInt(version) >= convSemanticVersionToInt(target)
return semver.Compare(fmt.Sprintf("v%s", version), fmt.Sprintf("v%s", target)) > -1
}
// IsVersionGreaterThan returns true if version is greater than target.
func IsVersionGreaterThan(version, target string) bool {
return convSemanticVersionToInt(version) > convSemanticVersionToInt(target)
return semver.Compare(fmt.Sprintf("v%s", version), fmt.Sprintf("v%s", target)) > 0
}
type SortVersion []string
func (s SortVersion) Len() int {
return len(s)
}
func (s SortVersion) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s SortVersion) Less(i, j int) bool {
v1 := fmt.Sprintf("v%s", s[i])
v2 := fmt.Sprintf("v%s", s[j])
return semver.Compare(v1, v2) == -1
}

View File

@@ -0,0 +1,93 @@
package version
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsVersionGreaterOrEqualThan(t *testing.T) {
tests := []struct {
version string
target string
want bool
}{
{
version: "0.9.1",
target: "0.9.1",
want: true,
},
{
version: "0.10.0",
target: "0.9.1",
want: true,
},
{
version: "0.9.0",
target: "0.9.1",
want: false,
},
}
for _, test := range tests {
result := IsVersionGreaterOrEqualThan(test.version, test.target)
if result != test.want {
t.Errorf("got result %v, want %v.", result, test.want)
}
}
}
func TestIsVersionGreaterThan(t *testing.T) {
tests := []struct {
version string
target string
want bool
}{
{
version: "0.9.1",
target: "0.9.1",
want: false,
},
{
version: "0.10.0",
target: "0.8.0",
want: true,
},
{
version: "0.8.0",
target: "0.10.0",
want: false,
},
{
version: "0.9.0",
target: "0.9.1",
want: false,
},
}
for _, test := range tests {
result := IsVersionGreaterThan(test.version, test.target)
if result != test.want {
t.Errorf("got result %v, want %v.", result, test.want)
}
}
}
func TestSortVersion(t *testing.T) {
tests := []struct {
versionList []string
want []string
}{
{
versionList: []string{"0.9.1", "0.10.0", "0.8.0"},
want: []string{"0.8.0", "0.9.1", "0.10.0"},
},
{
versionList: []string{"1.9.1", "0.9.1", "0.10.0", "0.8.0"},
want: []string{"0.8.0", "0.9.1", "0.10.0", "1.9.1"},
},
}
for _, test := range tests {
sort.Sort(SortVersion(test.versionList))
assert.Equal(t, test.versionList, test.want)
}
}

89
store/activity.go Normal file
View File

@@ -0,0 +1,89 @@
package store
import (
"context"
"database/sql"
"github.com/usememos/memos/api"
)
// activityRaw is the store model for an Activity.
// Fields have exactly the same meanings as Activity.
type activityRaw struct {
ID int
// Standard fields
CreatorID int
CreatedTs int64
// Domain specific fields
Type api.ActivityType
Level api.ActivityLevel
Payload string
}
// toActivity creates an instance of Activity based on the ActivityRaw.
func (raw *activityRaw) toActivity() *api.Activity {
return &api.Activity{
ID: raw.ID,
CreatorID: raw.CreatorID,
CreatedTs: raw.CreatedTs,
Type: raw.Type,
Level: raw.Level,
Payload: raw.Payload,
}
}
// CreateActivity creates an instance of Activity.
func (s *Store) CreateActivity(ctx context.Context, create *api.ActivityCreate) (*api.Activity, error) {
if s.profile.Mode == "prod" {
return nil, nil
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
activityRaw, err := createActivity(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
activity := activityRaw.toActivity()
return activity, nil
}
// createActivity creates a new activity.
func createActivity(ctx context.Context, tx *sql.Tx, create *api.ActivityCreate) (*activityRaw, error) {
query := `
INSERT INTO activity (
creator_id,
type,
level,
payload
)
VALUES (?, ?, ?, ?)
RETURNING id, type, level, payload, creator_id, created_ts
`
var activityRaw activityRaw
if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.Type, create.Level, create.Payload).Scan(
&activityRaw.ID,
&activityRaw.Type,
&activityRaw.Level,
&activityRaw.Payload,
&activityRaw.CreatedTs,
&activityRaw.CreatedTs,
); err != nil {
return nil, FormatError(err)
}
return &activityRaw, nil
}

View File

@@ -1,72 +1,15 @@
package store
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"github.com/VictoriaMetrics/fastcache"
"github.com/usememos/memos/api"
)
var (
// 64 MiB.
cacheSize = 1024 * 1024 * 64
_ api.CacheService = (*CacheService)(nil)
)
// CacheService implements a cache.
type CacheService struct {
cache *fastcache.Cache
func getUserSettingCacheKey(userSetting userSettingRaw) string {
return fmt.Sprintf("%d-%s", userSetting.UserID, userSetting.Key.String())
}
// NewCacheService creates a cache service.
func NewCacheService() *CacheService {
return &CacheService{
cache: fastcache.New(cacheSize),
}
}
// FindCache finds the value in cache.
func (s *CacheService) FindCache(namespace api.CacheNamespace, id int, entry interface{}) (bool, error) {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
buf2, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
if has {
dec := gob.NewDecoder(bytes.NewReader(buf2))
if err := dec.Decode(entry); err != nil {
return false, fmt.Errorf("failed to decode entry for cache namespace: %s, error: %w", namespace, err)
}
return true, nil
}
return false, nil
}
// UpsertCache upserts the value to cache.
func (s *CacheService) UpsertCache(namespace api.CacheNamespace, id int, entry interface{}) error {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
var buf2 bytes.Buffer
enc := gob.NewEncoder(&buf2)
if err := enc.Encode(entry); err != nil {
return fmt.Errorf("failed to encode entry for cache namespace: %s, error: %w", namespace, err)
}
s.cache.Set(append([]byte(namespace), buf1...), buf2.Bytes())
return nil
}
// DeleteCache deletes the cache.
func (s *CacheService) DeleteCache(namespace api.CacheNamespace, id int) {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
_, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
if has {
s.cache.Del(append([]byte(namespace), buf1...))
}
func getUserSettingFindCacheKey(userSettingFind *api.UserSettingFind) string {
return fmt.Sprintf("%d-%s", userSettingFind.UserID, userSettingFind.Key.String())
}

View File

@@ -24,8 +24,8 @@ var seedFS embed.FS
type DB struct {
// sqlite db connection instance
Db *sql.DB
profile *profile.Profile
DBInstance *sql.DB
profile *profile.Profile
}
// NewDB returns a new instance of DB associated with the given datasource name.
@@ -43,23 +43,13 @@ func (db *DB) Open(ctx context.Context) (err error) {
}
// Connect to the database without foreign_key.
sqlDB, err := sql.Open("sqlite3", db.profile.DSN+"?_foreign_keys=0")
sqliteDB, err := sql.Open("sqlite3", db.profile.DSN+"?cache=shared&_foreign_keys=0&_journal_mode=WAL")
if err != nil {
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
}
db.Db = sqlDB
db.DBInstance = sqliteDB
if db.profile.Mode == "dev" {
// In dev mode, we should migrate and seed the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
if err := db.seed(ctx); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
}
} else {
if db.profile.Mode == "prod" {
// If db file not exists, we should migrate the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
@@ -68,20 +58,28 @@ func (db *DB) Open(ctx context.Context) (err error) {
}
currentVersion := version.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
migrationHistoryList, err := db.FindMigrationHistoryList(ctx, &MigrationHistoryFind{})
if err != nil {
return fmt.Errorf("failed to find migration history, err: %w", err)
}
if migrationHistory == nil {
if _, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
if len(migrationHistoryList) == 0 {
_, err := db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
Version: currentVersion,
}); err != nil {
})
if err != nil {
return fmt.Errorf("failed to upsert migration history, err: %w", err)
}
return nil
}
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
migrationHistoryVersionList := []string{}
for _, migrationHistory := range migrationHistoryList {
migrationHistoryVersionList = append(migrationHistoryVersionList, migrationHistory.Version)
}
sort.Sort(version.SortVersion(migrationHistoryVersionList))
latestMigrationHistoryVersion := migrationHistoryVersionList[len(migrationHistoryVersionList)-1]
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
@@ -98,7 +96,7 @@ func (db *DB) Open(ctx context.Context) (err error) {
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
@@ -112,6 +110,19 @@ func (db *DB) Open(ctx context.Context) (err error) {
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
}
}
} else {
// In non-prod mode, we should always migrate the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
// In demo mode, we should seed the database.
if db.profile.Mode == "demo" {
if err := db.seed(ctx); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
}
}
}
return nil
@@ -122,7 +133,11 @@ const (
)
func (db *DB) applyLatestSchema(ctx context.Context) error {
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
schemaMode := "dev"
if db.profile.Mode == "prod" {
schemaMode = "prod"
}
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", schemaMode, latestSchemaFileName)
buf, err := migrationFS.ReadFile(latestSchemaPath)
if err != nil {
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
@@ -156,7 +171,7 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
}
}
tx, err := db.Db.Begin()
tx, err := db.DBInstance.Begin()
if err != nil {
return err
}
@@ -197,7 +212,7 @@ func (db *DB) seed(ctx context.Context) error {
// execute runs a single SQL statement within a transaction.
func (db *DB) execute(ctx context.Context, stmt string) error {
tx, err := db.Db.Begin()
tx, err := db.DBInstance.Begin()
if err != nil {
return err
}
@@ -229,7 +244,7 @@ func getMinorVersionList() []string {
panic(err)
}
sort.Strings(minorVersionList)
sort.Sort(version.SortVersion(minorVersionList))
return minorVersionList
}

View File

@@ -23,7 +23,8 @@ CREATE TABLE user (
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
open_id TEXT NOT NULL UNIQUE,
avatar_url TEXT NOT NULL DEFAULT ''
);
-- user_setting
@@ -86,3 +87,37 @@ CREATE TABLE memo_resource (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
-- tag
CREATE TABLE tag (
name TEXT NOT NULL,
creator_id INTEGER NOT NULL,
UNIQUE(name, creator_id)
);
-- activity
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
type TEXT NOT NULL DEFAULT '',
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
payload TEXT NOT NULL DEFAULT '{}'
);
-- storage
CREATE TABLE storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
config TEXT NOT NULL DEFAULT '{}'
);
-- idp
CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}'
);

View File

@@ -0,0 +1,9 @@
-- activity
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
type TEXT NOT NULL DEFAULT '',
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
payload TEXT NOT NULL DEFAULT '{}'
);

View File

@@ -0,0 +1,4 @@
ALTER TABLE
user
ADD
COLUMN avatar_url TEXT NOT NULL DEFAULT '';

View File

@@ -0,0 +1,8 @@
-- idp
CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}'
);

View File

@@ -0,0 +1,7 @@
-- storage
CREATE TABLE storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
config TEXT NOT NULL DEFAULT '{}'
);

View File

@@ -1,53 +1,60 @@
-- change user role field from "OWNER"/"USER" to "HOST"/"USER".
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
ALTER TABLE
user RENAME TO _user_old;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user (
id, created_ts, updated_ts, row_status,
email, name, password_hash, open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
name,
password_hash,
open_id
FROM
INSERT INTO
user (
id,
created_ts,
updated_ts,
row_status,
email,
name,
password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
name,
password_hash,
open_id
FROM
_user_old;
UPDATE
user
SET
role = 'HOST'
WHERE
UPDATE
user
SET
role = 'HOST'
WHERE
id IN (
SELECT
id
FROM
_user_old
WHERE
SELECT
id
FROM
_user_old
WHERE
role = 'OWNER'
);
DROP TABLE IF EXISTS _user_old;
PRAGMA foreign_keys = on;
PRAGMA foreign_keys = on;

View File

@@ -1 +1,4 @@
ALTER TABLE memo ADD COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';
ALTER TABLE
memo
ADD
COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';

View File

@@ -1,37 +1,43 @@
-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC".
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
ALTER TABLE
memo RENAME TO _memo_old;
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO memo (
id, creator_id, created_ts, updated_ts,
row_status, content, visibility
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
INSERT INTO
memo (
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
visibility
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
visibility
FROM
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
PRAGMA foreign_keys = on;
PRAGMA foreign_keys = on;

View File

@@ -6,4 +6,4 @@ CREATE TABLE user_setting (
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);

View File

@@ -1,8 +1,9 @@
PRAGMA foreign_keys=off;
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
ALTER TABLE
user RENAME TO _user_old;
-- user
CREATE TABLE user (
@@ -17,7 +18,12 @@ CREATE TABLE user (
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user SELECT * FROM _user_old;
INSERT INTO
user
SELECT
*
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
@@ -33,11 +39,13 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
ALTER TABLE
memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
@@ -51,7 +59,12 @@ CREATE TABLE memo (
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO memo SELECT * FROM _memo_old;
INSERT INTO
memo
SELECT
*
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
@@ -67,11 +80,13 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE memo_organizer RENAME TO _memo_organizer_old;
ALTER TABLE
memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
@@ -84,13 +99,19 @@ CREATE TABLE memo_organizer (
UNIQUE(memo_id, user_id)
);
INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old;
INSERT INTO
memo_organizer
SELECT
*
FROM
_memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE shortcut RENAME TO _shortcut_old;
ALTER TABLE
shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
@@ -104,7 +125,12 @@ CREATE TABLE shortcut (
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO shortcut SELECT * FROM _shortcut_old;
INSERT INTO
shortcut
SELECT
*
FROM
_shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
@@ -120,11 +146,13 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE resource RENAME TO _resource_old;
ALTER TABLE
resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
@@ -139,7 +167,12 @@ CREATE TABLE resource (
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO resource SELECT * FROM _resource_old;
INSERT INTO
resource
SELECT
*
FROM
_resource_old;
DROP TABLE IF EXISTS _resource_old;
@@ -155,11 +188,13 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE user_setting RENAME TO _user_setting_old;
ALTER TABLE
user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
@@ -170,8 +205,13 @@ CREATE TABLE user_setting (
UNIQUE(user_id, key)
);
INSERT INTO user_setting SELECT * FROM _user_setting_old;
INSERT INTO
user_setting
SELECT
*
FROM
_user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
PRAGMA foreign_keys=on;
PRAGMA foreign_keys = on;

View File

@@ -7,4 +7,4 @@ CREATE TABLE memo_resource (
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE,
UNIQUE(memo_id, resource_id)
);
);

View File

@@ -4,4 +4,4 @@ CREATE TABLE system_setting (
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
);

View File

@@ -1 +1,4 @@
ALTER TABLE resource ADD COLUMN external_link TEXT NOT NULL DEFAULT '';
ALTER TABLE
resource
ADD
COLUMN external_link TEXT NOT NULL DEFAULT '';

View File

@@ -10,6 +10,7 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
@@ -24,6 +25,7 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
@@ -38,6 +40,7 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
@@ -52,4 +55,5 @@ SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
END;

View File

@@ -1,8 +1,9 @@
PRAGMA foreign_keys=off;
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
ALTER TABLE
user RENAME TO _user_old;
-- user
CREATE TABLE user (
@@ -17,13 +18,19 @@ CREATE TABLE user (
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user SELECT * FROM _user_old;
INSERT INTO
user
SELECT
*
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
ALTER TABLE
memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
@@ -36,13 +43,19 @@ CREATE TABLE memo (
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
INSERT INTO memo SELECT * FROM _memo_old;
INSERT INTO
memo
SELECT
*
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE memo_organizer RENAME TO _memo_organizer_old;
ALTER TABLE
memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
@@ -53,13 +66,19 @@ CREATE TABLE memo_organizer (
UNIQUE(memo_id, user_id)
);
INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old;
INSERT INTO
memo_organizer
SELECT
*
FROM
_memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE shortcut RENAME TO _shortcut_old;
ALTER TABLE
shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
@@ -72,13 +91,19 @@ CREATE TABLE shortcut (
payload TEXT NOT NULL DEFAULT '{}'
);
INSERT INTO shortcut SELECT * FROM _shortcut_old;
INSERT INTO
shortcut
SELECT
*
FROM
_shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE resource RENAME TO _resource_old;
ALTER TABLE
resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
@@ -93,29 +118,37 @@ CREATE TABLE resource (
size INTEGER NOT NULL DEFAULT 0
);
INSERT INTO resource (
id, creator_id, created_ts, updated_ts,
filename, blob, external_link, type,
INSERT INTO
resource (
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
FROM
FROM
_resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE user_setting RENAME TO _user_setting_old;
ALTER TABLE
user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
@@ -125,13 +158,19 @@ CREATE TABLE user_setting (
UNIQUE(user_id, key)
);
INSERT INTO user_setting SELECT * FROM _user_setting_old;
INSERT INTO
user_setting
SELECT
*
FROM
_user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
DROP TABLE IF EXISTS _memo_resource_old;
ALTER TABLE memo_resource RENAME TO _memo_resource_old;
ALTER TABLE
memo_resource RENAME TO _memo_resource_old;
-- memo_resource
CREATE TABLE memo_resource (
@@ -142,6 +181,11 @@ CREATE TABLE memo_resource (
UNIQUE(memo_id, resource_id)
);
INSERT INTO memo_resource SELECT * FROM _memo_resource_old;
INSERT INTO
memo_resource
SELECT
*
FROM
_memo_resource_old;
DROP TABLE IF EXISTS _memo_resource_old;
DROP TABLE IF EXISTS _memo_resource_old;

View File

@@ -1,4 +1,7 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;

View File

@@ -2,4 +2,4 @@
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
);

View File

@@ -3,7 +3,8 @@
-- add role `ADMIN`
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
ALTER TABLE
user RENAME TO _user_old;
-- user
CREATE TABLE user (
@@ -19,23 +20,31 @@ CREATE TABLE user (
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user (
id, created_ts, updated_ts, row_status,
username, role, email, nickname, password_hash,
INSERT INTO
user (
id,
created_ts,
updated_ts,
row_status,
username,
role,
email,
nickname,
password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
role,
email,
name,
password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
role,
email,
name,
password_hash,
open_id
FROM
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
DROP TABLE IF EXISTS _user_old;

View File

@@ -0,0 +1,6 @@
-- tag
CREATE TABLE tag (
name TEXT NOT NULL,
creator_id INTEGER NOT NULL,
UNIQUE(name, creator_id)
);

View File

@@ -23,7 +23,8 @@ CREATE TABLE user (
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
open_id TEXT NOT NULL UNIQUE,
avatar_url TEXT NOT NULL DEFAULT ''
);
-- user_setting
@@ -86,3 +87,37 @@ CREATE TABLE memo_resource (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
-- tag
CREATE TABLE tag (
name TEXT NOT NULL,
creator_id INTEGER NOT NULL,
UNIQUE(name, creator_id)
);
-- activity
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
type TEXT NOT NULL DEFAULT '',
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
payload TEXT NOT NULL DEFAULT '{}'
);
-- storage
CREATE TABLE storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
config TEXT NOT NULL DEFAULT '{}'
);
-- idp
CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}'
);

View File

@@ -19,8 +19,8 @@ type MigrationHistoryFind struct {
Version *string
}
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
tx, err := db.Db.BeginTx(ctx, nil)
func (db *DB) FindMigrationHistoryList(ctx context.Context, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
tx, err := db.DBInstance.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
@@ -31,16 +31,11 @@ func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFi
return nil, err
}
if len(list) == 0 {
return nil, nil
}
migrationHistory := list[0]
return migrationHistory, nil
return list, nil
}
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
tx, err := db.Db.BeginTx(ctx, nil)
tx, err := db.DBInstance.BeginTx(ctx, nil)
if err != nil {
return nil, err
}

View File

@@ -1,5 +1,14 @@
DELETE FROM memo_organizer;
DELETE FROM resource;
DELETE FROM shortcut;
DELETE FROM memo;
DELETE FROM user;
DELETE FROM
memo_organizer;
DELETE FROM
resource;
DELETE FROM
shortcut;
DELETE FROM
memo;
DELETE FROM
user;

View File

@@ -1,67 +1,67 @@
INSERT INTO
INSERT INTO
user (
`id`,
`username`,
`id`,
`username`,
`role`,
`email`,
`nickname`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
101,
101,
'demohero',
'HOST',
'demo@usememos.com',
'Demo Hero',
'demo_open_id',
hex(randomblob(16)),
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
INSERT INTO
INSERT INTO
user (
`id`,
`username`,
`id`,
`username`,
`role`,
`email`,
`nickname`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
102,
102,
'jack',
'USER',
'jack@usememos.com',
'Jack',
'jack_open_id',
hex(randomblob(16)),
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
INSERT INTO
INSERT INTO
user (
`id`,
`row_status`,
`username`,
`id`,
`row_status`,
`username`,
`role`,
`email`,
`nickname`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
103,
'ARCHIVED',
103,
'ARCHIVED',
'bob',
'USER',
'bob@usememos.com',
'Bob',
'bob_open_id',
hex(randomblob(16)),
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);

Some files were not shown because too many files have changed in this diff Show More