Compare commits

...

779 Commits

Author SHA1 Message Date
Steven
c25c57ab61 feat: support updating display time 2024-05-18 07:52:35 +08:00
Steven
b0aad6f694 chore: tweak resource payload 2024-05-17 08:50:02 +08:00
Steven
0c251f9ab8 chore: fix resource delete handler 2024-05-16 21:53:12 +08:00
wzc90
0a9212f815 fix: memo find for mysql(#3387)
* Update memo.go

* Update store/db/mysql/memo.go

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

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-05-16 07:37:42 +08:00
Steven
537ae622d2 chore: filter inbox type 2024-05-15 23:22:23 +08:00
Steven
3e5e5b1f80 chore: fix search memos 2024-05-15 22:42:31 +08:00
wzc90
3ecbbf31a9 fix: migration scripts for mysql (#3384) 2024-05-15 17:34:59 +08:00
wzc90
deb1deb14c fix: resource create time format for mysql (#3381) 2024-05-15 15:51:43 +08:00
Steven
c8ff3fa2ee chore: fix uploading flag 2024-05-15 09:08:34 +08:00
dependabot[bot]
f67676ac43 chore: bump github.com/aws/aws-sdk-go-v2/feature/s3/manager from 1.16.15 to 1.16.17 (#3367)
chore: bump github.com/aws/aws-sdk-go-v2/feature/s3/manager

Bumps [github.com/aws/aws-sdk-go-v2/feature/s3/manager](https://github.com/aws/aws-sdk-go-v2) from 1.16.15 to 1.16.17.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.16.15...service/ram/v1.16.17)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 11:48:46 +08:00
dependabot[bot]
38c6c50681 chore: bump golangci/golangci-lint-action from 5 to 6 (#3365)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 11:46:44 +08:00
dependabot[bot]
74e0f5429a chore: bump pnpm/action-setup from 3.0.0 to 4.0.0 (#3366)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 11:46:35 +08:00
dependabot[bot]
3add70714f chore: bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.53.1 to 1.53.2 (#3369)
chore: bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.53.1 to 1.53.2.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.53.1...service/s3/v1.53.2)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 11:46:28 +08:00
dependabot[bot]
78f0ae649a chore: bump golang.org/x/net from 0.24.0 to 0.25.0 (#3370)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/net/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 08:12:52 +08:00
Steven
9265b8e2bd chore: update tags filter 2024-05-14 08:12:25 +08:00
Steven
2317204c50 fix: list idp 2024-05-14 07:04:17 +08:00
dependabot[bot]
32abc50af4 chore: bump github.com/aws/aws-sdk-go-v2/config from 1.27.11 to 1.27.13 (#3371) 2024-05-14 06:45:14 +08:00
Steven
0c673d49ef chore: retire memo tags 2024-05-13 22:12:56 +08:00
Steven
d3744ccfa3 chore: tweak seed data 2024-05-13 22:07:44 +08:00
Steven
c561362d62 feat: implement memo property 2024-05-13 22:04:37 +08:00
Steven
555b4fbe32 chore: update seed data 2024-05-13 20:27:24 +08:00
Steven
8948edf654 chore: impl memo payload definition 2024-05-13 20:24:11 +08:00
Steven
31e07c083d chore: fix types 2024-05-13 20:07:56 +08:00
Steven
b8763905ba chore: tweak naming 2024-05-13 20:03:04 +08:00
Michael
62705fed39 chore: translated using Weblate (French) (#3358)
Translated using Weblate (French)

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/fr/

Co-authored-by: Ezmana <ezmana.land@gmail.com>
2024-05-13 10:18:33 +08:00
Michael
6df3cf808e chore: update seed data 2024-05-13 10:15:30 +08:00
Michael
6c481743de chore: translated using Weblate (Turkish) (#3356)
Translated using Weblate (Turkish)

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/tr/

Co-authored-by: Oğuz Han <h4n.3545@gmail.com>
2024-05-13 08:10:15 +08:00
Steven
83b9ea45b9 chore: tweak dark mode styles 2024-05-13 08:09:32 +08:00
Steven
f79554371d chore: retire share dialog 2024-05-13 07:57:58 +08:00
Steven
eda1983964 chore: return workspace setting with default value 2024-05-12 13:32:26 +08:00
Steven
cf423026a5 chore: update store cache 2024-05-12 13:19:31 +08:00
Leo LI
93e8fa4912 fix: upgrade scripts for mysql
* Fix mysql db migration

* Update store/db/mysql/migration/prod/0.22/01__memo_tags.sql

* Update store/db/mysql/migration/prod/0.22/02__memo_payload.sql

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-05-12 12:34:21 +08:00
Steven
bb076ce486 chore: update list workspace settings 2024-05-12 08:09:14 +08:00
Steven
cf7718f8dc chore: update presign expires 2024-05-12 08:03:56 +08:00
Steven
29a22914cf fix: postgres upgrade script 2024-05-11 22:35:26 +08:00
Steven
087f60ba7b chore: fix user setting key checks 2024-05-11 21:14:21 +08:00
wzc90
d4157bcffa fix: upgrade scripts (#3347) 2024-05-11 17:43:13 +08:00
Michael
ee984a8872 chore: translated using Weblate (Chinese (Simplified)) (#3345)
Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

Co-authored-by: Coisini-H <2972186196@qq.com>
2024-05-11 16:00:33 +08:00
wzc90
80df8f8aed fix: upgrade scripts (#3344) 2024-05-11 15:38:14 +08:00
Michael
a15025303b chore: update i18n with Weblate (#3339)
* Translated using Weblate (Japanese)

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ja/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.6% (254 of 255 strings)

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

---------

Co-authored-by: Somme4096 <somme4096@gmail.com>
Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
2024-05-11 08:15:40 +08:00
Lincoln Nogueira
f45f673aec chore: respect supplied listening address (#3338) 2024-05-11 06:50:22 +08:00
Steven
f30599fbd2 chore: fix scripts 2024-05-10 23:23:15 +08:00
Steven
041f9b1beb chore: bump version 2024-05-10 23:02:57 +08:00
boojack
3fbc4d8539 feat: implement drag and drop for resource order in editor (#3337)
* Implement drag and drop for resource order in editor

* chore: update

* chore: update

* chore: update
2024-05-10 20:34:35 +08:00
Steven
5f207c8f0c fix: math overflow 2024-05-10 06:21:47 +08:00
Michael
fe5a0fe98a chore: update i18n with Weblate (#3335)
* Translated using Weblate (Japanese)

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ja/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (255 of 255 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

---------

Co-authored-by: Somme4096 <somme4096@gmail.com>
Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-05-09 22:40:27 +08:00
Steven
584c669068 chore: tweak max width 2024-05-09 22:10:27 +08:00
Steven
46a085f8df chore: add datepicker to timeline 2024-05-09 20:31:37 +08:00
Steven
e7a788fa71 chore: tweak memo detail page 2024-05-09 20:10:13 +08:00
Steven
942052b1ea chore: update grpc options 2024-05-09 20:08:18 +08:00
Steven
deae53c1f1 chore: add dayjs to fix datetime format in safari 2024-05-09 08:52:34 +08:00
Steven
1d99dad435 feat: update timeline page 2024-05-09 07:56:00 +08:00
Steven
33133ea1a3 chore: tweak searchbar styles 2024-05-08 22:50:13 +08:00
Steven
a4235bb4bf chore: tweak tags style 2024-05-08 22:31:22 +08:00
Steven
e0977e53f7 chore: remove unused route 2024-05-08 22:15:29 +08:00
Michael
4e12744811 chore: update translation files (#3332)
Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-05-08 21:35:19 +08:00
Steven
40a5503d45 chore: tweak i18n locales 2024-05-08 21:33:49 +08:00
Steven
16670b6b8d chore: tweak seed data 2024-05-08 20:08:53 +08:00
Michael
d57e7d5a0b chore: update translation files (#3331)
Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-05-08 20:06:05 +08:00
Steven
f7f4206fa1 chore: fix linter 2024-05-08 20:05:16 +08:00
Steven
292861be16 chore: retire tag store 2024-05-08 20:03:18 +08:00
Steven
d0655ece53 refactor: update memo tags 2024-05-08 20:03:01 +08:00
Steven
2c270438ec chore: update default service config 2024-05-08 06:57:53 +08:00
Jerwin Arnado
a9caecf479 fix: Set max height to 50vh for better viewing for larger screens. (#3330)
Feature: Set max height to 50vh for better viewing for larger screens.
2024-05-07 23:19:20 +08:00
Steven
d355e2c631 chore: tweak tags section 2024-05-07 22:11:46 +08:00
Steven
4950ea1c74 fix: grpc max message size 2024-05-07 22:10:27 +08:00
Steven
f9258e41a0 chore: tweak max width 2024-05-07 22:10:08 +08:00
steven
e16546f80a chore: handle legacy workspace setting 2024-05-07 22:07:44 +08:00
dependabot[bot]
a73f979f96 chore: bump lucide-react from 0.368.0 to 0.378.0 in /web (#3313)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.368.0 to 0.378.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.378.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 08:53:35 +08:00
dependabot[bot]
41549a6951 chore: bump @types/node from 20.12.9 to 20.12.10 in /web (#3323)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.12.9 to 20.12.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 08:53:18 +08:00
dependabot[bot]
1e8346290c chore: bump @mui/joy from 5.0.0-beta.32 to 5.0.0-beta.36 in /web (#3317)
Bumps [@mui/joy](https://github.com/mui/material-ui/tree/HEAD/packages/mui-joy) from 5.0.0-beta.32 to 5.0.0-beta.36.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/next/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/HEAD/packages/mui-joy)

---
updated-dependencies:
- dependency-name: "@mui/joy"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 08:53:06 +08:00
dependabot[bot]
6a2701ee83 chore: bump react-redux from 9.1.1 to 9.1.2 in /web (#3316) 2024-05-07 05:11:26 +08:00
dependabot[bot]
68fcf250a0 chore: bump google.golang.org/protobuf from 1.33.0 to 1.34.1 (#3318) 2024-05-07 05:11:15 +08:00
dependabot[bot]
5aaf0b88fd chore: bump golang.org/x/oauth2 from 0.19.0 to 0.20.0 (#3319) 2024-05-07 05:11:04 +08:00
dependabot[bot]
f9b0a2b693 chore: bump golang.org/x/crypto from 0.22.0 to 0.23.0 (#3320) 2024-05-07 05:10:52 +08:00
dependabot[bot]
bf07a2cfe6 chore: bump modernc.org/sqlite from 1.29.8 to 1.29.9 (#3321) 2024-05-07 05:10:40 +08:00
Steven
776664105a chore: tweak content length limit 2024-05-06 19:12:30 +08:00
Michael
b0ecb72eaa chore: translated using Weblate (Hungarian) (#3312)
Translated using Weblate (Hungarian)

Currently translated at 98.6% (299 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/hu/

Co-authored-by: Vermunds <33235701+Vermunds@users.noreply.github.com>
2024-05-06 08:03:52 +08:00
Steven
af1ad2f2db chore: add memo content length limit setting 2024-05-06 08:02:39 +08:00
Nabiel Omar Syarif
56ceba2dec fix: fix deleting tag from tag lists (#3311) 2024-05-06 07:38:15 +08:00
Steven
e32a585bbf chore: update resource dashboard 2024-05-06 07:37:48 +08:00
Steven
dfc0889a4f chore: tweak package name 2024-05-06 07:18:54 +08:00
Michael
da535c8dc6 chore: translated using Weblate (Chinese (Simplified)) (#3307)
Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (303 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

Co-authored-by: Sung Kim <1740603900@qq.com>
2024-05-04 11:52:26 +08:00
Steven
10c57167cc chore: remove unused flags 2024-05-03 07:16:07 +08:00
Steven
5742f9ca13 fix: seed data 2024-05-03 07:02:08 +08:00
Steven
e9831caca1 chore: remove unused field 2024-05-02 22:21:49 +08:00
Steven
b23b6302de chore: tweak linter 2024-05-02 22:10:50 +08:00
Steven
74145157a0 chore: add presign background service 2024-05-02 22:08:45 +08:00
Steven
05f73a2236 chore: tweak linter 2024-05-02 21:46:47 +08:00
Steven
775b79338d chore: update object in s3 2024-05-02 21:44:17 +08:00
Steven
26545c855c refactor: implement s3 storage 2024-05-02 21:28:06 +08:00
Michael
355ea352aa chore: translated using Weblate (Chinese (Traditional)) (#3304)
Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (303 of 303 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (303 of 303 strings)


Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hant/
Translation: memos-i18n/i18n

Co-authored-by: Yuri <isyuricunha@duck.com>
2024-05-02 20:23:12 +08:00
Steven
144269fbbc chore: fix router 2024-05-01 12:44:18 +08:00
Steven
590b626052 chore: update upload resources button 2024-05-01 10:43:09 +08:00
Steven
832ad92bac chore: update gitignore 2024-05-01 10:31:26 +08:00
Steven
20dd3e17f7 chore: rename router package 2024-05-01 10:28:32 +08:00
Steven
8ae4bc95dc chore: migrate auth package 2024-05-01 10:26:46 +08:00
Steven
ff175bbb7e refactor: update resource binary request handler 2024-05-01 10:23:56 +08:00
Steven
6d3d71df30 chore: update frontend dependencies 2024-04-30 22:27:20 +08:00
Steven
d668794c7c chore: update build docker image actions 2024-04-30 22:21:46 +08:00
Steven
33f52320f7 fix: remove lazy import components 2024-04-30 22:06:47 +08:00
Steven
6295979592 chore: add user avatar route 2024-04-30 22:06:34 +08:00
Michael
cac6f42770 fix: server matchs 2024-04-30 10:18:04 +08:00
dependabot[bot]
00f1773495 chore: bump golangci/golangci-lint-action from 4 to 5 (#3298)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 23:28:15 +08:00
Steven
8a3845ff54 chore: remove external resource dialog 2024-04-29 22:57:47 +08:00
Steven
155c5baf2c refactor: add markdown service 2024-04-29 08:00:37 +08:00
Steven
4338234641 chore: retire storage related functions 2024-04-28 21:50:09 +08:00
Steven
320963098f refactor: update storage setting 2024-04-28 21:36:22 +08:00
Steven
f25c7d9b24 fix: serve frontend assets 2024-04-28 08:36:33 +08:00
Steven
d8aeec993c chore: replace classnames with clsx 2024-04-28 00:58:40 +08:00
Steven
1b291422e7 refactor: api version 2024-04-28 00:44:29 +08:00
Steven
8bba7f706e chore: update IME mode checks 2024-04-28 00:12:44 +08:00
Steven
f9942002f9 chore: fix linter 2024-04-27 23:36:57 +08:00
Steven
92872118b9 refactor: tweak api definition 2024-04-27 23:14:58 +08:00
Steven
9b66ef5e26 chore: tweak api definition 2024-04-27 22:02:15 +08:00
Steven
04c78e180c chore: update buf dependencies 2024-04-27 00:37:45 +08:00
Michael
cc2370e01e chore: update i18n with Weblate (#3287)
* Translated using Weblate (Japanese)

Currently translated at 91.0% (276 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ja/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (302 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

---------

Co-authored-by: mirukupc <mirukupc.jp@gmail.com>
Co-authored-by: Coisini-H <2972186196@qq.com>
2024-04-27 00:32:58 +08:00
Steven
7217605831 chore: update gitignore 2024-04-27 00:26:04 +08:00
Steven
a6a62eb79c chore: update gitignore 2024-04-27 00:25:47 +08:00
Steven
5f26c52b49 feat: add goreleaser 2024-04-27 00:22:27 +08:00
Steven
647602beac chore: update link preview 2024-04-25 21:01:13 +08:00
Steven
81d24f32a7 fix: memo comment keys 2024-04-25 09:10:11 +08:00
SkyWT
9bcd4f59a2 fix: editor IME composing event behavior (#3267)
* fix: editor IME composing event behavior

* fix: editor IME composing event behavior

* Update web/src/components/MemoEditor/hooks/useAutoComplete.ts

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-04-25 08:59:26 +08:00
Steven
205bf7ed0e chore: update link target 2024-04-24 22:02:11 +08:00
Steven
cdcb61da17 chore: fix update storage 2024-04-24 09:18:33 +08:00
Michael
7f9af9b12c chore: translated using Weblate (French) (#3272)
Translated using Weblate (French)

Currently translated at 99.6% (302 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/fr/

Co-authored-by: Alexis <alexisl61@outlook.fr>
2024-04-24 06:22:57 +08:00
dependabot[bot]
6b253247cb chore: bump modernc.org/sqlite from 1.29.7 to 1.29.8 (#3266)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.29.7 to 1.29.8.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.29.7...v1.29.8)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 10:04:10 +08:00
Michael
761b4e115b chore: translated using Weblate (English) (#3259)
Translated using Weblate (English)

Currently translated at 100.0% (303 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/en/

Co-authored-by: noxonad <noxonad@proton.me>
2024-04-22 10:11:30 +08:00
Nabiel Omar Syarif
16f209eada fix: specify go toolchain in go.mod (fix #3252) (#3253) 2024-04-22 10:06:33 +08:00
刘明野
06a79a2e37 fix: tag dependencies (#3251)
fix: 修改tag名称后重新请求数据
2024-04-20 21:16:17 +08:00
Michael
330cc3dc89 chore: translated using Weblate (French) (#3249)
Translated using Weblate (French)

Currently translated at 99.6% (302 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/fr/

Co-authored-by: Alexis <alexisl61@outlook.fr>
2024-04-19 08:58:39 +08:00
Steven
e8dfd579c3 chore: update background services 2024-04-18 23:34:35 +08:00
Steven
2a93b8d720 chore: tweak linter 2024-04-18 21:44:46 +08:00
Steven
5d967f41d9 chore: update server 2024-04-18 21:41:00 +08:00
Steven
339fecbfff chore: allow search comments 2024-04-18 21:04:10 +08:00
Steven
2cdcd17ba3 chore: tweak linter 2024-04-17 09:00:16 +08:00
Steven
14d4cfd5a4 chore: tweak store methods name 2024-04-17 08:56:52 +08:00
Michael
00d25b12c1 chore: translated using Weblate (Russian) (#3245)
Translated using Weblate (Russian)

Currently translated at 90.4% (274 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ru/

Co-authored-by: sergeybutakov <pushkagun@gmail.com>
2024-04-16 22:38:20 +08:00
Steven
95df647265 chore: tweak comments 2024-04-16 22:33:25 +08:00
Steven
f52e0e005a chore: update dependencies 2024-04-16 21:28:40 +08:00
Steven
70f3d131f6 chore: update pnpm version 2024-04-16 21:22:40 +08:00
Steven
f7f139d65a chore: tweak typo 2024-04-16 21:15:17 +08:00
coderwander
162521885c chore: fix some typos in comments (#3243)
Signed-off-by: coderwander <770732124@qq.com>
2024-04-16 21:14:38 +08:00
dependabot[bot]
b0b9513de7 chore: bump @types/react from 18.2.75 to 18.2.79 in /web (#3240)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.75 to 18.2.79.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:36:03 +08:00
dependabot[bot]
f221f9bfe9 chore: bump i18next from 23.11.1 to 23.11.2 in /web (#3235)
Bumps [i18next](https://github.com/i18next/i18next) from 23.11.1 to 23.11.2.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v23.11.1...v23.11.2)

---
updated-dependencies:
- dependency-name: i18next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:08:40 +08:00
dependabot[bot]
e831fa8190 chore: bump react-redux from 9.1.0 to 9.1.1 in /web (#3234)
Bumps [react-redux](https://github.com/reduxjs/react-redux) from 9.1.0 to 9.1.1.
- [Release notes](https://github.com/reduxjs/react-redux/releases)
- [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/react-redux/compare/v9.1.0...v9.1.1)

---
updated-dependencies:
- dependency-name: react-redux
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:08:23 +08:00
dependabot[bot]
eae0b7115b chore: bump @types/react-dom from 18.2.24 to 18.2.25 in /web (#3233)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.2.24 to 18.2.25.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:08:01 +08:00
dependabot[bot]
1f0fffda95 chore: bump lucide-react from 0.367.0 to 0.368.0 in /web (#3232)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.367.0 to 0.368.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.368.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 07:07:49 +08:00
Michael
404751f378 chore: update i18n with Weblate (#3230)
* Translated using Weblate (Turkish)

Currently translated at 100.0% (303 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (302 of 303 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

---------

Co-authored-by: Oğuz Han <h4n.3545@gmail.com>
Co-authored-by: Coisini-H <2972186196@qq.com>
2024-04-15 11:19:52 +08:00
Steven
f9dd29ae07 chore: tweak tag store 2024-04-14 22:20:09 +08:00
Steven
1f0bfd2169 chore: update tag store 2024-04-14 22:03:56 +08:00
Michael
4aa72306ff chore: update i18n with Weblate (#3227)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (308 of 308 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (307 of 308 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

---------

Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
Co-authored-by: Coisini-H <2972186196@qq.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-04-14 21:27:55 +08:00
Eng Zer Jun
3b550a8ab8 chore: replace util.Min helper with built-in min (#3224) 2024-04-13 20:16:22 +08:00
Steven
cb7886dc49 chore: retire telegram integration in frontend 2024-04-13 12:42:34 +08:00
Steven
d00e4fdf17 chore: retire telegram plugin 2024-04-13 12:39:41 +08:00
Steven
9a2c423435 chore: update var-naming 2024-04-13 12:11:59 +08:00
Steven
49f8cfd5d1 chore: update jwt middleware 2024-04-13 12:10:57 +08:00
Steven
b27004daae chore: retire unused plugin 2024-04-13 12:09:32 +08:00
Steven
75359854cc chore: fix resource routes 2024-04-13 12:07:53 +08:00
Steven
cebc46adc7 chore: tweak store definition 2024-04-13 11:54:37 +08:00
Steven
6ee3b0f704 chore: go mod tidy 2024-04-13 11:02:29 +08:00
Steven
bbd206e893 chore: retire legacy api 2024-04-13 11:01:16 +08:00
Steven
cf4db17080 chore: fix linter 2024-04-13 10:53:39 +08:00
Steven
c373131b89 chore: migrate idp service 2024-04-13 10:50:25 +08:00
Steven
a77703260f chore: fix linter 2024-04-13 02:57:32 +08:00
Steven
8f51529c78 chore: implement storage service 2024-04-13 02:55:40 +08:00
Steven
707e5caf89 chore: update workspace setting store 2024-04-13 02:08:35 +08:00
Michael
17e8fc5408 chore: update i18n with Weblate (#3220)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (302 of 308 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (302 of 308 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

---------

Co-authored-by: Coisini-H <2972186196@qq.com>
Co-authored-by: LibreTranslate <noreply-mt-libretranslate@weblate.org>
2024-04-12 22:45:12 +08:00
Steven
074e7cf71a chore: fix resource path 2024-04-12 08:57:34 +08:00
Steven
f5461264c5 chore: fix dialog background color 2024-04-12 08:44:19 +08:00
Michael
e2ae32063e chore: translated using Weblate (Turkish) (#3215)
Translated using Weblate (Turkish)

Currently translated at 100.0% (300 of 300 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/tr/

Co-authored-by: Oğuz Han <h4n.3545@gmail.com>
2024-04-12 08:36:38 +08:00
Steven
133951328b chore: fix linter 2024-04-12 08:36:02 +08:00
imikod
257b8add8c chore: localization improvements (#3213)
* localization improvements

* typo fix

* fix linting error
2024-04-12 08:35:14 +08:00
Steven
755d5b83c6 chore: retire legacy setting api 2024-04-12 08:32:54 +08:00
Steven
3088cabe10 chore: tweak linter 2024-04-11 17:58:49 +08:00
Steven
3e6e56b008 refactor: update workspace store definition 2024-04-11 17:53:00 +08:00
Steven
6d842711e2 chore: fix dependencies 2024-04-10 23:34:29 +08:00
dependabot[bot]
b84a0592bb chore: bump react-i18next from 11.18.6 to 14.1.0 in /web (#3210)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 11.18.6 to 14.1.0.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v11.18.6...v14.1.0)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:28:11 +08:00
dependabot[bot]
581a64d5c1 chore: bump lucide-react from 0.309.0 to 0.367.0 in /web (#3208)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.309.0 to 0.367.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.367.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:27:34 +08:00
dependabot[bot]
4537411109 chore: bump @typescript-eslint/eslint-plugin from 6.21.0 to 7.0.0 in /web (#3207)
chore: bump @typescript-eslint/eslint-plugin in /web

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.21.0 to 7.0.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.0.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:26:53 +08:00
dependabot[bot]
d6b1326a2d chore: bump eslint-config-prettier from 8.10.0 to 9.1.0 in /web (#3206)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.10.0 to 9.1.0.
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.10.0...v9.1.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:26:36 +08:00
dependabot[bot]
9490e890c7 chore: bump i18next from 21.10.0 to 23.11.1 in /web (#3205)
Bumps [i18next](https://github.com/i18next/i18next) from 21.10.0 to 23.11.1.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v21.10.0...v23.11.1)

---
updated-dependencies:
- dependency-name: i18next
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:26:22 +08:00
dependabot[bot]
aed76165ea chore: bump @reduxjs/toolkit from 1.9.7 to 2.2.3 in /web (#3204)
Bumps [@reduxjs/toolkit](https://github.com/reduxjs/redux-toolkit) from 1.9.7 to 2.2.3.
- [Release notes](https://github.com/reduxjs/redux-toolkit/releases)
- [Commits](https://github.com/reduxjs/redux-toolkit/compare/v1.9.7...v2.2.3)

---
updated-dependencies:
- dependency-name: "@reduxjs/toolkit"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:26:11 +08:00
dependabot[bot]
51a90b0013 chore: bump golangci/golangci-lint-action from 3 to 4 (#3203)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:25:53 +08:00
dependabot[bot]
7b1fe9fcfc chore: bump pnpm/action-setup from 2.4.0 to 3.0.0 (#3202)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.4.0 to 3.0.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.4.0...v3.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:25:42 +08:00
Steven
ec67f1b3c9 chore: add dependabot 2024-04-10 23:24:17 +08:00
Steven
97cfab1758 chore: upgrade dependencies 2024-04-10 23:19:59 +08:00
Steven
eb251a097e chore: update workspace setting service 2024-04-10 23:01:01 +08:00
Steven
4c47e93fce chore: tweak workspace setting store 2024-04-10 22:31:55 +08:00
Michael
af954db473 chore: update i18n with Weblate (#3201)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (299 of 299 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (299 of 299 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (299 of 299 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (299 of 299 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

---------

Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
Co-authored-by: Oğuz Han <h4n.3545@gmail.com>
Co-authored-by: Coisini-H <2972186196@qq.com>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
2024-04-10 22:05:31 +08:00
Steven
a376dc4dd6 chore: fix linter 2024-04-10 22:04:02 +08:00
Dubzer
8ee56bd29f feat: add code wrapping option on mobile devices (#3196)
* feat: add code wrapping option on mobile devices

* Minor changes

* oopsie
2024-04-10 22:03:47 +08:00
Steven
71c39ed554 chore: update workspace setting definition 2024-04-10 21:15:55 +08:00
Steven
c93b1efbae chore: update workspace setting store 2024-04-10 20:05:17 +08:00
Steven
58ae3217ff chore: update auth callback messages 2024-04-09 21:23:17 +08:00
Steven
6028838f03 chore: update component name 2024-04-09 20:51:09 +08:00
Michael
000b3a7a2c chore: update i18n with weblate (#3195)
Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-04-09 20:29:26 +08:00
Steven
bbdd40b2b0 chore: add placeholder to comment editor 2024-04-09 20:01:19 +08:00
imikod
8356ebc46b chore: improve localization (#3190)
* improve localization

* follow suggestion
2024-04-09 16:11:32 +08:00
Steven
fc95876617 chore: update memo creator display 2024-04-09 09:05:34 +08:00
Steven
b1e196bb4f fix: delete memo reaction 2024-04-08 20:52:46 +08:00
Steven
6d10251cbd feat: impl list syntax auto complete to editor 2024-04-08 20:42:53 +08:00
Steven
436a6cb084 chore: fix text truncate 2024-04-08 19:20:16 +08:00
imikod
90ed908d2e fix: set navigation icons to not shrink (#3186) 2024-04-08 10:16:52 +08:00
Steven
756ab71302 chore: tweak truncate styles 2024-04-07 23:30:28 +08:00
Steven
a6b09aa5b1 chore: add confirm dialog for disable password login setting 2024-04-07 23:22:59 +08:00
Steven
ff81ea602d chore: tweak error message 2024-04-07 22:35:02 +08:00
Steven
8101a5e0b1 chore: add origin flag to config cors 2024-04-07 22:15:15 +08:00
Michael
b5893aa60b chore: update i18n with Weblate (#3183)
* Translated using Weblate (French)

Currently translated at 100.0% (295 of 295 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (295 of 295 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/tr/

---------

Co-authored-by: Ezmana <ezmana.land@gmail.com>
Co-authored-by: Oğuz Han <h4n.3545@gmail.com>
2024-04-07 10:13:12 +08:00
Steven
bedf3b3025 chore: tweak embedded memo styles 2024-04-02 23:32:43 +08:00
The Anh Nguyen
09ffa7b818 chore: fix memo link (#3173) 2024-04-02 07:13:58 +08:00
Steven
ba7dbc1fca chore: fix task node click handler 2024-04-01 21:38:18 +08:00
Steven
15ef57589e chore: go mod tidy 2024-04-01 21:35:58 +08:00
stayweek
5aa633948e chore: use the built-in slices library (#3172)
Signed-off-by: stayweek <jiangtengtao@outlook.com>
2024-04-01 17:16:29 +08:00
Steven
b1297f5d01 chore: tweak linter 2024-04-01 00:41:25 +08:00
Steven
33e9b13665 chore: tweak linter 2024-04-01 00:34:51 +08:00
Steven
b79f626a74 chore: fix memo comment inbox 2024-04-01 00:26:46 +08:00
Michael
ec35a42fb5 chore: update i18n with Weblate (#3168)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (295 of 295 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (295 of 295 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (290 of 295 strings)

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

---------

Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
Co-authored-by: Coisini-H <2972186196@qq.com>
2024-04-01 00:07:03 +08:00
Michael
98c9ab70e2 chore: update translation files (#3166)
Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-03-30 22:54:38 +08:00
Steven
edc7645086 chore: fix linter 2024-03-30 15:01:19 +08:00
Steven
cbebbca7d6 chore: add idp definition 2024-03-30 14:58:47 +08:00
Steven
4d62ed46cc chore: tweak words 2024-03-30 14:14:42 +08:00
Steven
bb10bb200c chore: implement search random memos 2024-03-30 13:50:18 +08:00
Steven
03c93785f4 chore: implement list random users 2024-03-30 13:29:48 +08:00
The Anh Nguyen
2fe6d606ec feat: handle restore memo when is in archived memo detail page (#3165) 2024-03-30 13:06:08 +08:00
Steven
c8baeb86ec chore: fix memo links 2024-03-30 13:04:40 +08:00
The Anh Nguyen
9a88e00df0 fix: link embedded content to uid instead of name (#3164) 2024-03-30 12:30:09 +08:00
Steven
54a3c25ebd chore: update memo resource name definition 2024-03-30 08:26:28 +08:00
Steven
d84a88e805 chore: fix linter 2024-03-30 00:05:19 +08:00
Steven
5ab845d92e chore: fix linter 2024-03-30 00:02:34 +08:00
Steven
4f7a6cd6cc chore: fix default logo url 2024-03-29 23:12:30 +08:00
Steven
c7aaf791e6 chore: tweak props naming 2024-03-29 22:49:19 +08:00
Michael
239348c403 chore: update i18n with Weblate (#3162)
* Translated using Weblate (Russian)

Currently translated at 85.1% (252 of 296 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ru/

* Translated using Weblate (Vietnamese)

Currently translated at 91.8% (272 of 296 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/vi/

---------

Co-authored-by: LibreTranslate <noreply-mt-libretranslate@weblate.org>
Co-authored-by: The Anh Nguyen <anhntdev0@gmail.com>
2024-03-29 21:32:55 +08:00
Michael
9f803aa9bb chore: translated using Weblate (Russian) (#3161)
Translated using Weblate (Russian)

Currently translated at 85.1% (252 of 296 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ru/

Co-authored-by: xyznetsov <invisiblesun2016@yandex.ru>
2024-03-29 21:29:49 +08:00
Steven
200a0d3e2b fix: memo relations 2024-03-29 21:27:38 +08:00
Steven
ea6628066d chore: update sidebar components 2024-03-29 09:16:55 +08:00
Steven
526f46807e chore: tweak user list display 2024-03-29 08:37:07 +08:00
Michael
bfaf06582c chore: translated using Weblate (Portuguese (Brazil)) (#3157)
Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (296 of 296 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
2024-03-29 08:20:56 +08:00
Steven
7331c598df fix: edit account dialog fields 2024-03-29 08:19:32 +08:00
The Anh Nguyen
f7a445ac8b chore: update Vietnamese translation (#3155) 2024-03-29 08:07:02 +08:00
Steven
90679cc33a chore: add explore sidebar 2024-03-29 00:01:45 +08:00
Steven
192ee7acc0 chore: fix memo link 2024-03-28 22:53:22 +08:00
Michael
7d94256a2a chore: update i18n with Weblate (#3152)
* Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (291 of 296 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (291 of 296 strings)

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

---------

Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Co-authored-by: Coisini-H <2972186196@qq.com>
2024-03-28 22:51:50 +08:00
Michael
aed4c1392d chore: update i18n with Weblate (#3151)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (296 of 296 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (291 of 296 strings)

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

---------

Co-authored-by: Coisini-H <2972186196@qq.com>
2024-03-28 22:48:32 +08:00
Paul
3605efc4d1 chore: removed sanitization logic (#3147)
* removed sanitization logic

* to avoid lint err
2024-03-28 22:44:44 +08:00
Dubzer
a0846c2818 feat: add shortcut to edit the previous memo (#3122)
* Add shortcut to edit the previous memo

* Fix compilation

* Update web/src/components/MemoEditor/index.tsx

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

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-03-27 21:46:29 +08:00
Steven
2ebea4dba9 chore: fix workspace profile api 2024-03-27 18:51:11 +08:00
Steven
16cfef32d6 chore: tweak statistics translation 2024-03-26 23:49:37 +08:00
Steven
f637deacfc chore: tweak tag list 2024-03-26 23:43:31 +08:00
Steven
a07805907b chore: fix action run 2024-03-26 22:46:17 +08:00
Steven
2e87da4927 chore: update frontend dependencies 2024-03-26 22:35:11 +08:00
Steven
d460e6bf41 fix: link node 2024-03-25 23:59:19 +08:00
Steven
e703b4f70d fix: resource v1 definition 2024-03-24 22:35:10 +08:00
Steven
be525fa3df chore: fix migration script 2024-03-23 01:03:11 +08:00
Steven
d1e8af48c0 chore: tweak memo view styles 2024-03-23 00:58:15 +08:00
Steven
005305cd9a chore: update relative time display 2024-03-22 08:12:08 +08:00
Steven
8fd1dff484 chore: update migration scripts 2024-03-21 22:24:39 +08:00
Steven
c5fa4fe304 chore: tweak imports order 2024-03-21 21:44:43 +08:00
Steven
8f37d7490c chore: fix linter 2024-03-21 21:42:44 +08:00
Steven
18d16abdb5 chore: update workspace service 2024-03-21 21:39:34 +08:00
Steven
1d83c68cb5 chore: update type definitions 2024-03-21 08:12:38 +08:00
Michael
861a1e4c9b chore: update i18n with Weblate (#3115)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (293 of 293 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (293 of 293 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

---------

Co-authored-by: Coisini-H <2972186196@qq.com>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
2024-03-20 21:42:08 +08:00
Steven
26e5b70483 chore: upgrade frontend dependencies 2024-03-20 21:32:06 +08:00
Steven
7c5261b5d2 chore: tweak resource definition 2024-03-20 21:17:04 +08:00
Steven
7cc8b951a3 refactor: update resource id naming 2024-03-20 20:39:16 +08:00
Steven
a3a4e37cb0 chore: update memo content styles 2024-03-20 09:07:54 +08:00
Steven
76c936357b chore: fix resource link 2024-03-20 08:51:32 +08:00
Steven
6db427e396 chore: update acl list 2024-03-20 08:18:43 +08:00
Steven
0a0ccbf6e1 chore: update acl list 2024-03-20 08:12:46 +08:00
Steven
96a7d0eb25 chore: tweak line height 2024-03-20 00:40:46 +08:00
Steven
ed89cb8310 chore: update memo relation definition 2024-03-19 21:53:44 +08:00
Steven
722e356044 chore: update memo store 2024-03-19 20:55:33 +08:00
Steven
efb15a0453 chore: tweak naming 2024-03-18 23:41:57 +08:00
Steven
5f2d6b22be chore: tweak memo structure 2024-03-18 23:23:53 +08:00
Steven
2dc8ed773c chore: tweak linter 2024-03-18 19:57:53 +08:00
Steven
86db6d0254 chore: tweak user api definition 2024-03-18 12:56:52 +08:00
Steven
f3fb5e0c60 chore: tweak route enum 2024-03-18 12:07:44 +08:00
Steven
1e43c8d84b chore: disallow update workspace setting in demo mode 2024-03-17 19:18:45 +08:00
Steven
cf207df672 chore: tweak link styles 2024-03-17 18:28:37 +08:00
Michael
5dd1251d1e chore: update i18n with Weblate (#3109)
* Translated using Weblate (Japanese)

Currently translated at 95.2% (279 of 293 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ja/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (293 of 293 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

---------

Co-authored-by: Somme4096 <somme4096@gmail.com>
Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
2024-03-17 18:22:22 +08:00
sunxunle
ea104a5e54 chore: fix typo (#3106) 2024-03-17 09:52:04 +08:00
Steven
335a0312f2 chore: tweak link styles 2024-03-16 23:35:36 +08:00
Mehad Nadeem
9c1e2f8137 feat: implemented link previews (frontend files) (#3074)
* feat: implmented link previews (frontend files)

* chore: updated frontend side for Link Previews

* chore: updated frontend gen types with the renamed (server) service file

* fix: passing errors

* chore: switched to using generated type instead of separate fields

* fix: passing linter error

* chore: updated Link.tsx

* chore: using `useResponsiveWidth` to render for different devices

* chore: refactored Link.tsx
2024-03-16 22:51:16 +08:00
Steven
14479347d8 chore: remove demo checks 2024-03-15 09:05:02 +08:00
Steven
0f48cfbb4e chore: tweak padding styles 2024-03-15 08:52:11 +08:00
Steven
606a30640d fix: check username in signup 2024-03-15 08:37:58 +08:00
Steven
ab136e3310 chore: tweak styles 2024-03-15 00:12:22 +08:00
Michael
21af2a004d chore: translated using Weblate (French) (#3099)
Translated using Weblate (French)

Currently translated at 100.0% (293 of 293 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/fr/

Co-authored-by: Ezmana <ezmana.land@gmail.com>
2024-03-14 17:09:29 +08:00
Steven
115b5551b3 chore: tweak default avatar 2024-03-14 09:11:57 +08:00
Steven
15be18fa85 chore: update compace mode handler 2024-03-14 09:07:18 +08:00
Steven
9ce381abb9 chore: tweak user profile 2024-03-14 00:13:56 +08:00
Steven
2516431b20 chore: tweak memo content styles 2024-03-14 00:01:33 +08:00
Steven
ded4da07a3 feat: use @github/relative-time-element to display time 2024-03-13 23:47:34 +08:00
Steven
e795149186 chore: tweak memo view display 2024-03-13 22:30:59 +08:00
Steven
8fe6874b1b chore: add description field to user 2024-03-13 21:24:16 +08:00
boojack
7c1510e7a9 chore: update i18n with Weblate (#3090)
* Translated using Weblate (Spanish)

Currently translated at 95.4% (293 of 307 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/es/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (307 of 307 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

---------

Co-authored-by: Hisiste <aditooliva@gmail.com>
Co-authored-by: keocheung <keocheung@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-03-13 20:55:04 +08:00
Steven
e5fc107920 chore: tweak i18n locales 2024-03-13 20:51:19 +08:00
Steven
5d740c3813 chore: update link service 2024-03-13 20:25:27 +08:00
Mehad Nadeem
0abe20df72 feat: implemented link previews (server files) (#3073)
* feat: implmented link previews (server files)

* chore: updated variable name

* chore: renamed service file from `metadata_service.go` to `link_service.go`

* fix: passing errors

* fix: fixed linter warnong about `ctx`
2024-03-13 17:31:53 +08:00
Mehad Nadeem
4d41b68d4c feat: implemented link previews (proto files) (#3072)
* feat: implmented link previews (proto files)

* fix: passing proto linter errors

* chore: renamed MetadataService service in proto file.

* fix: removed unused files

* chore: removed meaningless comments from proto file
2024-03-13 16:52:59 +08:00
Lincoln Nogueira
7c87c1ff74 chore: update build scripts (#3085) 2024-03-13 08:50:56 +08:00
Steven
963c630428 chore: update readme with weblate 2024-03-13 08:25:38 +08:00
Steven
6b6edc3791 chore: update tg message handler 2024-03-12 22:48:53 +08:00
Mario Gómez
425e85f0f9 chore: fix spanish typos (#3083) 2024-03-12 21:48:02 +08:00
huajin tong
8e7f826ae6 chore: fix typo (#3080)
Signed-off-by: thirdkeyword <fliterdashen@gmail.com>
2024-03-12 17:05:49 +08:00
Michael
9ffc1515f4 chore: update i18n with Weblate (#3079)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (307 of 307 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (307 of 307 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/zh_Hans/

---------

Co-authored-by: Matsuri <matsuri@vmoe.info>
Co-authored-by: keocheung <keocheung@outlook.com>
2024-03-12 13:14:56 +08:00
steven
8cdc0c7ffa chore: add delete tag action 2024-03-09 17:26:58 +08:00
zty
999a05307e fix: body overflow styles #3056
Co-authored-by: zty <zty.dev@outlook.com>
2024-03-09 14:09:31 +08:00
steven
ed1954c58c chore: fix check setting value 2024-03-09 12:56:44 +08:00
steven
77bafba682 chore: update gomark.wasm 2024-03-09 12:47:20 +08:00
Michael
1540de2f44 chore: translated using Weblate (Korean) (#3060)
Translated using Weblate (Korean)

Currently translated at 95.1% (292 of 307 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/ko/

Co-authored-by: WonSeok <wonseok@duck.com>
2024-03-09 12:40:42 +08:00
Steven
ed4b48c54f chore: update gomark.wasm 2024-03-08 00:34:34 +08:00
Michael
706e749275 chore: translated using Weblate (Portuguese (Brazil)) (#3050)
Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (307 of 307 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
2024-03-07 11:38:24 +08:00
Steven
845999292a chore: update frontend dependencies 2024-03-06 23:33:00 +08:00
Michael
240da335c1 chore(i18n): update translation files (#3042)
Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-03-05 11:44:51 +08:00
Steven
f9f277695d chore: fix comment amount 2024-03-04 23:37:38 +08:00
Dubzer
6cbfbe9c07 chore: settings page improvements (#3034)
* Don't allow to select text on buttons in settings + refactoring

* Remove redundant useMemo

* Update web/src/pages/Setting.tsx

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

* Rename other constants too

* Rename AdminSection to ADMIN_SECTIONS

---------

Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-03-04 23:33:43 +08:00
Steven
ec206104e5 chore: tweak i18n locale 2024-03-04 23:23:14 +08:00
Steven
342f341b3d chore: ignore import locale error 2024-03-04 13:16:55 +08:00
WonSeok
f46b64a17d fix: check disallow public memo in Telegram(#3036) (#3037) 2024-03-04 11:24:09 +08:00
Steven
7d5f603482 chore: update compact view 2024-03-04 11:21:48 +08:00
Dubzer
f0a521f5b8 feat: improve theming (#3032)
Add color-scheme and dark scrollbar styles
2024-03-04 09:35:40 +08:00
Steven
59314cdf80 chore: add compact mode to memo view 2024-03-04 01:11:34 +08:00
Steven
ac0315334d chore: update i18n locales imports 2024-03-04 00:05:27 +08:00
Steven
ac8a374ebd chore: bump version 2024-03-03 23:45:52 +08:00
Steven
95b02341eb chore: tweak logger 2024-03-03 23:45:32 +08:00
Steven
d34273b186 chore: update memo icons style 2024-03-03 16:41:03 +08:00
Steven
dcfb2b7de2 chore: tweak docs generate path 2024-03-03 15:05:42 +08:00
Steven
ebcf43c997 chore: update gomark.wasm 2024-03-03 14:56:40 +08:00
Steven
cfb50f19aa chore: tweak linter 2024-03-03 14:23:21 +08:00
Steven
e441e3882b chore: update error logger 2024-03-03 14:20:59 +08:00
Steven
c1da87a819 chore: update get request origin 2024-03-03 14:10:48 +08:00
jjaychen
3b089eeae3 fix: ignore "Tab" key down event when is composing in editor(#3026) (#3027) 2024-03-03 13:04:00 +08:00
Steven
9a8a1d017e chore: add comments icon 2024-03-02 11:29:21 +08:00
Steven
7e23ceb242 chore: fix frontend linter 2024-03-01 19:39:24 +08:00
Steven
898b3c3779 chore: update gomark.wasm 2024-03-01 19:31:00 +08:00
Steven
8eb01adb6f chore: go mod tidy 2024-02-29 23:56:20 +08:00
jg0312
621a8d4e20 chore: fix typo in build script's if statement (#3017)
missing 'i' in 'if' statement
2024-02-29 23:55:25 +08:00
Steven
dfa78cac49 chore: update logger 2024-02-29 23:54:43 +08:00
Steven
12f4d3a10d chore: tweak default page size 2024-02-29 23:42:06 +08:00
Steven
00788bd8f8 chore: update actions 2024-02-29 09:21:55 +08:00
Steven
5f2a624c0c chore: tweak imports order 2024-02-29 01:32:59 +08:00
Steven
3e50bee7da chore: move api directory 2024-02-29 01:16:43 +08:00
Steven
1aa75847d6 chore: upgrade gomark 2024-02-29 01:07:53 +08:00
Steven
89a270cede chore: add username to cache key 2024-02-28 00:34:55 +08:00
Xylit
5526355621 fix: dark mode tag selection and suggestion (#3004)
* fix: use correct higlight color on selecting a tag in darkmode

* fix: take scrolling into account in tab suggestion

Issue: When editing a long memo and editing a tag somewhere where the user has
to scroll down to, the tag suggestions would be out of place (too far down)
because the scrolling wouldn't be taken into account.

Fix: Substract the suggestions div by the scroll amount.

* fix: don't show tag suggestion when on '#'

Fixes this issue like this:
1. Text #tag text
        ^ backspace here
2. Text#tag text
       ^ space
3. Text #tag text
        ^ tag suggestion opens
2024-02-27 09:16:56 +08:00
ercd
1ae9bf23a0 fix: telegram callback query handler dereferencing nil pointer if memo not found (#3003)
* fix: telegram callback query handler dereferencing nil pointer if memo not found

* chore: add an answer to callback query if memo not found
2024-02-27 00:03:08 +08:00
Zhiyuan Zheng
51a7934616 fix: firefox/safari to redirect to /explore in HomeLayout (#3001) 2024-02-26 13:38:24 +08:00
Mr. Lin
8d08cfe1c9 fix: incorrectly checking whether registration is allowed in the login v1 api (#2998)
fix: Incorrectly checking whether registration is allowed in the login interface, resulting in the inability to log in through apiv1
2024-02-26 13:36:17 +08:00
Steven
b0f52ade7a chore: remove metric service 2024-02-24 23:32:39 +08:00
Steven
222d04fb22 chore: add fuse to get tag suggestions 2024-02-24 11:02:46 +08:00
Steven
68468927dd chore: tweak setting styles 2024-02-24 11:01:57 +08:00
Steven
dfe29ec766 chore: tweak route layout 2024-02-24 10:18:53 +08:00
Steven
db56e1b575 chore: fix user banner dropdown 2024-02-23 23:47:26 +08:00
Steven
5b92ac1775 chore: fix setting migrator 2024-02-23 09:16:34 +08:00
Steven
f2eb9f1b8f chore: fix get workspace setting 2024-02-23 09:11:39 +08:00
Steven
e602aeecc1 fix: update workspace general setting 2024-02-23 09:08:49 +08:00
Steven
ce133ad69b chore: retire unused workspace settings 2024-02-23 08:40:33 +08:00
Steven
e585578553 chore: tweak readme 2024-02-23 08:25:54 +08:00
Steven
4d9c929c32 chore: upgrade gomark 2024-02-23 08:22:39 +08:00
Steven
39bf850591 fix: embed memos callback 2024-02-22 23:17:15 +08:00
Steven
9cd835b979 chore: upgrade gomark wasm 2024-02-22 23:11:32 +08:00
Steven
0afdbe3332 chore: remove animation of spoiler 2024-02-22 19:25:24 +08:00
Steven
6b14d87521 chore: fix linter 2024-02-21 23:45:12 +08:00
Steven
51d58d3982 chore: add workspace setting migrator 2024-02-21 23:43:18 +08:00
Steven
4378816e44 chore: tweak i18n 2024-02-21 23:02:18 +08:00
Steven
e7bbd850b2 chore: tweak spoiler animation 2024-02-21 21:31:34 +08:00
Steven
c6162d3f38 chore: update dependencies 2024-02-21 21:24:31 +08:00
Steven
ce32206677 chore: remove unused system setting 2024-02-21 21:15:28 +08:00
Steven
50a3af3b29 chore: fix get general setting 2024-02-21 20:44:35 +08:00
Steven
80b64c02fd chore: tweak workspace setting seeds 2024-02-21 20:25:25 +08:00
Steven
13b911ebf0 chore: add spoiler node 2024-02-20 23:55:54 +08:00
Steven
4a6da91719 chore: fix serve frontend flag 2024-02-20 23:24:13 +08:00
Steven
fa62e8b59a chore: tweak linter errors 2024-02-20 23:07:42 +08:00
Steven
8e11826db1 chore: update workspace setting service 2024-02-20 23:02:01 +08:00
David Nguyen
e6d0c00cf6 chore: update vi.json (#2980) 2024-02-20 17:57:06 +08:00
Kristián
03d67d5a00 feat: add mermaid support in codeblock (#2971) 2024-02-19 15:10:58 +08:00
Kazuki H
a86117f613 feat: add new line if the cursor is on a character when adding a tag (#2960) 2024-02-19 08:54:47 +08:00
Søm
fc1a2cf2fc chore: update ja.json (#2966) 2024-02-17 09:31:52 +08:00
Steven
d22b772232 chore: add memo actions to memo detail page 2024-02-15 11:16:51 +08:00
Steven
f1ec5775a7 chore: update inbox props 2024-02-14 09:44:35 +08:00
Bryan
4aa4417d91 chore: allow all 20x response status code in webhook (#2947) 2024-02-13 09:30:48 +08:00
Steven
606e574e19 chore: update enum type 2024-02-13 09:30:28 +08:00
Brilliant Hanabi
ebe3678288 feat: add visibility select in ShareMemoDialog (#2941)
In ShareMemoDialog, user can change the visibility
of the memo, so that the memo can be set to public
to be viewed by anyone with the link.
2024-02-13 09:28:16 +08:00
Steven
b3ca9969c4 chore: tweak linter 2024-02-09 22:18:55 +08:00
Steven
3dddd3ec4c chore: tweak reaction store 2024-02-09 21:59:45 +08:00
Brilliant Hanabi
81aa9b107f feat: add notice when sharing private links in MemoDetail (#2942) 2024-02-09 09:30:01 +08:00
Steven
60efd3ac32 chore: tweak memo view 2024-02-08 22:52:49 +08:00
Steven
4081a6f5ad chore: add more reactions 2024-02-08 21:20:51 +08:00
Lincoln Nogueira
334e489867 chore: improve docker-compose.dev (#2938) 2024-02-08 16:28:43 +08:00
Steven
c7822515a1 chore: tweak view checks 2024-02-08 13:37:38 +08:00
Steven
d86f0bac8c chore: implement reaction frontend 2024-02-08 13:25:15 +08:00
Steven
e5f244cb50 chore: fix tests 2024-02-08 11:58:23 +08:00
Steven
3a5bc82d39 chore: implement reaction service 2024-02-08 11:54:59 +08:00
Steven
a4fa67cd18 chore: update dependencies 2024-02-08 08:13:42 +08:00
Steven
43a2d6ce09 chore: tweak user setting 2024-02-08 08:06:55 +08:00
Mehad Nadeem
d2434111b4 chore: impl compact mode setting (#2935)
* chore: backend/DB related files for compact view setting.

* fix: passing lint errors

* fix2: passing linter errors
2024-02-08 08:05:56 +08:00
Steven
559e427c50 chore: implement reaction store 2024-02-07 23:40:23 +08:00
Steven
99568236a3 chore: run buf generate 2024-02-07 21:51:30 +08:00
Mehad Nadeem
06fb2174c3 feat: add compact mode setting (Proto) (#2934)
* chore: proto related files for compact view setting.

* fix: pasing lint errors
2024-02-07 21:45:11 +08:00
Kazuki H
5ac17fc012 fix: displaying archived memos (#2933)
* fix: web: Archived: Show displayTime instead of updateTime

Archiving a memo is considered "updating" it, so the time it was
archived will be displayed, instead of the time it was created.

* fix: web: Archived: Add an option to fetch more memos

Just like on other pages, add a button to fetch more memos. Otherwise,
the user would only be able to load the latest 10 memos, as defined in
DEFAULT_MEMO_LIMIT.
2024-02-07 17:53:23 +08:00
Steven
a76b86f18a chore: fix highlight code 2024-02-06 20:59:17 +08:00
Steven
ded8001735 chore: fix v2 routes 2024-02-06 20:55:27 +08:00
Steven
185ec2ad2a chore: update inbox service 2024-02-06 19:46:25 +08:00
Steven
6b59c7670c chore: fix linter warning 2024-02-05 23:32:01 +08:00
Steven
434ef44f8c chore: add cookie builder 2024-02-05 23:28:29 +08:00
Steven
46ea16ef7e chore: fix cookie attrs 2024-02-05 22:14:58 +08:00
Steven
8f15e8773a chore: update cookie attrs 2024-02-05 21:42:23 +08:00
Steven
25efc33b24 chore: tweak timeline styles 2024-02-05 21:37:43 +08:00
Steven
ba460382b0 chore: remove type-gen script 2024-02-05 20:42:01 +08:00
Steven
e35225ff24 chore: fix resource url 2024-02-05 19:35:23 +08:00
Steven
397a7f00ef chore: add postinstall script 2024-02-05 19:29:47 +08:00
Steven
06eff151e7 chore: tweak memo find builder 2024-02-05 06:40:55 +08:00
Steven
c30d7ab8f3 chore: update cors middleware 2024-02-05 06:10:10 +08:00
Steven
ab4a670bec chore: add env example 2024-02-05 00:33:07 +08:00
Steven
ce663efc14 chore: update cookie attrs 2024-02-05 00:11:36 +08:00
Steven
9e72432f19 chore: tweak cookie attrs 2024-02-05 00:10:54 +08:00
Steven
b056c59dea chore: add vercel.json 2024-02-05 00:01:31 +08:00
Steven
15c90871d9 chore: update request base url 2024-02-04 23:48:26 +08:00
Steven
be899cd027 chore: update eslint config 2024-02-04 22:52:47 +08:00
Steven
8773a3d2c1 chore: tweak assets folder 2024-02-04 22:45:51 +08:00
Steven
d2603ee67b chore: upgrade frontend dependencies 2024-02-04 21:36:11 +08:00
Steven
c92507728a chore: tweak filter checks 2024-02-04 21:07:14 +08:00
Steven
eb4f7b47b7 chore: update memo find builder 2024-02-04 20:54:17 +08:00
Steven
1e07b70d23 chore: fix export memos 2024-02-04 20:20:14 +08:00
Athurg Gooth
b8a9783db5 fix: signin error notification is not shown (#2908)
fix signin error notification is not shown
2024-02-04 14:25:51 +08:00
Ikko Eltociear Ashimine
82e72813f9 chore: fix typo in About.tsx (#2899) 2024-02-04 08:33:13 +08:00
Steven
57510ddee5 chore: update readme with used resources 2024-02-03 23:42:24 +08:00
Steven
00c47a0673 chore: fix menu z-index 2024-02-03 23:17:03 +08:00
Steven
374f3f7d96 chore: fix filter initial state 2024-02-03 22:03:56 +08:00
Steven
8340e6b247 chore: upgrade gomark 2024-02-03 21:52:54 +08:00
Steven
7f5148d490 chore: clean dropdown 2024-02-01 21:56:08 +08:00
Steven
c522e1450a chore: update codeblock style 2024-02-01 21:38:28 +08:00
Steven
c342c464a2 chore: update services comment 2024-02-01 21:26:09 +08:00
Steven
f6f193af2d chore: format proto 2024-02-01 21:15:56 +08:00
Steven
dd06278692 feat: add batch upsert tags 2024-02-01 21:13:42 +08:00
Steven
fdd17ce849 chore: update memo content key 2024-02-01 20:40:43 +08:00
Elliot Chen
7cd3fcbc61 fix: wrong order of the timeline in the resource page & add webhook when create memos using Telegram bot (#2886)
* fix: wrong order in resource page timeline

* feat: add webhook when create memos using Telegram bot

* rename variables and fix typos for static checks
2024-02-01 20:24:58 +08:00
Steven
e78311b3af fix: initial gomark wasm 2024-02-01 19:39:41 +08:00
Steven
e3afad74ce fix: update initial wasm 2024-02-01 19:27:54 +08:00
Steven
554f93eccc fix: move initial wasm into app 2024-02-01 19:06:44 +08:00
Steven
79227021f5 chore: bump version 2024-01-31 23:08:33 +08:00
Steven
b4f2a3bd14 chore: remove migrator 2024-01-31 22:58:43 +08:00
Steven
0b4914d880 chore: update generated node types 2024-01-31 22:42:51 +08:00
Steven
2f0b0e0071 chore: remove node definition 2024-01-31 22:32:09 +08:00
Steven
8ce6a32aac feat: use gomark wasm in frontend 2024-01-31 22:25:24 +08:00
Wen Sun
3158c4b8b5 fix: role error in api/v2 when the first user registers (#2875)
Fix role error in api/v2 when the first user registers
2024-01-31 19:55:52 +08:00
Steven
30ae4140f3 chore: update gomark source 2024-01-31 19:01:08 +08:00
Lincoln Nogueira
279cba0e6b chore: greatly speed up migrator and lower memory usage (#2874)
* chore: add en-GB language

* chore: remove en-GB contents

* chore: prevent visitors from breaking demo
- prevent disabling password login
- prevent updating `memos-demo` user
- prevent setting additional style
- prevent setting additional script
- add some error feedback to system settings UI

* Revert "chore: add en-GB language"

This reverts commit 2716377b04.

* chore: speed-up migrator and lower memory usage
- remove all Store indirections
- query database directly with prepared statements

* chore: fix golangci-lint warnings
2024-01-31 16:45:21 +08:00
Lincoln Nogueira
52539fc130 chore: prevent visitors from breaking demo (#2869)
* chore: add en-GB language

* chore: remove en-GB contents

* chore: prevent visitors from breaking demo
- prevent disabling password login
- prevent updating `memos-demo` user
- prevent setting additional style
- prevent setting additional script
- add some error feedback to system settings UI

* Revert "chore: add en-GB language"

This reverts commit 2716377b04.
2024-01-31 13:16:31 +08:00
Steven
49e3eb107c chore: update gomark wasm 2024-01-31 00:25:01 +08:00
Lincoln Nogueira
e7d5dfe515 chore: add en-GB language (#2865)
* chore: add en-GB language

* chore: remove en-GB contents
2024-01-30 23:39:13 +08:00
Steven
28c7a75ea2 chore: fix import nodes 2024-01-30 22:15:05 +08:00
Steven
59d69a05fa feat: initial gomark wasm importer 2024-01-30 22:12:44 +08:00
Steven
ad2d492dec chore: move preview memo content 2024-01-30 22:10:17 +08:00
Elliot Chen
bee6f278ba fix: the same-storage check in the new pre-sign feature (#2860)
* fix: error check for the same oss-storage

* fix: conflict error2 variable in code refactor in s3.go

* chore: rename endpointUrl to endpointURL
2024-01-30 19:07:16 +08:00
Brilliant Hanabi
1bad0543d0 feat: add notice when sharing private links (#2809)
Co-authored-by: boojack <stevenlgtm@gmail.com>
2024-01-30 19:06:30 +08:00
Noah Alderton
73337331cb feat: export all user Memos as a .zip of Markdown files (#2854)
* Add gRPC Memos Export

* Update code style

* Add URL.revokeObjectURL

* Rename protobuf and ESLint fix

* Change MemosExport to ExportMemos
2024-01-30 16:42:54 +08:00
Wen Sun
50f7f131ea fix: month grouping error in timeline page (#2861) 2024-01-30 07:56:03 +08:00
Steven
a16bde23f7 chore: tweak variable name 2024-01-29 23:15:47 +08:00
Steven
c5a5f67fdb refactor: migrate auth service 2024-01-29 23:12:02 +08:00
Steven
de8db63811 chore: rename workspace setting service 2024-01-29 22:43:40 +08:00
Steven
dd9ee44a1f docs: regenerate swagger 2024-01-29 22:05:33 +08:00
Aleksandr Baryshnikov
fa17dce046 feat: pre-signed URL for S3 storage (#2855)
Adds automatically background refresh of all external links if they are belongs to the current blob (S3) storage. The feature is disabled by default in order to keep backward compatibility.

The background go-routine spawns once during startup and periodically signs and updates external links if that links belongs to current S3 storage.

The original idea was to sign external links on-demand, however, with current architecture it will require duplicated code in plenty of places. If do it, the changes will be quite invasive and in the end pointless: I believe, the architecture will be eventually updated to give more scalable way for pluggable storage. For example - Upload/Download interface without hard dependency on external link. There are stubs already, but I don't feel confident enough to change significant part of the application architecture.
2024-01-29 21:12:29 +08:00
Steven
cbcec80c5d chore: fix import order 2024-01-29 21:08:30 +08:00
Steven
2b7bd47b44 fix: rss routes 2024-01-29 21:04:35 +08:00
Steven
54c5039db3 chore: fix golang linter 2024-01-29 19:17:25 +08:00
Steven
af646ce2de refactor: move gomark 2024-01-29 19:14:46 +08:00
Steven
f4ac7ff529 chore: update memo resource url 2024-01-28 23:02:38 +08:00
Steven
55ecdae509 chore: fix auto link matcher 2024-01-28 22:13:19 +08:00
Steven
ef73299340 chore: update resource name migrator 2024-01-28 21:40:24 +08:00
Steven
8c6292925e chore: update code block styles 2024-01-28 15:41:11 +08:00
Steven
f05a89315c chore: fix list memos 2024-01-28 08:38:29 +08:00
Steven
a4452d8a2f chore: update linter rules 2024-01-28 08:17:11 +08:00
Steven
5e74394643 chore: add resource name migrator 2024-01-28 07:58:53 +08:00
Steven
f4e722c516 chore: remove latest tag 2024-01-28 07:46:08 +08:00
Steven
12275c6a34 chore: fix linter warning 2024-01-28 07:38:01 +08:00
Steven
21ef5a9bc0 chore: tweak workspace service 2024-01-28 07:35:42 +08:00
Steven
87b23940a6 chore: upgrade backend dependencies 2024-01-28 07:13:11 +08:00
Steven
11dd23f59b chore: tweak link checks 2024-01-28 07:04:35 +08:00
Lincoln Nogueira
887903b66b feat: add buf plugin to generate openapiv2 spec (#2843) 2024-01-28 06:16:37 +08:00
Steven
309fab222e chore: implement nested blockquote 2024-01-27 21:38:07 +08:00
Steven
1dc4f02b64 chore: update memo requests 2024-01-27 20:35:48 +08:00
Steven
8db90a040c chore: remove unused dependencies 2024-01-27 20:20:36 +08:00
Steven
932f636d84 chore: update codeblock renderer 2024-01-27 19:09:10 +08:00
Steven
ed32b20c9e chore: update frontend dependencies 2024-01-27 17:28:06 +08:00
Steven
10d709c167 chore: fix highlight cursor 2024-01-27 12:35:01 +08:00
Steven
8455114eef chore: fix list memos request 2024-01-27 11:17:59 +08:00
Steven
c26109cd36 chore: update list memos request 2024-01-27 11:14:17 +08:00
Steven
4b223c1e4c chore: update collapse sidebar 2024-01-27 05:27:42 +08:00
Steven
b9cbe6626f chore: update tag rename 2024-01-27 05:26:32 +08:00
Steven
566171783d chore: tweak embedded memo style 2024-01-26 23:33:40 +08:00
Steven
7edb3598ea chore: update default request limit 2024-01-26 23:03:10 +08:00
Steven
bc2d2d0cde feat: support set embedded content in UI 2024-01-26 22:51:57 +08:00
Steven
e1977df14b chore: remove check underscores 2024-01-26 21:23:36 +08:00
Wei Zhang
ddc89029b7 fix: use mysql to parse dsn (#2838)
Signed-off-by: Zhang Wei <kweizh@gmail.com>
2024-01-26 13:43:48 +08:00
Steven
f8b9a83d4a chore: tweak default value 2024-01-26 09:22:47 +08:00
Steven
2f16b7065a chore: tweak scripts 2024-01-26 09:15:27 +08:00
Steven
e5ff1829a5 chore: add Hungarian locale 2024-01-26 08:44:12 +08:00
Steven
d7889d9903 chore: tweak url filters 2024-01-26 08:39:53 +08:00
Steven
db3457e081 chore: bump version 2024-01-26 08:30:22 +08:00
Steven
4f2b00b4f3 chore: add migration scripts 2024-01-26 08:29:11 +08:00
Steven
79558028c0 feat: implement rename tag 2024-01-25 23:09:35 +08:00
Steven
70d1301dc3 chore: use filter in url params 2024-01-25 20:05:47 +08:00
Steven
6d5e1def76 chore: update member section 2024-01-25 19:49:39 +08:00
boojack
a5bc2d0ed6 chore: update i18n from Weblate (#2832)
* Added translation using Weblate (Hungarian)

* Translated using Weblate (Hungarian)

Currently translated at 0.3% (1 of 317 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/hu/

* Translated using Weblate (Hungarian)

Currently translated at 71.2% (226 of 317 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/hu/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (316 of 316 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (316 of 316 strings)

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

---------

Co-authored-by: Vermunds <com.github@weylus.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
Co-authored-by: ti777777 <ti12272198686@yahoo.com.tw>
2024-01-25 19:36:02 +08:00
Steven
08ac60cc70 chore: update memo relation dialog 2024-01-25 08:59:10 +08:00
Mudkip
f654d3c90e fix: encode filename when using url prefix for resources (#2829)
* fix: encode filename when using url prefix for resources

* fix: only encode the last parts of filename

* fix: encode all parts in filepath
2024-01-24 11:28:26 +08:00
Steven
1b69b73eb9 chore: update calendar styles 2024-01-23 23:15:54 +08:00
Steven
3dbb254aeb chore: update referenced memo title from params 2024-01-23 21:54:15 +08:00
Steven
fdb1779a59 chore: implement referenced content renderer 2024-01-23 21:47:17 +08:00
Steven
a316e239ce chore: implement referenced content node 2024-01-23 21:40:59 +08:00
Steven
d7f02b94e5 chore: fix linter 2024-01-23 21:27:05 +08:00
Steven
d165d87288 refactor: markdown parser matchers 2024-01-23 21:23:40 +08:00
Steven
bf905bba86 chore: remove unused date picker 2024-01-22 22:50:27 +08:00
Brilliant Hanabi
3a129d5cfb fix: avoid making memos public when disabled (#2816)
* fix: avoid making memos public when disabled in v2

* fix: avoid making memos public when disabled in v1
2024-01-22 20:51:33 +08:00
ti777777
024a818e91 chore: update zh-Hant (#2815) 2024-01-21 23:50:44 +08:00
Steven
54a24833a7 chore: fix resource seeds 2024-01-21 21:36:31 +08:00
Steven
a620d140c5 chore: update embedded content renderers 2024-01-21 21:27:04 +08:00
Steven
370054e040 chore: implement collapsed navigation 2024-01-21 21:02:55 +08:00
Steven
fae0b4e900 chore: update memo relation style 2024-01-21 11:22:25 +08:00
Steven
c38404b5d5 chore: tweak get memo by name 2024-01-21 10:57:53 +08:00
Steven
4d48f50815 chore: use resource name in frontend 2024-01-21 10:55:49 +08:00
Steven
582cc6609c feat: add user-defined id to resource 2024-01-21 10:49:30 +08:00
Steven
40bd75c725 fix: create memo with resource name 2024-01-21 10:33:31 +08:00
Steven
b2fc3076f6 chore: update memo store 2024-01-21 01:23:55 +08:00
Steven
288527914b chore: migrate memo route 2024-01-21 01:23:44 +08:00
Steven
a2aea3747c chore: remove server tests 2024-01-20 23:51:24 +08:00
Steven
8382354ef7 feat: add user-defined name to memo 2024-01-20 23:48:35 +08:00
Steven
264e6e6e9c chore: tweak file name 2024-01-20 23:47:04 +08:00
Steven
eb72609ea3 chore: update memo editor cache key 2024-01-20 20:52:11 +08:00
Brilliant Hanabi
776785ac90 chore: update zh-Hans & zh-Hant translations (#2804) 2024-01-20 20:27:20 +08:00
Steven
d5f874e185 chore: handle resource not found 2024-01-20 12:47:43 +08:00
Steven
89d940d9b7 feat: implement params field for embedded content node 2024-01-20 12:41:08 +08:00
Mark Zhao
bd1cf62761 feat: enable iframe rendering in markdown code block (#2799)
* enable iframe rendering in code block

* fix eslint issue
2024-01-20 11:36:45 +08:00
Steven
196facfacd feat: implement embedded resource renderer 2024-01-20 09:17:31 +08:00
Steven
afe75fd9f2 chore: fix tokens split tests 2024-01-20 02:09:33 +08:00
Steven
8a34013558 feat: implement embedded memo renderer 2024-01-20 01:56:10 +08:00
Steven
67f5ac3657 feat: implement subscript and superscript renderer 2024-01-19 23:10:16 +08:00
Steven
7236552b6c feat: implement subscript and superscript parsers 2024-01-19 23:06:22 +08:00
Steven
1f5899d238 chore: update dependencies 2024-01-19 19:20:59 +08:00
Wen Sun
ec4884ea04 fix: incorrect timeline month display (#2792)
Fix incorrect timeline month display
2024-01-19 18:07:08 +08:00
Elliot Chen
2e0619b4dc feat: add a webhook action for deleted memos (#2791) 2024-01-19 09:56:00 +08:00
Steven
c9146bc749 chore: update code style 2024-01-19 07:06:28 +08:00
Steven
f5b5bd64bc chore: tweak datetime.ts 2024-01-18 22:18:56 +08:00
Steven
d31d9eb71c chore: remove unused nil checks 2024-01-18 19:23:45 +08:00
Steven
f28b654057 chore: fix setting nil checks 2024-01-18 19:20:48 +08:00
Steven
8738b68a44 chore: tweak readme 2024-01-18 19:06:49 +08:00
Steven
42381fa154 chore: add stable build action 2024-01-18 18:49:06 +08:00
Steven
22427101f8 chore: add stale issue action 2024-01-18 16:18:18 +08:00
Steven
2a4ebf5774 chore: add search bar in archived page 2024-01-18 15:28:37 +08:00
Steven
5172e4df7c chore: create memo visibility when creating 2024-01-18 15:20:22 +08:00
Steven
893dd2c85e chore: add disable filter to renderer context 2024-01-18 14:52:57 +08:00
Steven
d426f89cf0 chore: add time filter to timeline 2024-01-18 14:30:20 +08:00
Steven
7de3de5610 chore: fix go test warning 2024-01-18 11:45:59 +08:00
Steven
2856e66609 chore: fix go test 2024-01-18 11:42:57 +08:00
Steven
354011f994 chore: handle filter in user profile page 2024-01-18 11:38:45 +08:00
Steven
8ed827cd2d chore: update table node delimiter 2024-01-18 11:36:13 +08:00
Steven
05c0aeb789 feat: implement table renderer 2024-01-18 10:49:28 +08:00
Steven
aecffe3402 feat: implement table parser 2024-01-18 10:21:08 +08:00
Mehad Nadeem
70e6b2bb82 chore: added tooltip for vacuum button (#2778)
* chore: added tooltip for vacuum button

- At the moment only has text for English language therefore other JSON files need to be updated accordingly

* Update en.json
2024-01-18 08:09:49 +08:00
Steven
54296f0437 chore: update filter in timeline 2024-01-18 08:06:59 +08:00
Steven
8fcd9332f7 chore: tweak timeline styles 2024-01-17 22:32:58 +08:00
Steven
1857362d03 fix: locale and appearance setting 2024-01-17 22:16:12 +08:00
Steven
6d7186fc81 feat: rebuild timeline page 2024-01-17 21:59:49 +08:00
Wen Sun
e4488da96e fix: signup is not allowed if password login is disabled (#2776)
Signup is not allowed if password login is disabled

If password login is disabled in the system configuration, the "signup" in the "/auth" page disappears, but the user can manually enter "/auth/signup" to access the system by creating a new user.
2024-01-17 10:49:22 +08:00
Steven
cc43d06d33 chore: update memo stats response 2024-01-17 09:17:33 +08:00
Steven
9ffd827028 fix: appearance and locale initial value 2024-01-16 22:53:45 +08:00
Hanqin Guan
15e6542f0d fix: server overrides user's locale/appearance (#2771) 2024-01-16 18:21:08 +08:00
Noah Alderton
24bb3e096a fix: DatePicker by passing in Timezone to API (#2770)
* Fix DatePicker by passing in Timezone to API

* Add some clarity
2024-01-16 18:02:09 +08:00
Lincoln Nogueira
5bcbbd4c52 chore: fix store tests on Windows (#2769)
It's just a matter of explicitly closing the database, so that TempDir.removeAll doesn't fail.
2024-01-16 13:51:26 +08:00
Noah Alderton
ff13d977e9 feat: add URL paste handler (#2768)
* feat: Add URL paste handler

* Check if text highlighted for URL pasting
2024-01-16 10:06:16 +08:00
Wen Sun
1fdb8b7b01 fix: apply customized profile name in mobile header (#2723)
Fix mobile header title, apply customized profile name
2024-01-16 09:04:03 +08:00
Wen Sun
f1ee88c4e1 fix: display system logo in user banner if user not logged in (#2747)
Display system logo in user banner if user not logged in
2024-01-16 09:03:27 +08:00
Noah Alderton
b578afbc6a fix: DatePicker Local Date (#2766)
Fix DatePicker Local Date
2024-01-16 08:35:48 +08:00
Steven
ad94e8e3c6 feat: implement highlight renderer 2024-01-15 22:54:18 +08:00
Steven
3f4b361fad feat: implement highlight parser 2024-01-15 22:30:06 +08:00
Steven
46bd470640 chore: update favicon 2024-01-15 21:10:41 +08:00
Steven
fdbf2d8af2 chore: fix blockquote renderer 2024-01-15 20:41:37 +08:00
Steven
5a723f00fa chore: split editor keydown handler 2024-01-15 20:33:42 +08:00
Noah Alderton
728a9705ea feat: Markdown Editor Keyboard Shortcuts (#2763)
* Add bold and italic keyboard shortcut

* Add hyperlink keyboard shortcut support
2024-01-15 20:19:59 +08:00
THELOSTSOUL
cd3a98c095 fix: change use-set priority (#2760)
The user settings(locale, appearance) are not in use when restart broswer
2024-01-15 20:08:14 +08:00
Wen Sun
a22ad90174 fix: set memo resources error in mysql (#2761)
Fix error updating memo resources in mysql
2024-01-15 20:05:07 +08:00
Steven
5ebbed9115 chore: handle tag click 2024-01-15 08:15:34 +08:00
Steven
7ae4299df2 chore: implement create resource 2024-01-15 08:13:06 +08:00
Noah Alderton
3d23c01e26 feat: add additional favicon formats (#2752)
Add additional favicons
2024-01-15 07:54:33 +08:00
Lincoln Nogueira
089e04bcfd chore: use webp compression on logo (#2756)
- Logo size reduced from 310 KB to 36 KB.
- Point metadata image URL to local logo instead of remote
2024-01-14 22:21:03 +08:00
Steven
98762be1e5 feat: implement indent for list nodes 2024-01-14 22:19:03 +08:00
Steven
d44e74bd1e chore: update editor actions 2024-01-14 21:47:03 +08:00
Steven
8e0ce4d678 fix: list memos with pinned 2024-01-14 20:51:52 +08:00
Steven
45cf158508 chore: fix max width of home section 2024-01-14 20:25:45 +08:00
Anish Kelkar
7340ae15f7 chore: delete .vscode directory (#2693)
* Delete .vscode directory

* web/.vscode deleted
2024-01-14 20:12:57 +08:00
Steven
6db7ad76da chore: update tag selector 2024-01-13 16:26:42 +08:00
Steven
4a407668bc chore: tweak dialog close button 2024-01-13 15:33:23 +08:00
Steven
ab1fa44f00 feat: implement markdown buttons 2024-01-13 15:09:06 +08:00
Steven
cd0004cf88 chore: update icon version 2024-01-13 15:08:58 +08:00
Steven
667aaf06a0 chore: update dependencies 2024-01-13 14:33:50 +08:00
Steven
a8074d94e8 chore: update image attrs 2024-01-13 14:08:36 +08:00
Hanqin Guan
16e68fbfff fix: duplicated/reflexive relation in v2 api. (#2750) 2024-01-13 07:21:19 +08:00
Wen Sun
81942b3b98 chore: updating the default scopes of GitHub SSO (#2746)
Updating the default scopes of GitHub SSO

The scope of "user" in GitHub OAuth includes permissions to update a user's profile.
2024-01-12 14:02:11 +08:00
Athurg Gooth
a7cda28fc7 fix: filename with space (#2745) 2024-01-12 14:01:19 +08:00
Steven
0c52f1ee6a chore: tweak home style 2024-01-12 08:08:24 +08:00
Steven
1994c20c54 chore: tweak setting page 2024-01-11 22:25:05 +08:00
Steven
a1dda913c3 chore: fix tag selector position 2024-01-11 21:36:22 +08:00
Athurg Gooth
d626de1875 fix: pnpm install failed in docker (#2732)
fix pnpm install failed in docker
2024-01-11 21:29:55 +08:00
Wen Sun
6cfd94cc69 fix: deleting inbox records that senders have been deleted (#2743)
Deleting inbox records that senders have been deleted
2024-01-11 21:29:22 +08:00
Athurg Gooth
79b68222ff chore: set image loading to lazy (#2733)
set image loading to lazy to avoid concurrent problem
2024-01-11 10:27:18 +08:00
Steven
aaec46a39c chore: update find memo with updated time 2024-01-10 00:10:59 +08:00
Steven
9c663b1ba2 fix: merge mysql dsn with params 2024-01-10 00:03:47 +08:00
Steven
777ed899a3 chore: add memo uid 2024-01-08 21:48:26 +08:00
Steven
ddcf1d669d chore: add max content length 2024-01-08 21:17:21 +08:00
Steven
32d02ba022 chore: fix horizontal rule matcher 2024-01-08 21:00:45 +08:00
Steven
5449342016 fix: auto link converters 2024-01-08 20:57:44 +08:00
Noah Alderton
43e42079a4 feat: export Memos as Markdown FIles (#2716) 2024-01-08 11:40:50 +08:00
Steven
cafa7c5adc chore: update backend dependencies 2024-01-06 19:46:35 +08:00
Steven
1258c5a5b0 chore: update workspace setting proto 2024-01-06 19:46:21 +08:00
Steven
83141f9be2 chore: tweak navigation styles 2024-01-06 19:12:26 +08:00
Steven
4c59035757 chore: update about page 2024-01-06 19:01:11 +08:00
Steven
9459ae8265 chore: update postgres stmt builder 2024-01-06 17:12:10 +08:00
Steven
8893a302e2 chore: update logs 2024-01-06 16:58:58 +08:00
Steven
d67eaaaee2 chore: update database migrator 2024-01-06 16:55:13 +08:00
Steven
fd8333eeda chore: fix memo parent_id 2024-01-06 13:22:02 +08:00
Steven
f5a1739472 chore: update memo detail checks 2024-01-06 10:03:36 +08:00
Steven
a297cc3140 chore: exclude comments in memo list response 2024-01-06 09:48:11 +08:00
Steven
79c13c6f83 chore: fix edit memo params 2024-01-06 09:25:17 +08:00
Steven
8b9455d784 chore: fix memo resources position 2024-01-06 09:23:20 +08:00
Steven
501f8898f6 chore: fix postgres stmts 2024-01-05 21:27:16 +08:00
Steven
ee13927607 chore: fix restore tag node 2024-01-05 18:50:28 +08:00
Steven
d2a9aaa9d4 chore: update line break renderer 2024-01-05 09:18:37 +08:00
Steven
f563b58a85 chore: fix renderer props 2024-01-05 08:47:43 +08:00
Steven
ce2d37b90c chore: fix find sibling node 2024-01-05 08:43:30 +08:00
Steven
454cd4e24f feat: implement switchable task list node 2024-01-05 08:40:16 +08:00
Steven
6320d042c8 chore: update home padding styles 2024-01-04 22:50:46 +08:00
Steven
d7ed59581c chore: fix math block matcher 2024-01-04 21:50:13 +08:00
Steven
9593b0b091 chore: fix link rel field 2024-01-04 21:26:56 +08:00
Steven
ca53630410 chore: update drawer background 2024-01-04 21:11:22 +08:00
Steven
f484c38745 chore: fix dependencies 2024-01-04 20:07:53 +08:00
Steven
d12a2b0c38 feat: implement math expression parser 2024-01-04 20:05:29 +08:00
Steven
c842b921bc chore: update backend dependencies 2024-01-04 19:15:25 +08:00
Wen Sun
6b2eec86c2 fix: image upload failed with cloudflare R2 (#2704)
Fix image upload failed with cloudflare R2
2024-01-04 19:08:54 +08:00
Steven
2eba4e2cd4 chore: update version 2024-01-04 08:36:45 +08:00
Steven
73baeaa0ad chore: tweak dark mode styles 2024-01-04 08:32:14 +08:00
Steven
c58851bc97 chore: tweak accent color 2024-01-03 23:30:28 +08:00
Steven
96140f3875 chore: tweak dark mode styles 2024-01-03 23:12:50 +08:00
Lincoln Nogueira
369b8af109 chore: improve resource internal_path migrator (#2698)
* chore: improve internal path migrator
- handle mixed path styles
- handle Windows paths
- add tests

* chore: fix goimports error
2024-01-03 08:31:59 +08:00
Steven
914c0620c4 chore: add statistics view 2024-01-03 08:22:32 +08:00
Steven
138b69e36e chore: fix memo comment 2024-01-03 08:19:38 +08:00
Elliot Chen
3181c076b2 feat: add {uuid} in path template when using local storage or S3 (#2696)
Add {uuid} in path template when using local storage or S3

Add an addition tag `{uuid}` to the `replacePathTemplate`.

It is a workaround to leak the public links of a resource when using S3-based object storage. Currently, all resource blobs stored in S3 (R2, OSS) are set to be public. It is insecure as the resources for the private memos are also accessible on the Internet. Using an additional {uuid} might reduce this risk.

Meanwhile, it is also possible to avoid filename conflict
2024-01-02 20:57:55 +08:00
Noah Alderton
673809e07d fix: docker-compose.dev.yaml (#2695)
* Fix docker-compose.dev.yaml

* Add newline to .gitignore
2024-01-02 18:33:19 +08:00
Steven
f74fa97b4a chore: traverse nodes to upsert tags 2024-01-02 08:56:30 +08:00
Steven
c797099950 chore: update resource internal path migrator 2024-01-02 08:29:18 +08:00
Steven
0f8bfb6328 chore: update index.html 2023-12-29 08:28:17 +08:00
Steven
4cd01ece30 chore: update frontend metadata 2023-12-29 08:19:32 +08:00
Lincoln Nogueira
14b34edca3 chore: fix misuse of package path instead of filepath.path (#2684)
As stated by https://pkg.go.dev/path, "path" is mainly for URLs, "path.filepath" for file systems
2023-12-29 07:50:15 +08:00
Lincoln Nogueira
411e807dcc chore: use consistent relative paths for resources (#2683)
- always store resources with a relative path with forward slashes, which will be transformed as needed when the file is accessed

- fix an issue with thumbnail generation on Windows

- add several validations for local storage setting

- improve front-end error feedback when changing local storage

- add migrations to make existing resource paths relative (not needed, but improves database consistency)
2023-12-29 07:49:55 +08:00
Steven
ea87a1dc0c chore: update memo content props 2023-12-28 22:57:51 +08:00
Steven
46f7cffc7b feat: implement restore nodes 2023-12-28 22:35:39 +08:00
Steven
2a6f054876 chore: update auto link parser 2023-12-28 21:32:44 +08:00
Steven
30dca18b79 chore: fix suspense wrapper 2023-12-28 08:34:06 +08:00
Steven
09c195c752 chore: update backend dependencies 2023-12-28 08:28:50 +08:00
Steven
2ae6d94e2c chore: update frontend dependencies 2023-12-28 08:27:06 +08:00
Steven
9ee4b75bbd chore: tweak memo detail styles 2023-12-27 23:25:02 +08:00
Steven
cc40803b06 chore: update feature request template 2023-12-27 09:00:16 +08:00
Steven
a0a03b0389 chore: tweak list memos request 2023-12-27 08:54:00 +08:00
Steven
0dfc367e56 chore: start grpc server 2023-12-27 08:50:02 +08:00
Steven
c8d7f93dca feat: implement auto link parser 2023-12-27 08:44:51 +08:00
Steven
6fac116d8c chore: update user store 2023-12-26 23:05:33 +08:00
subks
f48ff102c9 fix: eslint check failure after fixing #2672 (#2673) 2023-12-26 21:46:26 +08:00
subks
bd5a0679ee fix: date format in share memo dialog (#2672)
fix: Date format in shareable Memo Images (#2668)
2023-12-26 17:50:09 +08:00
Steven
fcfb76a103 chore: remove user urlsets in sitemap 2023-12-23 19:35:46 +08:00
Steven
8e325f9986 chore: return username in user response 2023-12-23 19:23:39 +08:00
Steven
b8eaf1d57e chore: deprecate memo creation stats legacy api 2023-12-23 18:35:47 +08:00
Steven
42608cdd8f chore: fix server context 2023-12-23 17:59:15 +08:00
Steven
2cfa4c3b76 chore: tweak frontend routes register 2023-12-23 17:58:49 +08:00
Steven
aa136a2776 chore: remove vite pwa plugin 2023-12-23 17:42:13 +08:00
Steven
68413a5371 chore: update frontend service 2023-12-23 17:04:52 +08:00
Steven
638f17a02c chore: update scripts 2023-12-23 15:12:25 +08:00
Steven
273d6a6986 chore: update dockerfile 2023-12-23 14:13:40 +08:00
Steven
953141813c chore: regenerate pnpm lock file 2023-12-23 12:01:26 +08:00
Leyang
be2db3f170 feat: use vite plugin pwa for generate right sw.js (#2658)
Co-authored-by: boojack <stevenlgtm@gmail.com>
2023-12-23 12:00:03 +08:00
Steven
eefce6ade3 chore: implement webhook dispatch in v2 api 2023-12-23 11:17:35 +08:00
Steven
c6ebb5552e chore: add 403 page 2023-12-23 10:08:23 +08:00
Steven
4d64d4bf25 chore: upgrade frontend dependencies 2023-12-23 09:47:43 +08:00
Steven
2ee4d7d745 chore: add startTransition to links 2023-12-23 08:55:43 +08:00
Steven
1b81999329 chore: skip invalida setting checks 2023-12-23 08:55:23 +08:00
Steven
df5aeb6d88 chore: remove v1 prefix in store name 2023-12-23 08:48:11 +08:00
Steven
df3303dcd3 chore: update list users 2023-12-23 08:35:54 +08:00
Steven
c267074851 chore: prevent archive/delete current user 2023-12-23 08:05:05 +08:00
Steven
21874d0509 chore: fix navigation errors 2023-12-23 08:01:16 +08:00
Steven
7898df2876 chore: update issue templates 2023-12-23 08:00:59 +08:00
Steven
b2ec0d1217 chore: add skip cache requesting 2023-12-22 20:30:28 +08:00
Steven
5673e29e51 chore: compose memo in backend 2023-12-22 20:18:31 +08:00
Steven
feefaabce9 chore: update heatmap click handler 2023-12-22 20:07:17 +08:00
Steven
29b540ade3 chore: fix fetch memos 2023-12-22 19:47:46 +08:00
Steven
919f75af1a chore: update suspense loading 2023-12-22 19:27:09 +08:00
Steven
17e905085f chore: update acl config 2023-12-22 09:11:55 +08:00
Steven
34af969785 chore: fix list memos order by pinned 2023-12-22 09:09:03 +08:00
Steven
fd9c3ccbae chore: implement useMemoList store 2023-12-22 09:01:30 +08:00
Steven
a3feeceace chore: remove component v1 suffix 2023-12-22 08:29:02 +08:00
Steven
02265a6e1a chore: fix memo search 2023-12-22 00:31:29 +08:00
Steven
81524c38e9 chore: refactor memo module 2023-12-21 23:40:43 +08:00
Steven
671551bdc1 chore: update memo detail page 2023-12-21 22:42:06 +08:00
Steven
10c81ccba3 chore: fix type definition 2023-12-21 21:43:28 +08:00
Steven
9361613f23 chore: update timeline page 2023-12-21 21:24:08 +08:00
Athurg Gooth
b14334220f fix: trim the dirname of attachment send by telegram bot (#2651) 2023-12-21 09:47:57 +08:00
Athurg Gooth
f184d65267 fix: attachments send from telegram lost (#2650) 2023-12-21 09:47:25 +08:00
Steven
b64e2ff6ff chore: implement list memo resources api 2023-12-20 23:46:04 +08:00
Steven
cbdae24314 chore: update archived page 2023-12-20 23:23:26 +08:00
Steven
762cb25227 chore: update memo service 2023-12-20 23:14:15 +08:00
Steven
fc01a796f8 chore: fix demo seed data 2023-12-20 08:18:56 +08:00
Steven
feb700f325 chore: clear access token when user not found 2023-12-20 07:42:02 +08:00
Steven
5334fdf1b2 chore: use api v2 in archived page 2023-12-19 23:49:24 +08:00
Steven
abc14217f6 chore: tweak padding styles 2023-12-19 23:09:57 +08:00
Steven
af68cae6ea chore: regenerate swagger docs 2023-12-19 22:37:07 +08:00
Steven
e0cacfc6d6 chore: retire auto backup for sqlite 2023-12-19 22:34:06 +08:00
Steven
b575064d47 chore: tweak padding 2023-12-19 21:44:40 +08:00
Steven
6290234ad1 chore: fix button styles 2023-12-19 21:29:07 +08:00
Steven
aeed25648a chore: tweak drawer background 2023-12-19 08:55:21 +08:00
Steven
43e7506ed5 chore: fix react-uses import 2023-12-19 08:45:47 +08:00
Steven
a3a1bbe8de chore: tweak responsible styles 2023-12-19 08:41:41 +08:00
Steven
fe4ec0b156 chore: rename navigator 2023-12-19 00:13:22 +08:00
Steven
7c5fdd1b06 chore: remove demo banner 2023-12-18 23:46:48 +08:00
Steven
4d54463aeb chore: add mobile header 2023-12-18 23:33:09 +08:00
Steven
40bc8df63d chore: fix container height 2023-12-18 23:01:39 +08:00
Steven
61de7c8a32 chore: fix demo banner 2023-12-18 22:29:29 +08:00
Steven
d6656db20d feat: implement part of full-screen layout 2023-12-18 22:10:36 +08:00
Steven
15a091fe4c chore: fix offset params in explore page 2023-12-18 20:54:51 +08:00
Steven
d8a0528135 chore: tweak variable names 2023-12-18 20:47:29 +08:00
Steven
16fb5faebd chore: revert go mod update 2023-12-18 19:40:39 +08:00
Steven
2c4b5d75b3 chore: fix html escaping 2023-12-17 23:37:00 +08:00
Steven
770607f93f fix: add markdown service to acl 2023-12-17 11:37:38 +08:00
Steven
db0eff4743 chore: clean frontend dependencies 2023-12-17 11:25:10 +08:00
Steven
0793f96578 chore: update heading styles 2023-12-17 11:08:13 +08:00
Steven
8095d94c97 chore: deprecate marked 2023-12-17 11:02:16 +08:00
Steven
bcfcd59642 chore: deprecate old MemoContent 2023-12-17 10:58:22 +08:00
Steven
5d677c3c57 chore: implement node renderer components 2023-12-17 10:49:49 +08:00
Steven
28c0549705 feat: add markdown service 2023-12-17 09:53:22 +08:00
Steven
bb42042db4 chore: implement task list parser 2023-12-16 12:48:52 +08:00
Steven
1c7fb77e05 chore: update user setting names 2023-12-16 12:18:53 +08:00
Steven
e8ca2ea5a0 chore: rename renderer package 2023-12-16 11:57:36 +08:00
Steven
e43a445c34 chore: implement escaping character node 2023-12-16 11:47:29 +08:00
Steven
1237643028 chore: update parser tests 2023-12-16 11:34:55 +08:00
Steven
aee0e31b0a chore: update parser functions 2023-12-16 10:38:05 +08:00
Steven
47af632c79 chore: update inline parser 2023-12-16 10:09:20 +08:00
Steven
7b0ceee57b chore: update memo metadata description 2023-12-16 09:23:45 +08:00
Steven
bdc867d153 fix: heading render 2023-12-16 09:12:55 +08:00
Steven
6421fbc68a chore: implement list html render 2023-12-16 09:01:19 +08:00
Steven
b00443c222 chore: implement list nodes 2023-12-16 08:51:29 +08:00
Steven
a10b3d3821 chore: tweak custom profile 2023-12-15 22:57:53 +08:00
Steven
7735cfac31 chore: update seed data 2023-12-15 22:34:19 +08:00
Steven
749187e1e9 chore: update dockerfile 2023-12-15 21:46:11 +08:00
Steven
a9812592fe chore: tweak editor border styles 2023-12-15 21:35:31 +08:00
Steven
e4070f7753 chore: bump version 2023-12-15 21:11:04 +08:00
Steven
ff53187eae chore: add sitemap and robots routes 2023-12-15 20:18:01 +08:00
Steven
89ef9b8531 chore: add instance url system setting 2023-12-15 19:39:37 +08:00
Steven
56b55ad941 chore: update memo metadata 2023-12-15 19:13:56 +08:00
Steven
24672e0c5e chore: update memo metadata 2023-12-15 08:12:10 +08:00
Steven
52743017a3 chore: implement memo route 2023-12-15 07:32:49 +08:00
Steven
6cf7192d6a chore: add ssr placeholder in index.html 2023-12-14 23:29:42 +08:00
Steven
6763dab4e5 chore: handle newline in block parsers 2023-12-14 22:55:46 +08:00
Steven
e0290b94b4 chore: use gomark in rss api 2023-12-14 22:33:20 +08:00
Steven
242f64fa8e chore: implement html render 2023-12-14 22:21:23 +08:00
Steven
3edce174d6 chore: remove unused methods 2023-12-14 00:24:41 +08:00
Steven
5266a62685 chore: implement html renderer 2023-12-14 00:04:20 +08:00
Steven
43ef9eaced chore: implement part of html renderer 2023-12-13 23:50:05 +08:00
Steven
453707d18c feat: implement gomark parsers 2023-12-13 21:00:13 +08:00
Steven
2d9c5d16e1 chore: fix user string 2023-12-13 19:08:06 +08:00
Steven
b20e0097cf chore: implement part of nodes 2023-12-13 09:06:47 +08:00
Steven
dd83782522 chore: add line break node 2023-12-12 23:38:43 +08:00
Steven
aa3632e2ac chore: implement gomark skeleton 2023-12-12 23:24:02 +08:00
boojack
7f1f6f77a0 chore: update i18n with weblate (#2614)
* Translated using Weblate (French)

Currently translated at 100.0% (317 of 317 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/fr/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (317 of 317 strings)

Translation: memos-i18n/i18n
Translate-URL: https://hosted.weblate.org/projects/memos-i18n/english/pt_BR/

---------

Co-authored-by: Ezmana <ezmana.land@gmail.com>
Co-authored-by: Lincoln Nogueira <lincolnthalles@users.noreply.github.com>
2023-12-12 21:06:13 +08:00
Steven
7eb5be0a4e chore: fix update user 2023-12-12 20:01:31 +08:00
Steven
603a6a4971 chore: fix vacuum memo 2023-12-12 19:56:15 +08:00
Cologler
6bda64064e fix: delete one memo will delete all memos on pgsql (#2611)
fix: delete single memo will all memos on pgsql

Close #2605
2023-12-12 19:52:39 +08:00
Steven
ec7992553f chore: go mod update 2023-12-11 22:21:05 +08:00
Steven
e5de8c08f5 chore: clean debug code 2023-12-11 22:20:57 +08:00
Steven
c608877c3e chore: clean binary entries 2023-12-11 22:16:39 +08:00
Steven
52f399a154 chore: remove unused functions 2023-12-11 21:53:16 +08:00
Gabe Cook
88728906a8 fix(copydb): fix migration to Postgres (#2601)
* chore(copydb): Use query builder during setup

* fix(copydb): Fix migration to Postgres
2023-12-11 18:05:15 +08:00
699 changed files with 50299 additions and 50039 deletions

View File

@@ -1,11 +1,11 @@
name: Bug Report
description: Create a report to help us improve
description: If something isn't working as expected
labels: [bug]
body:
- type: markdown
attributes:
value: |
If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.
Before submitting a bug report, please check if the issue is already present in the issues. If it is, please add a reaction to the issue. If it isn't, please fill out the form below.
- type: textarea
attributes:
label: Describe the bug
@@ -24,8 +24,15 @@ body:
3. See error
validations:
required: true
- type: input
attributes:
label: The version of Memos you're using
description: |
Provide the version of Memos you're using.
validations:
required: true
- type: textarea
attributes:
label: Screenshots or additional context
description: |
Add screenshots or any other context about the problem.
If applicable, add screenshots to help explain your problem. And add any other context about the problem here. Such as the device you're using, etc.

View File

@@ -1,28 +1,36 @@
name: Feature Request
description: Suggest an idea for this project
description: If you have a suggestion for a new feature
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest an idea for memos!
- type: textarea
attributes:
label: Is your feature request related to a problem?
description: |
A clear and concise description of what the problem is.
placeholder: |
I'm always frustrated when [...]
validations:
required: true
Before submitting a feature request, please check if the issue is already present in the issues. If it is, please add a reaction to the issue. If it isn't, please fill out the form below.
- type: textarea
attributes:
label: Describe the solution you'd like
description: |
A clear and concise description of what you want to happen.
placeholder: |
It would be great if [...]
validations:
required: true
- type: dropdown
attributes:
label: Type of feature
description: What type of feature is this?
options:
- User Interface (UI)
- User Experience (UX)
- API
- Documentation
- Integrations
- Other
default: 0
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request.
description: |
What are you trying to do? Why is this important to you?

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "github-actions"
commit-message:
prefix: "chore"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: npm
commit-message:
prefix: "chore"
directory: "/web"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
commit-message:
prefix: "chore"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -19,15 +19,15 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.21
go mod tidy -go=1.22
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.54.1
args: --verbose --timeout=3m
@@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.22
check-latest: true
cache: true
- name: Run all tests

View File

@@ -27,8 +27,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
username: stevenlgtm
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -52,7 +52,6 @@ jobs:
neosmemo/memos
ghcr.io/usememos/memos
tags: |
type=raw,value=latest
type=semver,pattern={{version}},value=${{ env.VERSION }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }}

View File

@@ -0,0 +1,61 @@
name: build-and-push-stable-image
on:
push:
branches:
- "stable"
jobs:
build-and-push-release-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: stevenlgtm
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
install: true
version: v0.9.1
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
neosmemo/memos
ghcr.io/usememos/memos
tags: |
type=raw,value=stable
flavor: |
latest=true
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v5
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -19,8 +19,8 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
username: stevenlgtm
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3

33
.github/workflows/build-artifacts.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Build artifacts
on:
push:
tags:
- "*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: 1.22
check-latest: true
cache: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
# 'latest', 'nightly', or a semver
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,74 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
paths:
- "go.mod"
- "go.sum"
- "**.go"
- "proto/**"
- "web/**"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["go", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -11,42 +11,41 @@ on:
- "web/**"
jobs:
eslint-checks:
static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4.0.0
with:
version: 8
version: 9
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
cache: pnpm
cache-dependency-path: "web/pnpm-lock.yaml"
- run: pnpm install
working-directory: web
- run: pnpm type-gen
working-directory: web
- name: Run eslint check
run: pnpm lint
working-directory: web
- name: Run type checks
run: pnpm type-check
working-directory: web
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2.4.0
- uses: pnpm/action-setup@v4.0.0
with:
version: 8
version: 9
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
cache: pnpm
cache-dependency-path: "web/pnpm-lock.yaml"
- run: pnpm install
working-directory: web
- run: pnpm type-gen
working-directory: web
- name: Run frontend build
run: pnpm build
working-directory: web

17
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Close Stale Issues
on:
schedule:
- cron: "0 */8 * * *"
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v9.0.0
with:
days-before-issue-stale: 14
days-before-issue-close: 7

8
.gitignore vendored
View File

@@ -6,7 +6,6 @@ tmp
# Frontend asset
web/dist
server/dist
# build folder
build
@@ -16,4 +15,11 @@ build
# Jetbrains
.idea
# Docker Compose Environment File
.env
bin/air
dev-dist
dist

View File

@@ -67,6 +67,14 @@ linters-settings:
disabled: true
- name: early-return
disabled: true
- name: use-any
disabled: true
- name: exported
disabled: true
- name: unhandled-error
disabled: true
- name: if-return
disabled: true
gocritic:
disabled-checks:
- ifElseChain

38
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,38 @@
version: 1
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
main: ./bin/memos
binary: memos
goos:
- linux
- darwin
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
checksum:
disable: true
release:
draft: true
replace_existing_draft: true
make_latest: true
mode: replace
skip_upload: false

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
{
"json.schemaDownload.enable":true,
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint",
}

View File

@@ -1,23 +1,23 @@
# Build frontend dist.
FROM node:18-alpine AS frontend
FROM node:20-alpine AS frontend
WORKDIR /frontend-build
COPY . .
WORKDIR /frontend-build/web
RUN corepack enable && pnpm i --frozen-lockfile && pnpm type-gen
RUN corepack enable && pnpm i --frozen-lockfile
RUN pnpm build
# Build backend exec file.
FROM golang:1.21-alpine AS backend
FROM golang:1.22-alpine AS backend
WORKDIR /backend-build
COPY . .
COPY --from=frontend /frontend-build/web/dist ./server/dist
COPY --from=frontend /frontend-build/web/dist /backend-build/server/router/frontend/dist
RUN CGO_ENABLED=0 go build -o memos ./main.go
RUN CGO_ENABLED=0 go build -o memos ./bin/memos/main.go
# Make workspace with above generated files.
FROM alpine:latest AS monolithic

View File

@@ -8,13 +8,11 @@ A privacy-first, lightweight note-taking service. Easily capture and share your
<a href="https://demo.usememos.com/">Live Demo</a>
<p>
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos?logo=github" /></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>
<a href="https://hosted.weblate.org/engage/memos-i18n/"><img src="https://hosted.weblate.org/widget/memos-i18n/english/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>
![demo](https://www.usememos.com/demo.webp)
![demo](https://www.usememos.com/demo.png)
## Key points
@@ -27,7 +25,7 @@ A privacy-first, lightweight note-taking service. Easily capture and share your
## Deploy with Docker in seconds
```bash
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos ghcr.io/usememos/memos:latest
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:stable
```
> The `~/.memos/` directory will be used as the data directory on your local machine, while `/var/opt/memos` is the directory of the volume in Docker and should not be modified.
@@ -42,21 +40,19 @@ Contributions are what make the open-source community such an amazing place to l
<img src="https://contri-graphy.yourselfhosted.com/graph?repo=usememos/memos&format=svg" />
</a>
---
## Internationalization
- [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
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
- [eallion/memos.top](https://github.com/eallion/memos.top) - Static page rendered with the Memos API
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - Logseq plugin
- [quanru/obsidian-periodic-para](https://github.com/quanru/obsidian-periodic-para#daily-record) - Obsidian plugin
- [JakeLaoyu/memos-import-from-flomo](https://github.com/JakeLaoyu/memos-import-from-flomo) - Import data. Support from flomo, wechat reading
- [Quick Memo](https://www.icloud.com/shortcuts/1eaef307112843ed9f91d256f5ee7ad9) - Shortcuts (iOS, iPadOS or macOS)
- [Memos Raycast Extension](https://www.raycast.com/JakeYu/memos) - Raycast extension
- [Memos Desktop](https://github.com/xudaolong/memos-desktop) - Third party client for MacOS and Windows
- [MemosGallery](https://github.com/BarryYangi/MemosGallery) - A static Gallery rendered with the Memos API
Memos supports multiple languages. You can help us translate Memos into your language. We use Weblate to manage translations.
<a href="https://hosted.weblate.org/engage/memos-i18n/">
<img src="https://hosted.weblate.org/widget/memos-i18n/english/287x66-grey.png" alt="Translation status" />
</a>
## Star history
[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date)
## Other projects
- [**Slash**](https://github.com/yourselfhosted/slash): An open source, self-hosted bookmarks and link sharing platform. Save and share your links very easily.
- [**Gomark**](https://github.com/yourselfhosted/gomark): A markdown parser written in Go for Memos. And its [WebAssembly version](https://github.com/yourselfhosted/gomark-wasm) is also available.

View File

@@ -1,165 +0,0 @@
package resource
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/disintegration/imaging"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
const (
// The key name used to store user id in the context
// user id is extracted from the jwt token subject field.
userIDContextKey = "user-id"
// thumbnailImagePath is the directory to store image thumbnails.
thumbnailImagePath = ".thumbnail_cache"
)
type Service struct {
Profile *profile.Profile
Store *store.Store
}
func NewService(profile *profile.Profile, store *store.Store) *Service {
return &Service{
Profile: profile,
Store: store,
}
}
func (s *Service) RegisterResourcePublicRoutes(g *echo.Group) {
g.GET("/r/:resourceId", s.streamResource)
g.GET("/r/:resourceId/*", s.streamResource)
}
func (s *Service) streamResource(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID,
GetBlob: true,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
}
// Check the related memo visibility.
if resource.MemoID != nil {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: resource.MemoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", resource.MemoID)).SetInternal(err)
}
if memo != nil && memo.Visibility != store.Public {
userID, ok := c.Get(userIDContextKey).(int32)
if !ok || (memo.Visibility == store.Private && userID != resource.CreatorID) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match")
}
}
}
blob := resource.Blob
if resource.InternalPath != "" {
resourcePath := resource.InternalPath
src, err := os.Open(resourcePath)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", resourcePath)).SetInternal(err)
}
defer src.Close()
blob, err = io.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to read the local resource: %s", resourcePath)).SetInternal(err)
}
}
if c.QueryParam("thumbnail") == "1" && util.HasPrefixes(resource.Type, "image/png", "image/jpeg") {
ext := filepath.Ext(resource.Filename)
thumbnailPath := filepath.Join(s.Profile.Data, thumbnailImagePath, fmt.Sprintf("%d%s", resource.ID, ext))
thumbnailBlob, err := getOrGenerateThumbnailImage(blob, thumbnailPath)
if err != nil {
log.Warn(fmt.Sprintf("failed to get or generate local thumbnail with path %s", thumbnailPath), zap.Error(err))
} else {
blob = thumbnailBlob
}
}
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=3600")
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'none'; script-src 'none'; img-src 'self'; media-src 'self'; sandbox;")
c.Response().Writer.Header().Set("Content-Disposition", fmt.Sprintf(`filename="%s"`, resource.Filename))
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(blob))
return nil
}
return c.Stream(http.StatusOK, resourceType, bytes.NewReader(blob))
}
var availableGeneratorAmount int32 = 32
func getOrGenerateThumbnailImage(srcBlob []byte, dstPath string) ([]byte, error) {
if _, err := os.Stat(dstPath); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, errors.Wrap(err, "failed to check thumbnail image stat")
}
if atomic.LoadInt32(&availableGeneratorAmount) <= 0 {
return nil, errors.New("not enough available generator amount")
}
atomic.AddInt32(&availableGeneratorAmount, -1)
defer func() {
atomic.AddInt32(&availableGeneratorAmount, 1)
}()
reader := bytes.NewReader(srcBlob)
src, err := imaging.Decode(reader, imaging.AutoOrientation(true))
if err != nil {
return nil, errors.Wrap(err, "failed to decode thumbnail image")
}
thumbnailImage := imaging.Resize(src, 512, 0, imaging.Lanczos)
dstDir := path.Dir(dstPath)
if err := os.MkdirAll(dstDir, os.ModePerm); err != nil {
return nil, errors.Wrap(err, "failed to create thumbnail dir")
}
if err := imaging.Save(thumbnailImage, dstPath); err != nil {
return nil, errors.Wrap(err, "failed to resize thumbnail image")
}
}
dstFile, err := os.Open(dstPath)
if err != nil {
return nil, errors.Wrap(err, "failed to open the local resource")
}
defer dstFile.Close()
dstBlob, err := io.ReadAll(dstFile)
if err != nil {
return nil, errors.Wrap(err, "failed to read the local resource")
}
return dstBlob, nil
}

View File

@@ -1,399 +0,0 @@
package v1
import (
"context"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/plugin/idp/oauth2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
var (
usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
)
type SignIn struct {
Username string `json:"username"`
Password string `json:"password"`
Remember bool `json:"remember"`
}
type SSOSignIn struct {
IdentityProviderID int32 `json:"identityProviderId"`
Code string `json:"code"`
RedirectURI string `json:"redirectUri"`
}
type SignUp struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
g.POST("/auth/signin", s.SignIn)
g.POST("/auth/signin/sso", s.SignInSSO)
g.POST("/auth/signout", s.SignOut)
g.POST("/auth/signup", s.SignUp)
}
// SignIn godoc
//
// @Summary Sign-in to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignIn true "Sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
// @Failure 403 {object} nil "User has been archived with username %s"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin [POST]
func (s *APIV1Service) SignIn(c echo.Context) error {
ctx := c.Request().Context()
signin := &SignIn{}
disablePasswordLoginSystemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingDisablePasswordLoginName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
if disablePasswordLoginSystemSetting != nil {
disablePasswordLogin := false
err = json.Unmarshal([]byte(disablePasswordLoginSystemSetting.Value), &disablePasswordLogin)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if disablePasswordLogin {
return echo.NewHTTPError(http.StatusUnauthorized, "Password login is deactivated")
}
}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &signin.Username,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
} else if user.RowStatus == store.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
}
// Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
// If the two passwords don't match, return a 401 status.
return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again")
}
var expireAt time.Time
// Set cookie expiration to 100 years to make it persistent.
cookieExp := time.Now().AddDate(100, 0, 0)
if !signin.Remember {
expireAt = time.Now().Add(auth.AccessTokenDuration)
cookieExp = time.Now().Add(auth.CookieExpDuration)
}
accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, expireAt, []byte(s.Secret))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
}
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
}
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage)
}
// SignInSSO godoc
//
// @Summary Sign-in to memos using SSO.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SSOSignIn true "SSO sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Access denied, identifier does not match the filter."
// @Failure 403 {object} nil "User has been archived with username {username}"
// @Failure 404 {object} nil "Identity provider not found"
// @Failure 500 {object} nil "Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin/sso [POST]
func (s *APIV1Service) SignInSSO(c echo.Context) error {
ctx := c.Request().Context()
signin := &SSOSignIn{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
ID: &signin.IdentityProviderID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider").SetInternal(err)
}
if identityProvider == nil {
return echo.NewHTTPError(http.StatusNotFound, "Identity provider not found")
}
var userInfo *idp.IdentityProviderUserInfo
if identityProvider.Type == store.IdentityProviderOAuth2Type {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.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 := identityProvider.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.GetUser(ctx, &store.FindUser{
Username: &userInfo.Identifier,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
}
if user == nil {
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingAllowSignUpName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := true
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)
}
userCreate := &store.User{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: store.RoleUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
}
password, err := util.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 == store.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier))
}
accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
}
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
}
cookieExp := time.Now().Add(auth.CookieExpDuration)
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage)
}
// SignOut godoc
//
// @Summary Sign-out from memos.
// @Tags auth
// @Produce json
// @Success 200 {boolean} true "Sign-out success"
// @Router /api/v1/auth/signout [POST]
func (s *APIV1Service) SignOut(c echo.Context) error {
accessToken := findAccessToken(c)
userID, _ := getUserIDFromAccessToken(accessToken, s.Secret)
err := removeAccessTokenAndCookies(c, s.Store, userID, accessToken)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to remove access token, err: %s", err)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// SignUp godoc
//
// @Summary Sign-up to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignUp true "Sign-up object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signup request | Failed to find users"
// @Failure 401 {object} nil "signup is disabled"
// @Failure 403 {object} nil "Forbidden"
// @Failure 404 {object} nil "Not found"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signup [POST]
func (s *APIV1Service) SignUp(c echo.Context) error {
ctx := c.Request().Context()
signup := &SignUp{}
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
}
hostUserType := store.RoleHost
existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
Role: &hostUserType,
})
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
}
if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
}
userCreate := &store.User{
Username: signup.Username,
// The new signup user should be normal user by default.
Role: store.RoleUser,
Nickname: signup.Username,
}
if len(existedHostUsers) == 0 {
// Change the default role to host if there is no host user.
userCreate.Role = store.RoleHost
} else {
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingAllowSignUpName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := true
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)
}
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(ctx, userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
accessToken, err := auth.GenerateAccessToken(user.Username, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(s.Secret))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
}
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
}
cookieExp := time.Now().Add(auth.CookieExpDuration)
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage)
}
func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
if err != nil {
return errors.Wrap(err, "failed to get user access tokens")
}
userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
AccessToken: accessToken,
Description: "Account sign in",
}
userAccessTokens = append(userAccessTokens, &userAccessToken)
if _, err := s.Store.UpsertUserSettingV1(ctx, &storepb.UserSetting{
UserId: user.ID,
Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
Value: &storepb.UserSetting_AccessTokens{
AccessTokens: &storepb.AccessTokensUserSetting{
AccessTokens: userAccessTokens,
},
},
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
}
return nil
}
// removeAccessTokenAndCookies removes the jwt token from the cookies.
func removeAccessTokenAndCookies(c echo.Context, s *store.Store, userID int32, token string) error {
err := s.RemoveUserAccessToken(c.Request().Context(), userID, token)
if err != nil {
return err
}
cookieExp := time.Now().Add(-1 * time.Hour)
setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
return nil
}
// setTokenCookie sets the token to the cookie.
func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
cookie := new(http.Cookie)
cookie.Name = name
cookie.Value = token
cookie.Expires = expiration
cookie.Path = "/"
// Http-only helps mitigate the risk of client side script accessing the protected cookie.
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteStrictMode
c.SetCookie(cookie)
}

View File

@@ -1,15 +0,0 @@
package v1
// RowStatus is the status for a row.
type RowStatus string
const (
// Normal is the status for a normal row.
Normal RowStatus = "NORMAL"
// Archived is the status for an archived row.
Archived RowStatus = "ARCHIVED"
)
func (r RowStatus) String() string {
return string(r)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +0,0 @@
package v1
import (
"fmt"
"net/http"
"net/url"
"github.com/labstack/echo/v4"
getter "github.com/usememos/memos/plugin/http-getter"
)
func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
// GET /get/image?url={url} - Get image.
g.GET("/get/image", GetImage)
}
// GetImage godoc
//
// @Summary Get GetImage from URL
// @Tags image-url
// @Produce GetImage/*
// @Param url query string true "Image url"
// @Success 200 {object} nil "Image"
// @Failure 400 {object} nil "Missing GetImage url | Wrong url | Failed to get GetImage url: %s"
// @Failure 500 {object} nil "Failed to write GetImage blob"
// @Router /o/get/GetImage [GET]
func GetImage(c echo.Context) error {
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
image, err := getter.GetImage(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(image.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
}
return nil
}

View File

@@ -1,349 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/store"
)
type IdentityProviderType string
const (
IdentityProviderOAuth2Type IdentityProviderType = "OAUTH2"
)
func (t IdentityProviderType) String() string {
return string(t)
}
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 int32 `json:"id"`
Name string `json:"name"`
Type IdentityProviderType `json:"type"`
IdentifierFilter string `json:"identifierFilter"`
Config *IdentityProviderConfig `json:"config"`
}
type CreateIdentityProviderRequest struct {
Name string `json:"name"`
Type IdentityProviderType `json:"type"`
IdentifierFilter string `json:"identifierFilter"`
Config *IdentityProviderConfig `json:"config"`
}
type UpdateIdentityProviderRequest struct {
ID int32 `json:"-"`
Type IdentityProviderType `json:"type"`
Name *string `json:"name"`
IdentifierFilter *string `json:"identifierFilter"`
Config *IdentityProviderConfig `json:"config"`
}
func (s *APIV1Service) registerIdentityProviderRoutes(g *echo.Group) {
g.GET("/idp", s.GetIdentityProviderList)
g.POST("/idp", s.CreateIdentityProvider)
g.GET("/idp/:idpId", s.GetIdentityProvider)
g.PATCH("/idp/:idpId", s.UpdateIdentityProvider)
g.DELETE("/idp/:idpId", s.DeleteIdentityProvider)
}
// GetIdentityProviderList godoc
//
// @Summary Get a list of identity providers
// @Description *clientSecret is only available for host user
// @Tags idp
// @Produce json
// @Success 200 {object} []IdentityProvider "List of available identity providers"
// @Failure 500 {object} nil "Failed to find identity provider list | Failed to find user"
// @Router /api/v1/idp [GET]
func (s *APIV1Service) GetIdentityProviderList(c echo.Context) error {
ctx := c.Request().Context()
list, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find identity provider list").SetInternal(err)
}
userID, ok := c.Get(userIDContextKey).(int32)
isHostUser := false
if ok {
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role == store.RoleHost {
isHostUser = true
}
}
identityProviderList := []*IdentityProvider{}
for _, item := range list {
identityProvider := convertIdentityProviderFromStore(item)
// data desensitize
if !isHostUser {
identityProvider.Config.OAuth2Config.ClientSecret = ""
}
identityProviderList = append(identityProviderList, identityProvider)
}
return c.JSON(http.StatusOK, identityProviderList)
}
// CreateIdentityProvider godoc
//
// @Summary Create Identity Provider
// @Tags idp
// @Accept json
// @Produce json
// @Param body body CreateIdentityProviderRequest true "Identity provider information"
// @Success 200 {object} store.IdentityProvider "Identity provider information"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 400 {object} nil "Malformatted post identity provider request"
// @Failure 500 {object} nil "Failed to find user | Failed to create identity provider"
// @Router /api/v1/idp [POST]
func (s *APIV1Service) CreateIdentityProvider(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderCreate := &CreateIdentityProviderRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(identityProviderCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post identity provider request").SetInternal(err)
}
identityProvider, err := s.Store.CreateIdentityProvider(ctx, &store.IdentityProvider{
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, convertIdentityProviderFromStore(identityProvider))
}
// GetIdentityProvider godoc
//
// @Summary Get an identity provider by ID
// @Tags idp
// @Accept json
// @Produce json
// @Param idpId path int true "Identity provider ID"
// @Success 200 {object} store.IdentityProvider "Requested identity provider"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Identity provider not found"
// @Failure 500 {object} nil "Failed to find identity provider list | Failed to find user"
// @Router /api/v1/idp/{idpId} [GET]
func (s *APIV1Service) GetIdentityProvider(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderID, err := util.ConvertStringToInt32(c.Param("idpId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("idpId"))).SetInternal(err)
}
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
ID: &identityProviderID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get identity provider").SetInternal(err)
}
if identityProvider == nil {
return echo.NewHTTPError(http.StatusNotFound, "Identity provider not found")
}
return c.JSON(http.StatusOK, convertIdentityProviderFromStore(identityProvider))
}
// DeleteIdentityProvider godoc
//
// @Summary Delete an identity provider by ID
// @Tags idp
// @Accept json
// @Produce json
// @Param idpId path int true "Identity Provider ID"
// @Success 200 {boolean} true "Identity Provider deleted"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch identity provider request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to patch identity provider"
// @Router /api/v1/idp/{idpId} [DELETE]
func (s *APIV1Service) DeleteIdentityProvider(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderID, err := util.ConvertStringToInt32(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.DeleteIdentityProvider{ID: identityProviderID}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete identity provider").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// UpdateIdentityProvider godoc
//
// @Summary Update an identity provider by ID
// @Tags idp
// @Accept json
// @Produce json
// @Param idpId path int true "Identity Provider ID"
// @Param body body UpdateIdentityProviderRequest true "Patched identity provider information"
// @Success 200 {object} store.IdentityProvider "Patched identity provider"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch identity provider request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized
// @Failure 500 {object} nil "Failed to find user | Failed to patch identity provider"
// @Router /api/v1/idp/{idpId} [PATCH]
func (s *APIV1Service) UpdateIdentityProvider(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
identityProviderID, err := util.ConvertStringToInt32(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 := &UpdateIdentityProviderRequest{
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)
}
identityProvider, err := s.Store.UpdateIdentityProvider(ctx, &store.UpdateIdentityProvider{
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, convertIdentityProviderFromStore(identityProvider))
}
func convertIdentityProviderFromStore(identityProvider *store.IdentityProvider) *IdentityProvider {
return &IdentityProvider{
ID: identityProvider.ID,
Name: identityProvider.Name,
Type: IdentityProviderType(identityProvider.Type),
IdentifierFilter: identityProvider.IdentifierFilter,
Config: convertIdentityProviderConfigFromStore(identityProvider.Config),
}
}
func convertIdentityProviderConfigFromStore(config *store.IdentityProviderConfig) *IdentityProviderConfig {
return &IdentityProviderConfig{
OAuth2Config: &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: &FieldMapping{
Identifier: config.OAuth2Config.FieldMapping.Identifier,
DisplayName: config.OAuth2Config.FieldMapping.DisplayName,
Email: config.OAuth2Config.FieldMapping.Email,
},
},
}
}
func convertIdentityProviderConfigToStore(config *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

@@ -1,156 +0,0 @@
package v1
import (
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
const (
// The key name used to store user id in the context
// user id is extracted from the jwt token subject field.
userIDContextKey = "user-id"
)
func extractTokenFromHeader(c echo.Context) (string, error) {
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
return "", nil
}
authHeaderParts := strings.Fields(authHeader)
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
return "", errors.New("Authorization header format must be Bearer {token}")
}
return authHeaderParts[1], nil
}
func findAccessToken(c echo.Context) string {
// Check the HTTP request header first.
accessToken, _ := extractTokenFromHeader(c)
if accessToken == "" {
// Check the cookie.
cookie, _ := c.Cookie(auth.AccessTokenCookieName)
if cookie != nil {
accessToken = cookie.Value
}
}
return accessToken
}
// JWTMiddleware validates the access token.
func JWTMiddleware(server *APIV1Service, next echo.HandlerFunc, secret string) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
path := c.Request().URL.Path
method := c.Request().Method
if server.defaultAuthSkipper(c) {
return next(c)
}
// Skip validation for server status endpoints.
if util.HasPrefixes(path, "/api/v1/ping", "/api/v1/status") && method == http.MethodGet {
return next(c)
}
accessToken := findAccessToken(c)
if accessToken == "" {
// Allow the user to access the public endpoints.
if util.HasPrefixes(path, "/o") {
return next(c)
}
// When the request is not authenticated, we allow the user to access the memo endpoints for those public memos.
if util.HasPrefixes(path, "/api/v1/idp", "/api/v1/memo", "/api/v1/user") && path != "/api/v1/user" && method == http.MethodGet {
return next(c)
}
return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
}
userID, err := getUserIDFromAccessToken(accessToken, secret)
if err != nil {
err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken)
if err != nil {
log.Error("fail to remove AccessToken and Cookies", zap.Error(err))
}
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired access token")
}
accessTokens, err := server.Store.GetUserAccessTokens(ctx, userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user access tokens.").WithInternal(err)
}
if !validateAccessToken(accessToken, accessTokens) {
err = removeAccessTokenAndCookies(c, server.Store, userID, accessToken)
if err != nil {
log.Error("fail to remove AccessToken and Cookies", zap.Error(err))
}
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.")
}
// Even if there is no error, we still need to make sure the user still exists.
user, err := server.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to find user ID: %d", userID)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Failed to find user ID: %d", userID))
}
// Stores userID into context.
c.Set(userIDContextKey, userID)
return next(c)
}
}
func getUserIDFromAccessToken(accessToken, secret string) (int32, error) {
claims := &auth.ClaimsMessage{}
_, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
}
if kid, ok := t.Header["kid"].(string); ok {
if kid == "v1" {
return []byte(secret), nil
}
}
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
})
if err != nil {
return 0, errors.Wrap(err, "Invalid or expired access token")
}
// We either have a valid access token or we will attempt to generate new access token.
userID, err := util.ConvertStringToInt32(claims.Subject)
if err != nil {
return 0, errors.Wrap(err, "Malformed ID in the token")
}
return userID, nil
}
func (*APIV1Service) defaultAuthSkipper(c echo.Context) bool {
path := c.Path()
return util.HasPrefixes(path, "/api/v1/auth")
}
func validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
for _, userAccessToken := range userAccessTokens {
if accessTokenString == userAccessToken.AccessToken {
return true
}
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/store"
)
type MemoOrganizer struct {
MemoID int32 `json:"memoId"`
UserID int32 `json:"userId"`
Pinned bool `json:"pinned"`
}
type UpsertMemoOrganizerRequest struct {
Pinned bool `json:"pinned"`
}
func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) {
g.POST("/memo/:memoId/organizer", s.CreateMemoOrganizer)
}
// CreateMemoOrganizer godoc
//
// @Summary Organize memo (pin/unpin)
// @Tags memo-organizer
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to organize"
// @Param body body UpsertMemoOrganizerRequest true "Memo organizer object"
// @Success 200 {object} store.Memo "Memo information"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo organizer request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Memo not found: %v"
// @Failure 500 {object} nil "Failed to find memo | Failed to upsert memo organizer | Failed to find memo by ID: %v | Failed to compose memo response"
// @Router /api/v1/memo/{memoId}/organizer [POST]
func (s *APIV1Service) CreateMemoOrganizer(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
if memo == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %v", memoID))
}
if memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
request := &UpsertMemoOrganizerRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
}
upsert := &store.MemoOrganizer{
MemoID: memoID,
UserID: userID,
Pinned: request.Pinned,
}
_, err = s.Store.UpsertMemoOrganizer(ctx, upsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
}
memo, err = s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
if memo == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo not found: %v", memoID))
}
memoResponse, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
}
return c.JSON(http.StatusOK, memoResponse)
}

View File

@@ -1,156 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/store"
)
type MemoRelationType string
const (
MemoRelationReference MemoRelationType = "REFERENCE"
MemoRelationComment MemoRelationType = "COMMENT"
)
func (t MemoRelationType) String() string {
return string(t)
}
type MemoRelation struct {
MemoID int32 `json:"memoId"`
RelatedMemoID int32 `json:"relatedMemoId"`
Type MemoRelationType `json:"type"`
}
type UpsertMemoRelationRequest struct {
RelatedMemoID int32 `json:"relatedMemoId"`
Type MemoRelationType `json:"type"`
}
func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
g.GET("/memo/:memoId/relation", s.GetMemoRelationList)
g.POST("/memo/:memoId/relation", s.CreateMemoRelation)
g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", s.DeleteMemoRelation)
}
// GetMemoRelationList godoc
//
// @Summary Get a list of Memo Relations
// @Tags memo-relation
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to find relations"
// @Success 200 {object} []store.MemoRelation "Memo relation information list"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 500 {object} nil "Failed to list memo relations"
// @Router /api/v1/memo/{memoId}/relation [GET]
func (s *APIV1Service) GetMemoRelationList(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
}
return c.JSON(http.StatusOK, memoRelationList)
}
// CreateMemoRelation godoc
//
// @Summary Create Memo Relation
// @Description Create a relation between two memos
// @Tags memo-relation
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to relate"
// @Param body body UpsertMemoRelationRequest true "Memo relation object"
// @Success 200 {object} store.MemoRelation "Memo relation information"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo relation request"
// @Failure 500 {object} nil "Failed to upsert memo relation"
// @Router /api/v1/memo/{memoId}/relation [POST]
//
// NOTES:
// - Currently not secured
// - It's possible to create relations to memos that doesn't exist, which will trigger 404 errors when the frontend tries to load them.
// - It's possible to create multiple relations, though the interface only shows first.
func (s *APIV1Service) CreateMemoRelation(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
request := &UpsertMemoRelationRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo relation request").SetInternal(err)
}
memoRelation, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: memoID,
RelatedMemoID: request.RelatedMemoID,
Type: store.MemoRelationType(request.Type),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
}
return c.JSON(http.StatusOK, memoRelation)
}
// DeleteMemoRelation godoc
//
// @Summary Delete a Memo Relation
// @Description Removes a relation between two memos
// @Tags memo-relation
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to find relations"
// @Param relatedMemoId path int true "ID of memo to remove relation to"
// @Param relationType path MemoRelationType true "Type of relation to remove"
// @Success 200 {boolean} true "Memo relation deleted"
// @Failure 400 {object} nil "Memo ID is not a number: %s | Related memo ID is not a number: %s"
// @Failure 500 {object} nil "Failed to delete memo relation"
// @Router /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType} [DELETE]
//
// NOTES:
// - Currently not secured.
// - Will always return true, even if the relation doesn't exist.
func (s *APIV1Service) DeleteMemoRelation(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
relatedMemoID, err := util.ConvertStringToInt32(c.Param("relatedMemoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("relatedMemoId"))).SetInternal(err)
}
relationType := store.MemoRelationType(c.Param("relationType"))
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
MemoID: &memoID,
RelatedMemoID: &relatedMemoID,
Type: &relationType,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *MemoRelation {
return &MemoRelation{
MemoID: memoRelation.MemoID,
RelatedMemoID: memoRelation.RelatedMemoID,
Type: MemoRelationType(memoRelation.Type),
}
}

View File

@@ -1,487 +0,0 @@
package v1
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/storage/s3"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
type Resource struct {
ID int32 `json:"id"`
// Standard fields
CreatorID int32 `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"-"`
InternalPath string `json:"-"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"size"`
}
type CreateResourceRequest struct {
Filename string `json:"filename"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
}
type FindResourceRequest struct {
ID *int32 `json:"id"`
CreatorID *int32 `json:"creatorId"`
Filename *string `json:"filename"`
}
type UpdateResourceRequest struct {
Filename *string `json:"filename"`
}
const (
// The upload memory buffer is 32 MiB.
// It should be kept low, so RAM usage doesn't get out of control.
// This is unrelated to maximum upload size limit, which is now set through system setting.
maxUploadBufferSizeBytes = 32 << 20
MebiByte = 1024 * 1024
)
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
g.GET("/resource", s.GetResourceList)
g.POST("/resource", s.CreateResource)
g.POST("/resource/blob", s.UploadResource)
g.PATCH("/resource/:resourceId", s.UpdateResource)
g.DELETE("/resource/:resourceId", s.DeleteResource)
}
// GetResourceList godoc
//
// @Summary Get a list of resources
// @Tags resource
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} []store.Resource "Resource list"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to fetch resource list"
// @Router /api/v1/resource [GET]
func (s *APIV1Service) GetResourceList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
find := &store.FindResource{
CreatorID: &userID,
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
find.Limit = &limit
}
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
find.Offset = &offset
}
list, err := s.Store.ListResources(ctx, find)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
resourceMessageList := []*Resource{}
for _, resource := range list {
resourceMessageList = append(resourceMessageList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, resourceMessageList)
}
// CreateResource godoc
//
// @Summary Create resource
// @Tags resource
// @Accept json
// @Produce json
// @Param body body CreateResourceRequest true "Request object."
// @Success 200 {object} store.Resource "Created resource"
// @Failure 400 {object} nil "Malformatted post resource request | Invalid external link | Invalid external link scheme | Failed to request %s | Failed to read %s | Failed to read mime from %s"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to save resource | Failed to create resource | Failed to create activity"
// @Router /api/v1/resource [POST]
func (s *APIV1Service) CreateResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
request := &CreateResourceRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
}
create := &store.Resource{
CreatorID: userID,
Filename: request.Filename,
ExternalLink: request.ExternalLink,
Type: request.Type,
}
if request.ExternalLink != "" {
// Only allow those external links scheme with http/https
linkURL, err := url.Parse(request.ExternalLink)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err)
}
if linkURL.Scheme != "http" && linkURL.Scheme != "https" {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme")
}
}
resource, err := s.Store.CreateResource(ctx, create)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
metric.Enqueue("resource create")
return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}
// UploadResource godoc
//
// @Summary Upload resource
// @Tags resource
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "File to upload"
// @Success 200 {object} store.Resource "Created resource"
// @Failure 400 {object} nil "Upload file not found | File size exceeds allowed limit of %d MiB | Failed to parse upload data"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to get uploading file | Failed to open file | Failed to save resource | Failed to create resource | Failed to create activity"
// @Router /api/v1/resource/blob [POST]
func (s *APIV1Service) UploadResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
// This is the backend default max upload size limit.
maxUploadSetting := s.Store.GetSystemSettingValueWithDefault(ctx, SystemSettingMaxUploadSizeMiBName.String(), "32")
var settingMaxUploadSizeBytes int
if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil {
settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
} else {
log.Warn("Failed to parse max upload size", zap.Error(err))
settingMaxUploadSizeBytes = 0
}
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
}
if file == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
}
if file.Size > int64(settingMaxUploadSizeBytes) {
message := fmt.Sprintf("File size exceeds allowed limit of %d MiB", settingMaxUploadSizeBytes/MebiByte)
return echo.NewHTTPError(http.StatusBadRequest, message).SetInternal(err)
}
if err := c.Request().ParseMultipartForm(maxUploadBufferSizeBytes); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse upload data").SetInternal(err)
}
sourceFile, err := file.Open()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to open file").SetInternal(err)
}
defer sourceFile.Close()
create := &store.Resource{
CreatorID: userID,
Filename: file.Filename,
Type: file.Header.Get("Content-Type"),
Size: file.Size,
}
err = SaveResourceBlob(ctx, s.Store, create, sourceFile)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save resource").SetInternal(err)
}
resource, err := s.Store.CreateResource(ctx, create)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}
// DeleteResource godoc
//
// @Summary Delete a resource
// @Tags resource
// @Produce json
// @Param resourceId path int true "Resource ID"
// @Success 200 {boolean} true "Resource deleted"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 404 {object} nil "Resource not found: %d"
// @Failure 500 {object} nil "Failed to find resource | Failed to delete resource"
// @Router /api/v1/resource/{resourceId} [DELETE]
func (s *APIV1Service) DeleteResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID,
CreatorID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
}
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
ID: resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// UpdateResource godoc
//
// @Summary Update a resource
// @Tags resource
// @Produce json
// @Param resourceId path int true "Resource ID"
// @Param patch body UpdateResourceRequest true "Patch resource request"
// @Success 200 {object} store.Resource "Updated resource"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch resource request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Resource not found: %d"
// @Failure 500 {object} nil "Failed to find resource | Failed to patch resource"
// @Router /api/v1/resource/{resourceId} [PATCH]
func (s *APIV1Service) UpdateResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
}
if resource.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
request := &UpdateResourceRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
}
currentTs := time.Now().Unix()
update := &store.UpdateResource{
ID: resourceID,
UpdatedTs: &currentTs,
}
if request.Filename != nil && *request.Filename != "" {
update.Filename = request.Filename
}
resource, err = s.Store.UpdateResource(ctx, update)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
}
return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}
func replacePathTemplate(path, filename string) string {
t := time.Now()
path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
switch s {
case "{filename}":
return filename
case "{timestamp}":
return fmt.Sprintf("%d", t.Unix())
case "{year}":
return fmt.Sprintf("%d", t.Year())
case "{month}":
return fmt.Sprintf("%02d", t.Month())
case "{day}":
return fmt.Sprintf("%02d", t.Day())
case "{hour}":
return fmt.Sprintf("%02d", t.Hour())
case "{minute}":
return fmt.Sprintf("%02d", t.Minute())
case "{second}":
return fmt.Sprintf("%02d", t.Second())
}
return s
})
return path
}
func convertResourceFromStore(resource *store.Resource) *Resource {
return &Resource{
ID: resource.ID,
CreatorID: resource.CreatorID,
CreatedTs: resource.CreatedTs,
UpdatedTs: resource.UpdatedTs,
Filename: resource.Filename,
Blob: resource.Blob,
InternalPath: resource.InternalPath,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
}
}
// SaveResourceBlob save the blob of resource based on the storage config
//
// Depend on the storage config, some fields of *store.ResourceCreate will be changed:
// 1. *DatabaseStorage*: `create.Blob`.
// 2. *LocalStorage*: `create.InternalPath`.
// 3. Others( external service): `create.ExternalLink`.
func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource, r io.Reader) error {
systemSettingStorageServiceID, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
if err != nil {
return errors.Wrap(err, "Failed to find SystemSettingStorageServiceIDName")
}
storageServiceID := DefaultStorage
if systemSettingStorageServiceID != nil {
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal storage service id")
}
}
// `DatabaseStorage` means store blob into database
if storageServiceID == DatabaseStorage {
fileBytes, err := io.ReadAll(r)
if err != nil {
return errors.Wrap(err, "Failed to read file")
}
create.Blob = fileBytes
return nil
} else if storageServiceID == LocalStorage {
// `LocalStorage` means save blob into local disk
systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
if err != nil {
return errors.Wrap(err, "Failed to find SystemSettingLocalStoragePathName")
}
localStoragePath := "assets/{timestamp}_{filename}"
if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
if err != nil {
return errors.Wrap(err, "Failed to unmarshal SystemSettingLocalStoragePathName")
}
}
filePath := filepath.FromSlash(localStoragePath)
if !strings.Contains(filePath, "{filename}") {
filePath = filepath.Join(filePath, "{filename}")
}
filePath = filepath.Join(s.Profile.Data, replacePathTemplate(filePath, create.Filename))
dir := filepath.Dir(filePath)
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return errors.Wrap(err, "Failed to create directory")
}
dst, err := os.Create(filePath)
if err != nil {
return errors.Wrap(err, "Failed to create file")
}
defer dst.Close()
_, err = io.Copy(dst, r)
if err != nil {
return errors.Wrap(err, "Failed to copy file")
}
create.InternalPath = filePath
return nil
}
// Others: store blob into external service, such as S3
storage, err := s.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
if err != nil {
return errors.Wrap(err, "Failed to find StorageServiceID")
}
if storage == nil {
return errors.Errorf("Storage %d not found", storageServiceID)
}
storageMessage, err := ConvertStorageFromStore(storage)
if err != nil {
return errors.Wrap(err, "Failed to ConvertStorageFromStore")
}
if storageMessage.Type != StorageS3 {
return errors.Errorf("Unsupported storage type: %s", storageMessage.Type)
}
s3Config := storageMessage.Config.S3Config
s3Client, err := s3.NewClient(ctx, &s3.Config{
AccessKey: s3Config.AccessKey,
SecretKey: s3Config.SecretKey,
EndPoint: s3Config.EndPoint,
Region: s3Config.Region,
Bucket: s3Config.Bucket,
URLPrefix: s3Config.URLPrefix,
URLSuffix: s3Config.URLSuffix,
})
if err != nil {
return errors.Wrap(err, "Failed to create s3 client")
}
filePath := s3Config.Path
if !strings.Contains(filePath, "{filename}") {
filePath = filepath.Join(filePath, "{filename}")
}
filePath = replacePathTemplate(filePath, create.Filename)
link, err := s3Client.UploadFile(ctx, filePath, create.Type, r)
if err != nil {
return errors.Wrap(err, "Failed to upload via s3 client")
}
create.ExternalLink = link
return nil
}

View File

@@ -1,204 +0,0 @@
package v1
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/feeds"
"github.com/labstack/echo/v4"
"github.com/yuin/goldmark"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/store"
)
const maxRSSItemCount = 100
const maxRSSItemTitleLength = 100
func (s *APIV1Service) registerRSSRoutes(g *echo.Group) {
g.GET("/explore/rss.xml", s.GetExploreRSS)
g.GET("/u/:id/rss.xml", s.GetUserRSS)
}
// GetExploreRSS godoc
//
// @Summary Get RSS
// @Tags rss
// @Produce xml
// @Success 200 {object} nil "RSS"
// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
// @Router /explore/rss.xml [GET]
func (s *APIV1Service) GetExploreRSS(c echo.Context) error {
ctx := c.Request().Context()
systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
}
normalStatus := store.Normal
memoFind := store.FindMemo{
RowStatus: &normalStatus,
VisibilityList: []store.Visibility{store.Public},
}
memoList, err := s.Store.ListMemos(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 := s.generateRSSFromMemoList(ctx, 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)
}
// GetUserRSS godoc
//
// @Summary Get RSS for a user
// @Tags rss
// @Produce xml
// @Param id path int true "User ID"
// @Success 200 {object} nil "RSS"
// @Failure 400 {object} nil "User id is not a number"
// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
// @Router /u/{id}/rss.xml [GET]
func (s *APIV1Service) GetUserRSS(c echo.Context) error {
ctx := c.Request().Context()
id, err := util.ConvertStringToInt32(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
}
systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get system customized profile").SetInternal(err)
}
normalStatus := store.Normal
memoFind := store.FindMemo{
CreatorID: &id,
RowStatus: &normalStatus,
VisibilityList: []store.Visibility{store.Public},
}
memoList, err := s.Store.ListMemos(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 := s.generateRSSFromMemoList(ctx, 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)
}
func (s *APIV1Service) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, profile *CustomizedProfile) (string, error) {
feed := &feeds.Feed{
Title: profile.Name,
Link: &feeds.Link{Href: baseURL},
Description: profile.Description,
Created: time.Now(),
}
var itemCountLimit = util.Min(len(memoList), maxRSSItemCount)
feed.Items = make([]*feeds.Item, itemCountLimit)
for i := 0; i < itemCountLimit; i++ {
memoMessage, err := s.convertMemoFromStore(ctx, memoList[i])
if err != nil {
return "", err
}
feed.Items[i] = &feeds.Item{
Title: getRSSItemTitle(memoMessage.Content),
Link: &feeds.Link{Href: baseURL + "/m/" + fmt.Sprintf("%d", memoMessage.ID)},
Description: getRSSItemDescription(memoMessage.Content),
Created: time.Unix(memoMessage.CreatedTs, 0),
Enclosure: &feeds.Enclosure{Url: baseURL + "/m/" + fmt.Sprintf("%d", memoMessage.ID) + "/image"},
}
if len(memoMessage.ResourceList) > 0 {
resource := memoMessage.ResourceList[0]
enclosure := feeds.Enclosure{}
if resource.ExternalLink != "" {
enclosure.Url = resource.ExternalLink
} else {
enclosure.Url = baseURL + "/o/r/" + fmt.Sprintf("%d", resource.ID)
}
enclosure.Length = strconv.Itoa(int(resource.Size))
enclosure.Type = resource.Type
feed.Items[i].Enclosure = &enclosure
}
}
rss, err := feed.ToRss()
if err != nil {
return "", err
}
return rss, nil
}
func (s *APIV1Service) getSystemCustomizedProfile(ctx context.Context) (*CustomizedProfile, error) {
systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingCustomizedProfileName.String(),
})
if err != nil {
return nil, err
}
customizedProfile := &CustomizedProfile{
Name: "memos",
LogoURL: "",
Description: "",
Locale: "en",
Appearance: "system",
ExternalURL: "",
}
if systemSetting != nil {
if err := json.Unmarshal([]byte(systemSetting.Value), customizedProfile); err != nil {
return nil, err
}
}
return customizedProfile, nil
}
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 = util.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
}
// TODO: use our `./plugin/gomark` parser to handle markdown-like content.
var buf bytes.Buffer
if err := goldmark.Convert([]byte(description), &buf); err != nil {
panic(err)
}
return buf.String()
}
func isTitleDefined(content string) bool {
return strings.HasPrefix(content, "# ")
}

View File

@@ -1,315 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/store"
)
const (
// LocalStorage means the storage service is local file system.
LocalStorage int32 = -1
// DatabaseStorage means the storage service is database.
DatabaseStorage int32 = 0
// Default storage service is database.
DefaultStorage int32 = DatabaseStorage
)
type StorageType string
const (
StorageS3 StorageType = "S3"
)
func (t StorageType) String() string {
return string(t)
}
type StorageConfig struct {
S3Config *StorageS3Config `json:"s3Config"`
}
type StorageS3Config struct {
EndPoint string `json:"endPoint"`
Path string `json:"path"`
Region string `json:"region"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
URLPrefix string `json:"urlPrefix"`
URLSuffix string `json:"urlSuffix"`
}
type Storage struct {
ID int32 `json:"id"`
Name string `json:"name"`
Type StorageType `json:"type"`
Config *StorageConfig `json:"config"`
}
type CreateStorageRequest struct {
Name string `json:"name"`
Type StorageType `json:"type"`
Config *StorageConfig `json:"config"`
}
type UpdateStorageRequest struct {
Type StorageType `json:"type"`
Name *string `json:"name"`
Config *StorageConfig `json:"config"`
}
func (s *APIV1Service) registerStorageRoutes(g *echo.Group) {
g.GET("/storage", s.GetStorageList)
g.POST("/storage", s.CreateStorage)
g.PATCH("/storage/:storageId", s.UpdateStorage)
g.DELETE("/storage/:storageId", s.DeleteStorage)
}
// GetStorageList godoc
//
// @Summary Get a list of storages
// @Tags storage
// @Produce json
// @Success 200 {object} []store.Storage "List of storages"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to convert storage"
// @Router /api/v1/storage [GET]
func (s *APIV1Service) GetStorageList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
// We should only show storage list to host user.
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
list, err := s.Store.ListStorages(ctx, &store.FindStorage{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
}
storageList := []*Storage{}
for _, storage := range list {
storageMessage, err := ConvertStorageFromStore(storage)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
}
storageList = append(storageList, storageMessage)
}
return c.JSON(http.StatusOK, storageList)
}
// CreateStorage godoc
//
// @Summary Create storage
// @Tags storage
// @Accept json
// @Produce json
// @Param body body CreateStorageRequest true "Request object."
// @Success 200 {object} store.Storage "Created storage"
// @Failure 400 {object} nil "Malformatted post storage request"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to find user | Failed to create storage | Failed to convert storage"
// @Router /api/v1/storage [POST]
func (s *APIV1Service) CreateStorage(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
create := &CreateStorageRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
}
configString := ""
if create.Type == StorageS3 && create.Config.S3Config != nil {
configBytes, err := json.Marshal(create.Config.S3Config)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
}
configString = string(configBytes)
}
storage, err := s.Store.CreateStorage(ctx, &store.Storage{
Name: create.Name,
Type: create.Type.String(),
Config: configString,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
}
storageMessage, err := ConvertStorageFromStore(storage)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
}
return c.JSON(http.StatusOK, storageMessage)
}
// DeleteStorage godoc
//
// @Summary Delete a storage
// @Tags storage
// @Produce json
// @Param storageId path int true "Storage ID"
// @Success 200 {boolean} true "Storage deleted"
// @Failure 400 {object} nil "ID is not a number: %s | Storage service %d is using"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
// @Router /api/v1/storage/{storageId} [DELETE]
//
// NOTES:
// - error message "Storage service %d is using" probably should be "Storage service %d is in use".
func (s *APIV1Service) DeleteStorage(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
}
systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
}
if systemSetting != nil {
storageServiceID := DefaultStorage
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
}
if storageServiceID == storageID {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
}
}
if err = s.Store.DeleteStorage(ctx, &store.DeleteStorage{ID: storageID}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// UpdateStorage godoc
//
// @Summary Update a storage
// @Tags storage
// @Produce json
// @Param storageId path int true "Storage ID"
// @Param patch body UpdateStorageRequest true "Patch request"
// @Success 200 {object} store.Storage "Updated resource"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to patch storage | Failed to convert storage"
// @Router /api/v1/storage/{storageId} [PATCH]
func (s *APIV1Service) UpdateStorage(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
}
update := &UpdateStorageRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(update); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
}
storageUpdate := &store.UpdateStorage{
ID: storageID,
}
if update.Name != nil {
storageUpdate.Name = update.Name
}
if update.Config != nil {
if update.Type == StorageS3 {
configBytes, err := json.Marshal(update.Config.S3Config)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
}
configString := string(configBytes)
storageUpdate.Config = &configString
}
}
storage, err := s.Store.UpdateStorage(ctx, storageUpdate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
}
storageMessage, err := ConvertStorageFromStore(storage)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
}
return c.JSON(http.StatusOK, storageMessage)
}
func ConvertStorageFromStore(storage *store.Storage) (*Storage, error) {
storageMessage := &Storage{
ID: storage.ID,
Name: storage.Name,
Type: StorageType(storage.Type),
Config: &StorageConfig{},
}
if storageMessage.Type == StorageS3 {
s3Config := &StorageS3Config{}
if err := json.Unmarshal([]byte(storage.Config), s3Config); err != nil {
return nil, err
}
storageMessage.Config = &StorageConfig{
S3Config: s3Config,
}
}
return storageMessage, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,183 +0,0 @@
package v1
import (
"encoding/json"
"net/http"
"github.com/labstack/echo/v4"
"go.uber.org/zap"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
type SystemStatus struct {
Host *User `json:"host"`
Profile profile.Profile `json:"profile"`
DBSize int64 `json:"dbSize"`
// System settings
// Allow sign up.
AllowSignUp bool `json:"allowSignUp"`
// Disable password login.
DisablePasswordLogin bool `json:"disablePasswordLogin"`
// Disable public memos.
DisablePublicMemos bool `json:"disablePublicMemos"`
// Max upload size.
MaxUploadSizeMiB int `json:"maxUploadSizeMiB"`
// Auto Backup Interval.
AutoBackupInterval int `json:"autoBackupInterval"`
// 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"`
// Storage service ID.
StorageServiceID int32 `json:"storageServiceId"`
// Local storage path.
LocalStoragePath string `json:"localStoragePath"`
// Memo display with updated timestamp.
MemoDisplayWithUpdatedTs bool `json:"memoDisplayWithUpdatedTs"`
}
func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
g.GET("/ping", s.PingSystem)
g.GET("/status", s.GetSystemStatus)
g.POST("/system/vacuum", s.ExecVacuum)
}
// PingSystem godoc
//
// @Summary Ping the system
// @Tags system
// @Produce json
// @Success 200 {boolean} true "If succeed to ping the system"
// @Router /api/v1/ping [GET]
func (*APIV1Service) PingSystem(c echo.Context) error {
return c.JSON(http.StatusOK, true)
}
// GetSystemStatus godoc
//
// @Summary Get system GetSystemStatus
// @Tags system
// @Produce json
// @Success 200 {object} SystemStatus "System GetSystemStatus"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find host user | Failed to find system setting list | Failed to unmarshal system setting customized profile value"
// @Router /api/v1/status [GET]
func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
ctx := c.Request().Context()
systemStatus := SystemStatus{
Profile: profile.Profile{
Mode: s.Profile.Mode,
Version: s.Profile.Version,
},
// Allow sign up by default.
AllowSignUp: true,
MaxUploadSizeMiB: 32,
CustomizedProfile: CustomizedProfile{
Name: "memos",
Locale: "en",
Appearance: "system",
},
StorageServiceID: DefaultStorage,
LocalStoragePath: "assets/{timestamp}_{filename}",
}
hostUserType := store.RoleHost
hostUser, err := s.Store.GetUser(ctx, &store.FindUser{
Role: &hostUserType,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
if hostUser != nil {
systemStatus.Host = &User{ID: hostUser.ID}
}
systemSettingList, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
for _, systemSetting := range systemSettingList {
if systemSetting.Name == SystemSettingServerIDName.String() || systemSetting.Name == SystemSettingSecretSessionName.String() || systemSetting.Name == SystemSettingTelegramBotTokenName.String() {
continue
}
var baseValue any
err := json.Unmarshal([]byte(systemSetting.Value), &baseValue)
if err != nil {
log.Warn("Failed to unmarshal system setting value", zap.String("setting name", systemSetting.Name))
continue
}
switch systemSetting.Name {
case SystemSettingAllowSignUpName.String():
systemStatus.AllowSignUp = baseValue.(bool)
case SystemSettingDisablePasswordLoginName.String():
systemStatus.DisablePasswordLogin = baseValue.(bool)
case SystemSettingDisablePublicMemosName.String():
systemStatus.DisablePublicMemos = baseValue.(bool)
case SystemSettingMaxUploadSizeMiBName.String():
systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
case SystemSettingAutoBackupIntervalName.String():
systemStatus.AutoBackupInterval = int(baseValue.(float64))
case SystemSettingAdditionalStyleName.String():
systemStatus.AdditionalStyle = baseValue.(string)
case SystemSettingAdditionalScriptName.String():
systemStatus.AdditionalScript = baseValue.(string)
case SystemSettingCustomizedProfileName.String():
customizedProfile := CustomizedProfile{}
if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting customized profile value").SetInternal(err)
}
systemStatus.CustomizedProfile = customizedProfile
case SystemSettingStorageServiceIDName.String():
systemStatus.StorageServiceID = int32(baseValue.(float64))
case SystemSettingLocalStoragePathName.String():
systemStatus.LocalStoragePath = baseValue.(string)
case SystemSettingMemoDisplayWithUpdatedTsName.String():
systemStatus.MemoDisplayWithUpdatedTs = baseValue.(bool)
default:
log.Warn("Unknown system setting name", zap.String("setting name", systemSetting.Name))
}
}
return c.JSON(http.StatusOK, systemStatus)
}
// ExecVacuum godoc
//
// @Summary Vacuum the database
// @Tags system
// @Produce json
// @Success 200 {boolean} true "Database vacuumed"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to ExecVacuum database"
// @Router /api/v1/system/vacuum [POST]
func (s *APIV1Service) ExecVacuum(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
if err := s.Store.Vacuum(ctx); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}

View File

@@ -1,286 +0,0 @@
package v1
import (
"encoding/json"
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/usememos/memos/store"
)
type SystemSettingName string
const (
// SystemSettingServerIDName is the name of server id.
SystemSettingServerIDName SystemSettingName = "server-id"
// SystemSettingSecretSessionName is the name of secret session.
SystemSettingSecretSessionName SystemSettingName = "secret-session"
// SystemSettingAllowSignUpName is the name of allow signup setting.
SystemSettingAllowSignUpName SystemSettingName = "allow-signup"
// SystemSettingDisablePasswordLoginName is the name of disable password login setting.
SystemSettingDisablePasswordLoginName SystemSettingName = "disable-password-login"
// SystemSettingDisablePublicMemosName is the name of disable public memos setting.
SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos"
// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
// SystemSettingAdditionalStyleName is the name of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additional-style"
// SystemSettingAdditionalScriptName is the name of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additional-script"
// SystemSettingCustomizedProfileName is the name of customized server profile.
SystemSettingCustomizedProfileName SystemSettingName = "customized-profile"
// SystemSettingStorageServiceIDName is the name of storage service ID.
SystemSettingStorageServiceIDName SystemSettingName = "storage-service-id"
// SystemSettingLocalStoragePathName is the name of local storage path.
SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path"
// SystemSettingTelegramBotTokenName is the name of Telegram Bot Token.
SystemSettingTelegramBotTokenName SystemSettingName = "telegram-bot-token"
// SystemSettingMemoDisplayWithUpdatedTsName is the name of memo display with updated ts.
SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts"
// SystemSettingAutoBackupIntervalName is the name of auto backup interval as seconds.
SystemSettingAutoBackupIntervalName SystemSettingName = "auto-backup-interval"
)
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
type CustomizedProfile struct {
// Name is the server name, default is `memos`
Name string `json:"name"`
// LogoURL is the url of logo image.
LogoURL string `json:"logoUrl"`
// Description is the server description.
Description string `json:"description"`
// Locale is the server default locale.
Locale string `json:"locale"`
// Appearance is the server default appearance.
Appearance string `json:"appearance"`
// ExternalURL is the external url of server. e.g. https://usermemos.com
ExternalURL string `json:"externalUrl"`
}
func (key SystemSettingName) String() string {
return string(key)
}
type SystemSetting struct {
Name SystemSettingName `json:"name"`
// Value is a JSON string with basic value.
Value string `json:"value"`
Description string `json:"description"`
}
type UpsertSystemSettingRequest struct {
Name SystemSettingName `json:"name"`
Value string `json:"value"`
Description string `json:"description"`
}
func (s *APIV1Service) registerSystemSettingRoutes(g *echo.Group) {
g.GET("/system/setting", s.GetSystemSettingList)
g.POST("/system/setting", s.CreateSystemSetting)
}
// GetSystemSettingList godoc
//
// @Summary Get a list of system settings
// @Tags system-setting
// @Produce json
// @Success 200 {object} []SystemSetting "System setting list"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to find system setting list"
// @Router /api/v1/system/setting [GET]
func (s *APIV1Service) GetSystemSettingList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
list, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
systemSettingList := make([]*SystemSetting, 0, len(list))
for _, systemSetting := range list {
systemSettingList = append(systemSettingList, convertSystemSettingFromStore(systemSetting))
}
return c.JSON(http.StatusOK, systemSettingList)
}
// CreateSystemSetting godoc
//
// @Summary Create system setting
// @Tags system-setting
// @Accept json
// @Produce json
// @Param body body UpsertSystemSettingRequest true "Request object."
// @Success 200 {object} store.SystemSetting "Created system setting"
// @Failure 400 {object} nil "Malformatted post system setting request | invalid system setting"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 403 {object} nil "Cannot disable passwords if no SSO identity provider is configured."
// @Failure 500 {object} nil "Failed to find user | Failed to upsert system setting"
// @Router /api/v1/system/setting [POST]
func (s *APIV1Service) CreateSystemSetting(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
systemSettingUpsert := &UpsertSystemSettingRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
}
if err := systemSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
if systemSettingUpsert.Name == SystemSettingDisablePasswordLoginName {
var disablePasswordLogin bool
if err := json.Unmarshal([]byte(systemSettingUpsert.Value), &disablePasswordLogin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
identityProviderList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
if disablePasswordLogin && len(identityProviderList) == 0 {
return echo.NewHTTPError(http.StatusForbidden, "Cannot disable passwords if no SSO identity provider is configured.")
}
}
systemSetting, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: systemSettingUpsert.Name.String(),
Value: systemSettingUpsert.Value,
Description: systemSettingUpsert.Description,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
return c.JSON(http.StatusOK, convertSystemSettingFromStore(systemSetting))
}
func (upsert UpsertSystemSettingRequest) Validate() error {
switch settingName := upsert.Name; settingName {
case SystemSettingServerIDName:
return errors.Errorf("updating %v is not allowed", settingName)
case SystemSettingAllowSignUpName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingDisablePasswordLoginName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingDisablePublicMemosName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingMaxUploadSizeMiBName:
var value int
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingAdditionalStyleName:
var value string
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingAdditionalScriptName:
var value string
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingCustomizedProfileName:
customizedProfile := CustomizedProfile{
Name: "memos",
LogoURL: "",
Description: "",
Locale: "en",
Appearance: "system",
ExternalURL: "",
}
if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingStorageServiceIDName:
// Note: 0 is the default value(database) for storage service ID.
value := 0
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
return nil
case SystemSettingLocalStoragePathName:
value := ""
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingAutoBackupIntervalName:
var value int
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
if value < 0 {
return errors.New("must be positive")
}
case SystemSettingTelegramBotTokenName:
if upsert.Value == "" {
return nil
}
// Bot Token with Reverse Proxy shoule like `http.../bot<token>`
if strings.HasPrefix(upsert.Value, "http") {
slashIndex := strings.LastIndexAny(upsert.Value, "/")
if strings.HasPrefix(upsert.Value[slashIndex:], "/bot") {
return nil
}
return errors.New("token start with `http` must end with `/bot<token>`")
}
fragments := strings.Split(upsert.Value, ":")
if len(fragments) != 2 {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingMemoDisplayWithUpdatedTsName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
default:
return errors.New("invalid system setting name")
}
return nil
}
func convertSystemSettingFromStore(systemSetting *store.SystemSetting) *SystemSetting {
return &SystemSetting{
Name: SystemSettingName(systemSetting.Name),
Value: systemSetting.Value,
Description: systemSetting.Description,
}
}

View File

@@ -1,218 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"sort"
"github.com/labstack/echo/v4"
"golang.org/x/exp/slices"
"github.com/usememos/memos/store"
)
type Tag struct {
Name string
CreatorID int32
}
type UpsertTagRequest struct {
Name string `json:"name"`
}
type DeleteTagRequest struct {
Name string `json:"name"`
}
func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
g.GET("/tag", s.GetTagList)
g.POST("/tag", s.CreateTag)
g.GET("/tag/suggestion", s.GetTagSuggestion)
g.POST("/tag/delete", s.DeleteTag)
}
// GetTagList godoc
//
// @Summary Get a list of tags
// @Tags tag
// @Produce json
// @Success 200 {object} []string "Tag list"
// @Failure 400 {object} nil "Missing user id to find tag"
// @Failure 500 {object} nil "Failed to find tag list"
// @Router /api/v1/tag [GET]
func (s *APIV1Service) GetTagList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
}
list, err := s.Store.ListTags(ctx, &store.FindTag{
CreatorID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
}
tagNameList := []string{}
for _, tag := range list {
tagNameList = append(tagNameList, tag.Name)
}
return c.JSON(http.StatusOK, tagNameList)
}
// CreateTag godoc
//
// @Summary Create a tag
// @Tags tag
// @Accept json
// @Produce json
// @Param body body UpsertTagRequest true "Request object."
// @Success 200 {object} string "Created tag name"
// @Failure 400 {object} nil "Malformatted post tag request | Tag name shouldn't be empty"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to upsert tag | Failed to create activity"
// @Router /api/v1/tag [POST]
func (s *APIV1Service) CreateTag(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
tagUpsert := &UpsertTagRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
}
if tagUpsert.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
}
tag, err := s.Store.UpsertTag(ctx, &store.Tag{
Name: tagUpsert.Name,
CreatorID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
}
tagMessage := convertTagFromStore(tag)
return c.JSON(http.StatusOK, tagMessage.Name)
}
// DeleteTag godoc
//
// @Summary Delete a tag
// @Tags tag
// @Accept json
// @Produce json
// @Param body body DeleteTagRequest true "Request object."
// @Success 200 {boolean} true "Tag deleted"
// @Failure 400 {object} nil "Malformatted post tag request | Tag name shouldn't be empty"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to delete tag name: %v"
// @Router /api/v1/tag/delete [POST]
func (s *APIV1Service) DeleteTag(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
tagDelete := &DeleteTagRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
}
if tagDelete.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
}
err := s.Store.DeleteTag(ctx, &store.DeleteTag{
Name: tagDelete.Name,
CreatorID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// GetTagSuggestion godoc
//
// @Summary Get a list of tags suggested from other memos contents
// @Tags tag
// @Produce json
// @Success 200 {object} []string "Tag list"
// @Failure 400 {object} nil "Missing user session"
// @Failure 500 {object} nil "Failed to find memo list | Failed to find tag list"
// @Router /api/v1/tag/suggestion [GET]
func (s *APIV1Service) GetTagSuggestion(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
}
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &userID,
ContentSearch: []string{"#"},
RowStatus: &normalRowStatus,
}
memoMessageList, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
list, err := s.Store.ListTags(ctx, &store.FindTag{
CreatorID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
}
tagNameList := []string{}
for _, tag := range list {
tagNameList = append(tagNameList, tag.Name)
}
tagMapSet := make(map[string]bool)
for _, memo := range memoMessageList {
for _, tag := range findTagListFromMemoContent(memo.Content) {
if !slices.Contains(tagNameList, tag) {
tagMapSet[tag] = true
}
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return c.JSON(http.StatusOK, tagList)
}
func convertTagFromStore(tag *store.Tag) *Tag {
return &Tag{
Name: tag.Name,
CreatorID: tag.CreatorID,
}
}
var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)
func findTagListFromMemoContent(memoContent string) []string {
tagMapSet := make(map[string]bool)
matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
for _, v := range matches {
tagName := v[1]
tagMapSet[tagName] = true
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return tagList
}

View File

@@ -1,47 +0,0 @@
package v1
import (
"testing"
)
func TestFindTagListFromMemoContent(t *testing.T) {
tests := []struct {
memoContent string
want []string
}{
{
memoContent: "#tag1 ",
want: []string{"tag1"},
},
{
memoContent: "#tag1 #tag2 ",
want: []string{"tag1", "tag2"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 ",
want: []string{"tag1", "tag2", "tag3"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 123123#tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3 #tag4 http://123123.com?123123#tag2) ",
want: []string{"tag1", "tag2", "tag2)", "tag3", "tag4"},
},
}
for _, test := range tests {
result := findTagListFromMemoContent(test.memoContent)
if len(result) != len(test.want) {
t.Errorf("Find tag list %s: got result %v, want %v.", test.memoContent, result, test.want)
}
}
}

View File

@@ -1,484 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
// Role is the type of a role.
type Role string
const (
// RoleHost is the HOST role.
RoleHost Role = "HOST"
// RoleAdmin is the ADMIN role.
RoleAdmin Role = "ADMIN"
// RoleUser is the USER role.
RoleUser Role = "USER"
)
func (role Role) String() string {
return string(role)
}
type User struct {
ID int32 `json:"id"`
// Standard fields
RowStatus RowStatus `json:"rowStatus"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Username string `json:"username"`
Role Role `json:"role"`
Email string `json:"email"`
Nickname string `json:"nickname"`
PasswordHash string `json:"-"`
AvatarURL string `json:"avatarUrl"`
}
type CreateUserRequest struct {
Username string `json:"username"`
Role Role `json:"role"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Password string `json:"password"`
}
type UpdateUserRequest struct {
RowStatus *RowStatus `json:"rowStatus"`
Username *string `json:"username"`
Email *string `json:"email"`
Nickname *string `json:"nickname"`
Password *string `json:"password"`
AvatarURL *string `json:"avatarUrl"`
}
func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
g.GET("/user", s.GetUserList)
g.POST("/user", s.CreateUser)
g.GET("/user/me", s.GetCurrentUser)
// NOTE: This should be moved to /api/v2/user/:username
g.GET("/user/name/:username", s.GetUserByUsername)
g.GET("/user/:id", s.GetUserByID)
g.PATCH("/user/:id", s.UpdateUser)
g.DELETE("/user/:id", s.DeleteUser)
}
// GetUserList godoc
//
// @Summary Get a list of users
// @Tags user
// @Produce json
// @Success 200 {object} []store.User "User list"
// @Failure 500 {object} nil "Failed to fetch user list"
// @Router /api/v1/user [GET]
func (s *APIV1Service) GetUserList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to list users")
}
list, err := s.Store.ListUsers(ctx, &store.FindUser{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
}
userMessageList := make([]*User, 0, len(list))
for _, user := range list {
userMessage := convertUserFromStore(user)
// data desensitize
userMessage.Email = ""
userMessageList = append(userMessageList, userMessage)
}
return c.JSON(http.StatusOK, userMessageList)
}
// CreateUser godoc
//
// @Summary Create a user
// @Tags user
// @Accept json
// @Produce json
// @Param body body CreateUserRequest true "Request object"
// @Success 200 {object} store.User "Created user"
// @Failure 400 {object} nil "Malformatted post user request | Invalid user create format"
// @Failure 401 {object} nil "Missing auth session | Unauthorized to create user"
// @Failure 403 {object} nil "Could not create host user"
// @Failure 500 {object} nil "Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
// @Router /api/v1/user [POST]
func (s *APIV1Service) CreateUser(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
if currentUser.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to create user")
}
userCreate := &CreateUserRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
}
if !usernameMatcher.MatchString(strings.ToLower(userCreate.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err)
}
// Disallow host user to be created.
if userCreate.Role == RoleHost {
return echo.NewHTTPError(http.StatusForbidden, "Could not create host user")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
user, err := s.Store.CreateUser(ctx, &store.User{
Username: userCreate.Username,
Role: store.Role(userCreate.Role),
Email: userCreate.Email,
Nickname: userCreate.Nickname,
PasswordHash: string(passwordHash),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
userMessage := convertUserFromStore(user)
metric.Enqueue("user create")
return c.JSON(http.StatusOK, userMessage)
}
// GetCurrentUser godoc
//
// @Summary Get current user
// @Tags user
// @Produce json
// @Success 200 {object} store.User "Current user"
// @Failure 401 {object} nil "Missing auth session"
// @Failure 500 {object} nil "Failed to find user | Failed to find userSettingList"
// @Router /api/v1/user/me [GET]
func (s *APIV1Service) GetCurrentUser(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage)
}
// GetUserByUsername godoc
//
// @Summary Get user by username
// @Tags user
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} store.User "Requested user"
// @Failure 404 {object} nil "User not found"
// @Failure 500 {object} nil "Failed to find user"
// @Router /api/v1/user/name/{username} [GET]
func (s *APIV1Service) GetUserByUsername(c echo.Context) error {
ctx := c.Request().Context()
username := c.Param("username")
user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
userMessage := convertUserFromStore(user)
// data desensitize
userMessage.Email = ""
return c.JSON(http.StatusOK, userMessage)
}
// GetUserByID godoc
//
// @Summary Get user by id
// @Tags user
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} store.User "Requested user"
// @Failure 400 {object} nil "Malformatted user id"
// @Failure 404 {object} nil "User not found"
// @Failure 500 {object} nil "Failed to find user"
// @Router /api/v1/user/{id} [GET]
func (s *APIV1Service) GetUserByID(c echo.Context) error {
ctx := c.Request().Context()
id, err := util.ConvertStringToInt32(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &id})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
userMessage := convertUserFromStore(user)
userID, ok := c.Get(userIDContextKey).(int32)
if !ok || userID != user.ID {
// Data desensitize.
userMessage.Email = ""
}
return c.JSON(http.StatusOK, userMessage)
}
// DeleteUser godoc
//
// @Summary Delete a user
// @Tags user
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {boolean} true "User deleted"
// @Failure 400 {object} nil "ID is not a number: %s | Current session user not found with ID: %d"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 403 {object} nil "Unauthorized to delete user"
// @Failure 500 {object} nil "Failed to find user | Failed to delete user"
// @Router /api/v1/user/{id} [DELETE]
func (s *APIV1Service) DeleteUser(c echo.Context) error {
ctx := c.Request().Context()
currentUserID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &currentUserID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
} else if currentUser.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete user").SetInternal(err)
}
userID, err := util.ConvertStringToInt32(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
}
userDelete := &store.DeleteUser{
ID: userID,
}
if err := s.Store.DeleteUser(ctx, userDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// UpdateUser godoc
//
// @Summary Update a user
// @Tags user
// @Produce json
// @Param id path string true "User ID"
// @Param patch body UpdateUserRequest true "Patch request"
// @Success 200 {object} store.User "Updated user"
// @Failure 400 {object} nil "ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 403 {object} nil "Unauthorized to update user"
// @Failure 500 {object} nil "Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
// @Router /api/v1/user/{id} [PATCH]
func (s *APIV1Service) UpdateUser(c echo.Context) error {
ctx := c.Request().Context()
userID, err := util.ConvertStringToInt32(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
}
currentUserID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{ID: &currentUserID})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
} else if currentUser.Role != store.RoleHost && currentUserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to update user").SetInternal(err)
}
request := &UpdateUserRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if err := request.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid update user request").SetInternal(err)
}
currentTs := time.Now().Unix()
userUpdate := &store.UpdateUser{
ID: userID,
UpdatedTs: &currentTs,
}
if request.RowStatus != nil {
rowStatus := store.RowStatus(request.RowStatus.String())
userUpdate.RowStatus = &rowStatus
}
if request.Username != nil {
if !usernameMatcher.MatchString(strings.ToLower(*request.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err)
}
userUpdate.Username = request.Username
}
if request.Email != nil {
userUpdate.Email = request.Email
}
if request.Nickname != nil {
userUpdate.Nickname = request.Nickname
}
if request.Password != nil {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*request.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
passwordHashStr := string(passwordHash)
userUpdate.PasswordHash = &passwordHashStr
}
if request.AvatarURL != nil {
userUpdate.AvatarURL = request.AvatarURL
}
user, err := s.Store.UpdateUser(ctx, userUpdate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage)
}
func (create CreateUserRequest) Validate() error {
if len(create.Username) < 3 {
return errors.New("username is too short, minimum length is 3")
}
if len(create.Username) > 32 {
return errors.New("username is too long, maximum length is 32")
}
if len(create.Password) < 3 {
return errors.New("password is too short, minimum length is 3")
}
if len(create.Password) > 512 {
return errors.New("password is too long, maximum length is 512")
}
if len(create.Nickname) > 64 {
return errors.New("nickname is too long, maximum length is 64")
}
if create.Email != "" {
if len(create.Email) > 256 {
return errors.New("email is too long, maximum length is 256")
}
if !util.ValidateEmail(create.Email) {
return errors.New("invalid email format")
}
}
return nil
}
func (update UpdateUserRequest) Validate() error {
if update.Username != nil && len(*update.Username) < 3 {
return errors.New("username is too short, minimum length is 3")
}
if update.Username != nil && len(*update.Username) > 32 {
return errors.New("username is too long, maximum length is 32")
}
if update.Password != nil && len(*update.Password) < 3 {
return errors.New("password is too short, minimum length is 3")
}
if update.Password != nil && len(*update.Password) > 512 {
return errors.New("password is too long, maximum length is 512")
}
if update.Nickname != nil && len(*update.Nickname) > 64 {
return errors.New("nickname is too long, maximum length is 64")
}
if update.AvatarURL != nil {
if len(*update.AvatarURL) > 2<<20 {
return errors.New("avatar is too large, maximum is 2MB")
}
}
if update.Email != nil && *update.Email != "" {
if len(*update.Email) > 256 {
return errors.New("email is too long, maximum length is 256")
}
if !util.ValidateEmail(*update.Email) {
return errors.New("invalid email format")
}
}
return nil
}
func convertUserFromStore(user *store.User) *User {
return &User{
ID: user.ID,
RowStatus: RowStatus(user.RowStatus),
CreatedTs: user.CreatedTs,
UpdatedTs: user.UpdatedTs,
Username: user.Username,
Role: Role(user.Role),
Email: user.Email,
Nickname: user.Nickname,
PasswordHash: user.PasswordHash,
AvatarURL: user.AvatarURL,
}
}

View File

@@ -1,94 +0,0 @@
package v1
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/usememos/memos/api/resource"
"github.com/usememos/memos/plugin/telegram"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
type APIV1Service struct {
Secret string
Profile *profile.Profile
Store *store.Store
telegramBot *telegram.Bot
}
// @title memos API
// @version 1.0
// @description A privacy-first, lightweight note-taking service.
//
// @contact.name API Support
// @contact.url https://github.com/orgs/usememos/discussions
//
// @license.name MIT License
// @license.url https://github.com/usememos/memos/blob/main/LICENSE
//
// @BasePath /
//
// @externalDocs.url https://usememos.com/
// @externalDocs.description Find out more about Memos.
func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store, telegramBot *telegram.Bot) *APIV1Service {
return &APIV1Service{
Secret: secret,
Profile: profile,
Store: store,
telegramBot: telegramBot,
}
}
func (s *APIV1Service) Register(rootGroup *echo.Group) {
// Register RSS routes.
s.registerRSSRoutes(rootGroup)
// Register API v1 routes.
apiV1Group := rootGroup.Group("/api/v1")
apiV1Group.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{Rate: 30, Burst: 100, ExpiresIn: 3 * time.Minute},
),
IdentifierExtractor: func(ctx echo.Context) (string, error) {
id := ctx.RealIP()
return id, nil
},
ErrorHandler: func(context echo.Context, err error) error {
return context.JSON(http.StatusForbidden, nil)
},
DenyHandler: func(context echo.Context, identifier string, err error) error {
return context.JSON(http.StatusTooManyRequests, nil)
},
}))
apiV1Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return JWTMiddleware(s, next, s.Secret)
})
s.registerSystemRoutes(apiV1Group)
s.registerSystemSettingRoutes(apiV1Group)
s.registerAuthRoutes(apiV1Group)
s.registerIdentityProviderRoutes(apiV1Group)
s.registerUserRoutes(apiV1Group)
s.registerTagRoutes(apiV1Group)
s.registerStorageRoutes(apiV1Group)
s.registerResourceRoutes(apiV1Group)
s.registerMemoRoutes(apiV1Group)
s.registerMemoOrganizerRoutes(apiV1Group)
s.registerMemoRelationRoutes(apiV1Group)
// Register public routes.
publicGroup := rootGroup.Group("/o")
publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return JWTMiddleware(s, next, s.Secret)
})
s.registerGetterPublicRoutes(publicGroup)
// Create and register resource public routes.
resourceService := resource.NewService(s.Profile, s.Store)
resourceService.RegisterResourcePublicRoutes(publicGroup)
// programmatically set API version same as the server version
SwaggerInfo.Version = s.Profile.Version
}

View File

@@ -1,27 +0,0 @@
package v2
import "strings"
var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v2.SystemService/GetSystemInfo": true,
"/memos.api.v2.AuthService/GetAuthStatus": true,
"/memos.api.v2.UserService/GetUser": true,
"/memos.api.v2.MemoService/ListMemos": true,
}
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
func isUnauthorizeAllowedMethod(fullMethodName string) bool {
if strings.HasPrefix(fullMethodName, "/grpc.reflection") {
return true
}
return authenticationAllowlistMethods[fullMethodName]
}
var allowedMethodsOnlyForAdmin = map[string]bool{
"/memos.api.v2.UserService/CreateUser": true,
}
// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin.
func isOnlyForAdminAllowedMethod(methodName string) bool {
return allowedMethodsOnlyForAdmin[methodName]
}

View File

@@ -1,23 +0,0 @@
package v2
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
)
func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStatusRequest) (*apiv2pb.GetAuthStatusResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not found")
}
return &apiv2pb.GetAuthStatusResponse{
User: convertUserFromStore(user),
}, nil
}

View File

@@ -1,44 +0,0 @@
package v2
import (
"context"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func convertRowStatusFromStore(rowStatus store.RowStatus) apiv2pb.RowStatus {
switch rowStatus {
case store.Normal:
return apiv2pb.RowStatus_ACTIVE
case store.Archived:
return apiv2pb.RowStatus_ARCHIVED
default:
return apiv2pb.RowStatus_ROW_STATUS_UNSPECIFIED
}
}
func convertRowStatusToStore(rowStatus apiv2pb.RowStatus) store.RowStatus {
switch rowStatus {
case apiv2pb.RowStatus_ACTIVE:
return store.Normal
case apiv2pb.RowStatus_ARCHIVED:
return store.Archived
default:
return store.Normal
}
}
func getCurrentUser(ctx context.Context, s *store.Store) (*store.User, error) {
username, ok := ctx.Value(usernameContextKey).(string)
if !ok {
return nil, nil
}
user, err := s.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, err
}
return user, nil
}

View File

@@ -1,266 +0,0 @@
package v2
import (
"context"
"github.com/google/cel-go/cel"
"github.com/pkg/errors"
v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
create := &store.Memo{
CreatorID: user.ID,
Content: request.Content,
Visibility: store.Visibility(request.Visibility.String()),
}
memo, err := s.Store.CreateMemo(ctx, create)
if err != nil {
return nil, err
}
response := &apiv2pb.CreateMemoResponse{
Memo: convertMemoFromStore(memo),
}
return response, nil
}
func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemosRequest) (*apiv2pb.ListMemosResponse, error) {
memoFind := &store.FindMemo{}
if request.Filter != "" {
filter, err := parseListMemosFilter(request.Filter)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if filter.Visibility != nil {
memoFind.VisibilityList = []store.Visibility{*filter.Visibility}
}
if filter.CreatedTsBefore != nil {
memoFind.CreatedTsBefore = filter.CreatedTsBefore
}
if filter.CreatedTsAfter != nil {
memoFind.CreatedTsAfter = filter.CreatedTsAfter
}
}
user, _ := getCurrentUser(ctx, s.Store)
// If the user is not authenticated, only public memos are visible.
if user == nil {
memoFind.VisibilityList = []store.Visibility{store.Public}
}
if request.CreatorId != nil {
memoFind.CreatorID = request.CreatorId
}
// Remove the private memos from the list if the user is not the creator.
if user != nil && request.CreatorId != nil && *request.CreatorId != user.ID {
var filteredVisibility []store.Visibility
for _, v := range memoFind.VisibilityList {
if v != store.Private {
filteredVisibility = append(filteredVisibility, v)
}
}
memoFind.VisibilityList = filteredVisibility
}
if request.PageSize != 0 {
offset := int(request.Page * request.PageSize)
limit := int(request.PageSize)
memoFind.Offset = &offset
memoFind.Limit = &limit
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, err
}
memoMessages := make([]*apiv2pb.Memo, len(memos))
for i, memo := range memos {
memoMessages[i] = convertMemoFromStore(memo)
}
response := &apiv2pb.ListMemosResponse{
Memos: memoMessages,
}
return response, nil
}
func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequest) (*apiv2pb.GetMemoResponse, error) {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &request.Id,
})
if err != nil {
return nil, err
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
if memo.Visibility != store.Public {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if memo.Visibility == store.Private && memo.CreatorID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
response := &apiv2pb.GetMemoResponse{
Memo: convertMemoFromStore(memo),
}
return response, nil
}
func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.CreateMemoCommentRequest) (*apiv2pb.CreateMemoCommentResponse, error) {
// Create the comment memo first.
createMemoResponse, err := s.CreateMemo(ctx, request.Create)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create memo")
}
// Build the relation between the comment memo and the original memo.
memo := createMemoResponse.Memo
_, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: memo.Id,
RelatedMemoID: request.Id,
Type: store.MemoRelationComment,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create memo relation")
}
response := &apiv2pb.CreateMemoCommentResponse{
Memo: memo,
}
return response, nil
}
func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.ListMemoCommentsRequest) (*apiv2pb.ListMemoCommentsResponse, error) {
memoRelationComment := store.MemoRelationComment
memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
RelatedMemoID: &request.Id,
Type: &memoRelationComment,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memo relations")
}
var memos []*apiv2pb.Memo
for _, memoRelation := range memoRelations {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoRelation.MemoID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
if memo != nil {
memos = append(memos, convertMemoFromStore(memo))
}
}
response := &apiv2pb.ListMemoCommentsResponse{
Memos: memos,
}
return response, nil
}
// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
var ListMemosFilterCELAttributes = []cel.EnvOption{
cel.Variable("visibility", cel.StringType),
cel.Variable("created_ts_before", cel.IntType),
cel.Variable("created_ts_after", cel.IntType),
}
type ListMemosFilter struct {
Visibility *store.Visibility
CreatedTsBefore *int64
CreatedTsAfter *int64
}
func parseListMemosFilter(expression string) (*ListMemosFilter, error) {
e, err := cel.NewEnv(ListMemosFilterCELAttributes...)
if err != nil {
return nil, err
}
ast, issues := e.Compile(expression)
if issues != nil {
return nil, errors.Errorf("found issue %v", issues)
}
filter := &ListMemosFilter{}
expr, err := cel.AstToParsedExpr(ast)
if err != nil {
return nil, err
}
callExpr := expr.GetExpr().GetCallExpr()
findField(callExpr, filter)
return filter, nil
}
func findField(callExpr *v1alpha1.Expr_Call, filter *ListMemosFilter) {
if len(callExpr.Args) == 2 {
idExpr := callExpr.Args[0].GetIdentExpr()
if idExpr != nil {
if idExpr.Name == "visibility" {
visibility := store.Visibility(callExpr.Args[1].GetConstExpr().GetStringValue())
filter.Visibility = &visibility
}
if idExpr.Name == "created_ts_before" {
createdTsBefore := callExpr.Args[1].GetConstExpr().GetInt64Value()
filter.CreatedTsBefore = &createdTsBefore
}
if idExpr.Name == "created_ts_after" {
createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value()
filter.CreatedTsAfter = &createdTsAfter
}
return
}
}
for _, arg := range callExpr.Args {
callExpr := arg.GetCallExpr()
if callExpr != nil {
findField(callExpr, filter)
}
}
}
func convertMemoFromStore(memo *store.Memo) *apiv2pb.Memo {
return &apiv2pb.Memo{
Id: int32(memo.ID),
RowStatus: convertRowStatusFromStore(memo.RowStatus),
CreatedTs: memo.CreatedTs,
UpdatedTs: memo.UpdatedTs,
CreatorId: int32(memo.CreatorID),
Content: memo.Content,
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
}
}
func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility {
switch visibility {
case store.Private:
return apiv2pb.Visibility_PRIVATE
case store.Protected:
return apiv2pb.Visibility_PROTECTED
case store.Public:
return apiv2pb.Visibility_PUBLIC
default:
return apiv2pb.Visibility_VISIBILITY_UNSPECIFIED
}
}

View File

@@ -1,57 +0,0 @@
package v2
import (
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/usememos/memos/internal/util"
)
const (
UserNamePrefix = "users/"
InboxNamePrefix = "inboxes/"
)
// GetNameParentTokens returns the tokens from a resource name.
func GetNameParentTokens(name string, tokenPrefixes ...string) ([]string, error) {
parts := strings.Split(name, "/")
if len(parts) != 2*len(tokenPrefixes) {
return nil, errors.Errorf("invalid request %q", name)
}
var tokens []string
for i, tokenPrefix := range tokenPrefixes {
if fmt.Sprintf("%s/", parts[2*i]) != tokenPrefix {
return nil, errors.Errorf("invalid prefix %q in request %q", tokenPrefix, name)
}
if parts[2*i+1] == "" {
return nil, errors.Errorf("invalid request %q with empty prefix %q", name, tokenPrefix)
}
tokens = append(tokens, parts[2*i+1])
}
return tokens, nil
}
// ExtractUsernameFromName returns the username from a resource name.
func ExtractUsernameFromName(name string) (string, error) {
tokens, err := GetNameParentTokens(name, UserNamePrefix)
if err != nil {
return "", err
}
return tokens[0], nil
}
// ExtractInboxIDFromName returns the inbox ID from a resource name.
func ExtractInboxIDFromName(name string) (int32, error) {
tokens, err := GetNameParentTokens(name, InboxNamePrefix)
if err != nil {
return 0, err
}
id, err := util.ConvertStringToInt32(tokens[0])
if err != nil {
return 0, errors.Errorf("invalid inbox ID %q", tokens[0])
}
return id, nil
}

View File

@@ -1,105 +0,0 @@
package v2
import (
"context"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) ListResources(ctx context.Context, _ *apiv2pb.ListResourcesRequest) (*apiv2pb.ListResourcesResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
resources, err := s.Store.ListResources(ctx, &store.FindResource{
CreatorID: &user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err)
}
response := &apiv2pb.ListResourcesResponse{}
for _, resource := range resources {
response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource))
}
return response, nil
}
func (s *APIV2Service) UpdateResource(ctx context.Context, request *apiv2pb.UpdateResourceRequest) (*apiv2pb.UpdateResourceResponse, error) {
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
}
currentTs := time.Now().Unix()
update := &store.UpdateResource{
ID: request.Resource.Id,
UpdatedTs: &currentTs,
}
for _, field := range request.UpdateMask.Paths {
if field == "filename" {
update.Filename = &request.Resource.Filename
} else if field == "memo_id" {
update.MemoID = request.Resource.MemoId
}
}
resource, err := s.Store.UpdateResource(ctx, update)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err)
}
return &apiv2pb.UpdateResourceResponse{
Resource: s.convertResourceFromStore(ctx, resource),
}, nil
}
func (s *APIV2Service) DeleteResource(ctx context.Context, request *apiv2pb.DeleteResourceRequest) (*apiv2pb.DeleteResourceResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &request.Id,
CreatorID: &user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to find resource: %v", err)
}
if resource == nil {
return nil, status.Errorf(codes.NotFound, "resource not found")
}
// Delete the resource from the database.
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
ID: resource.ID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete resource: %v", err)
}
return &apiv2pb.DeleteResourceResponse{}, nil
}
func (s *APIV2Service) convertResourceFromStore(ctx context.Context, resource *store.Resource) *apiv2pb.Resource {
var memoID *int32
if resource.MemoID != nil {
memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{
ID: resource.MemoID,
})
if memo != nil {
memoID = &memo.ID
}
}
return &apiv2pb.Resource{
Id: resource.ID,
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
MemoId: memoID,
}
}

View File

@@ -1,92 +0,0 @@
package v2
import (
"context"
"strconv"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) GetSystemInfo(ctx context.Context, _ *apiv2pb.GetSystemInfoRequest) (*apiv2pb.GetSystemInfoResponse, error) {
defaultSystemInfo := &apiv2pb.SystemInfo{}
// Get the database size if the user is a host.
currentUser, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser != nil && currentUser.Role == store.RoleHost {
size, err := s.Store.GetCurrentDBSize(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get db size: %v", err)
}
defaultSystemInfo.DbSize = size
}
response := &apiv2pb.GetSystemInfoResponse{
SystemInfo: defaultSystemInfo,
}
return response, nil
}
func (s *APIV2Service) UpdateSystemInfo(ctx context.Context, request *apiv2pb.UpdateSystemInfoRequest) (*apiv2pb.UpdateSystemInfoResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user.Role != store.RoleHost {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
}
// Update system settings.
for _, field := range request.UpdateMask.Paths {
if field == "allow_registration" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "allow-signup",
Value: strconv.FormatBool(request.SystemInfo.AllowRegistration),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update allow_registration system setting: %v", err)
}
} else if field == "disable_password_login" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "disable-password-login",
Value: strconv.FormatBool(request.SystemInfo.DisablePasswordLogin),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update disable_password_login system setting: %v", err)
}
} else if field == "additional_script" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "additional-script",
Value: request.SystemInfo.AdditionalScript,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update additional_script system setting: %v", err)
}
} else if field == "additional_style" {
_, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: "additional-style",
Value: request.SystemInfo.AdditionalStyle,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update additional_style system setting: %v", err)
}
}
}
systemInfo, err := s.GetSystemInfo(ctx, &apiv2pb.GetSystemInfoRequest{})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get system info: %v", err)
}
return &apiv2pb.UpdateSystemInfoResponse{
SystemInfo: systemInfo.SystemInfo,
}, nil
}

View File

@@ -1,181 +0,0 @@
package v2
import (
"context"
"fmt"
"regexp"
"sort"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) UpsertTag(ctx context.Context, request *apiv2pb.UpsertTagRequest) (*apiv2pb.UpsertTagResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
tag, err := s.Store.UpsertTag(ctx, &store.Tag{
Name: request.Name,
CreatorID: user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
}
t, err := s.convertTagFromStore(ctx, tag)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
}
return &apiv2pb.UpsertTagResponse{
Tag: t,
}, nil
}
func (s *APIV2Service) ListTags(ctx context.Context, request *apiv2pb.ListTagsRequest) (*apiv2pb.ListTagsResponse, error) {
username, err := ExtractUsernameFromName(request.User)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
tags, err := s.Store.ListTags(ctx, &store.FindTag{
CreatorID: user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
}
response := &apiv2pb.ListTagsResponse{}
for _, tag := range tags {
t, err := s.convertTagFromStore(ctx, tag)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
}
response.Tags = append(response.Tags, t)
}
return response, nil
}
func (s *APIV2Service) DeleteTag(ctx context.Context, request *apiv2pb.DeleteTagRequest) (*apiv2pb.DeleteTagResponse, error) {
username, err := ExtractUsernameFromName(request.Tag.Creator)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if err := s.Store.DeleteTag(ctx, &store.DeleteTag{
Name: request.Tag.Name,
CreatorID: user.ID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
}
return &apiv2pb.DeleteTagResponse{}, nil
}
func (s *APIV2Service) GetTagSuggestions(ctx context.Context, request *apiv2pb.GetTagSuggestionsRequest) (*apiv2pb.GetTagSuggestionsResponse, error) {
username, err := ExtractUsernameFromName(request.User)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &user.ID,
ContentSearch: []string{"#"},
RowStatus: &normalRowStatus,
}
memoList, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
tagList, err := s.Store.ListTags(ctx, &store.FindTag{
CreatorID: user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
}
tagNameList := []string{}
for _, tag := range tagList {
tagNameList = append(tagNameList, tag.Name)
}
tagMapSet := make(map[string]bool)
for _, memo := range memoList {
for _, tag := range findTagListFromMemoContent(memo.Content) {
if !slices.Contains(tagNameList, tag) {
tagMapSet[tag] = true
}
}
}
suggestions := []string{}
for tag := range tagMapSet {
suggestions = append(suggestions, tag)
}
sort.Strings(suggestions)
return &apiv2pb.GetTagSuggestionsResponse{
Tags: suggestions,
}, nil
}
func (s *APIV2Service) convertTagFromStore(ctx context.Context, tag *store.Tag) (*apiv2pb.Tag, error) {
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &tag.CreatorID,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get user")
}
return &apiv2pb.Tag{
Name: tag.Name,
Creator: fmt.Sprintf("%s%s", UserNamePrefix, user.Username),
}, nil
}
var tagRegexp = regexp.MustCompile(`#([^\s#,]+)`)
func findTagListFromMemoContent(memoContent string) []string {
tagMapSet := make(map[string]bool)
matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
for _, v := range matches {
tagName := v[1]
tagMapSet[tagName] = true
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return tagList
}

View File

@@ -1,126 +0,0 @@
package v2
import (
"context"
"fmt"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"github.com/labstack/echo/v4"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
)
type APIV2Service struct {
apiv2pb.UnimplementedSystemServiceServer
apiv2pb.UnimplementedAuthServiceServer
apiv2pb.UnimplementedUserServiceServer
apiv2pb.UnimplementedMemoServiceServer
apiv2pb.UnimplementedResourceServiceServer
apiv2pb.UnimplementedTagServiceServer
apiv2pb.UnimplementedInboxServiceServer
apiv2pb.UnimplementedActivityServiceServer
apiv2pb.UnimplementedWebhookServiceServer
Secret string
Profile *profile.Profile
Store *store.Store
grpcServer *grpc.Server
grpcServerPort int
}
func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store, grpcServerPort int) *APIV2Service {
grpc.EnableTracing = true
authProvider := NewGRPCAuthInterceptor(store, secret)
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
authProvider.AuthenticationInterceptor,
),
)
apiv2Service := &APIV2Service{
Secret: secret,
Profile: profile,
Store: store,
grpcServer: grpcServer,
grpcServerPort: grpcServerPort,
}
apiv2pb.RegisterSystemServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterAuthServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterUserServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterMemoServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterTagServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterResourceServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterInboxServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterActivityServiceServer(grpcServer, apiv2Service)
apiv2pb.RegisterWebhookServiceServer(grpcServer, apiv2Service)
reflection.Register(grpcServer)
return apiv2Service
}
func (s *APIV2Service) GetGRPCServer() *grpc.Server {
return s.grpcServer
}
// RegisterGateway registers the gRPC-Gateway with the given Echo instance.
func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error {
// Create a client connection to the gRPC Server we just started.
// This is where the gRPC-Gateway proxies the requests.
conn, err := grpc.DialContext(
ctx,
fmt.Sprintf(":%d", s.grpcServerPort),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return err
}
gwMux := runtime.NewServeMux()
if err := apiv2pb.RegisterSystemServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterAuthServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterUserServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterMemoServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterTagServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterResourceServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterInboxServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterActivityServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := apiv2pb.RegisterWebhookServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
e.Any("/api/v2/*", echo.WrapHandler(gwMux))
// GRPC web proxy.
options := []grpcweb.Option{
grpcweb.WithCorsForRegisteredEndpointsOnly(false),
grpcweb.WithOriginFunc(func(origin string) bool {
return true
}),
}
wrappedGrpc := grpcweb.WrapServer(s.grpcServer, options...)
e.Any("/memos.api.v2.*", echo.WrapHandler(wrappedGrpc))
return nil
}

View File

@@ -1,8 +1,9 @@
package cmd
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
@@ -10,12 +11,9 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/server"
_profile "github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db"
)
@@ -32,66 +30,66 @@ const (
)
var (
profile *_profile.Profile
mode string
addr string
port int
data string
driver string
dsn string
enableMetric bool
mode string
addr string
port int
data string
driver string
dsn string
instanceProfile *profile.Profile
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) {
Short: `An open source, lightweight note-taking service. Easily capture and share your great thoughts.`,
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithCancel(context.Background())
dbDriver, err := db.NewDBDriver(profile)
dbDriver, err := db.NewDBDriver(instanceProfile)
if err != nil {
cancel()
log.Error("failed to create db driver", zap.Error(err))
slog.Error("failed to create db driver", err)
return
}
if err := dbDriver.Migrate(ctx); err != nil {
cancel()
log.Error("failed to migrate db", zap.Error(err))
slog.Error("failed to migrate database", err)
return
}
store := store.New(dbDriver, profile)
s, err := server.NewServer(ctx, profile, store)
storeInstance := store.New(dbDriver, instanceProfile)
if err := storeInstance.MigrateManually(ctx); err != nil {
cancel()
slog.Error("failed to migrate manually", err)
return
}
s, err := server.NewServer(ctx, instanceProfile, storeInstance)
if err != nil {
cancel()
log.Error("failed to create server", zap.Error(err))
slog.Error("failed to create server", err)
return
}
if profile.Metric {
// nolint
metric.NewMetricClient(s.ID, *profile)
}
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
log.Info(fmt.Sprintf("%s received.\n", sig.String()))
s.Shutdown(ctx)
cancel()
}()
printGreetings()
if err := s.Start(ctx); err != nil {
if err != http.ErrServerClosed {
log.Error("failed to start server", zap.Error(err))
slog.Error("failed to start server", err)
cancel()
}
}
printGreetings()
go func() {
<-c
s.Shutdown(ctx)
cancel()
}()
// Wait for CTRL-C.
<-ctx.Done()
},
@@ -99,7 +97,6 @@ var (
)
func Execute() error {
defer log.Sync()
return rootCmd.Execute()
}
@@ -112,7 +109,6 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "", "data directory")
rootCmd.PersistentFlags().StringVarP(&driver, "driver", "", "", "database driver")
rootCmd.PersistentFlags().StringVarP(&dsn, "dsn", "", "", "database source name(aka. DSN)")
rootCmd.PersistentFlags().BoolVarP(&enableMetric, "metric", "", true, "allow metric collection")
err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode"))
if err != nil {
@@ -138,51 +134,54 @@ func init() {
if err != nil {
panic(err)
}
err = viper.BindPFlag("metric", rootCmd.PersistentFlags().Lookup("metric"))
if err != nil {
panic(err)
}
viper.SetDefault("mode", "demo")
viper.SetDefault("driver", "sqlite")
viper.SetDefault("addr", "")
viper.SetDefault("port", 8081)
viper.SetDefault("metric", true)
viper.SetEnvPrefix("memos")
}
func initConfig() {
viper.AutomaticEnv()
var err error
profile, err = _profile.GetProfile()
instanceProfile, err = profile.GetProfile()
if err != nil {
fmt.Printf("failed to get profile, error: %+v\n", err)
slog.Error("failed to get profile", err)
return
}
println("---")
println("Server profile")
println("data:", profile.Data)
println("dsn:", profile.DSN)
println("addr:", profile.Addr)
println("port:", profile.Port)
println("mode:", profile.Mode)
println("driver:", profile.Driver)
println("version:", profile.Version)
println("metric:", profile.Metric)
println("---")
fmt.Printf(`---
Server profile
version: %s
data: %s
dsn: %s
addr: %s
port: %d
mode: %s
driver: %s
---
`, instanceProfile.Version, instanceProfile.Data, instanceProfile.DSN, instanceProfile.Addr, instanceProfile.Port, instanceProfile.Mode, instanceProfile.Driver)
}
func printGreetings() {
print(greetingBanner)
if len(profile.Addr) == 0 {
fmt.Printf("Version %s has been started on port %d\n", profile.Version, profile.Port)
if len(instanceProfile.Addr) == 0 {
fmt.Printf("Version %s has been started on port %d\n", instanceProfile.Version, instanceProfile.Port)
} else {
fmt.Printf("Version %s has been started on address '%s' and port %d\n", profile.Version, profile.Addr, profile.Port)
fmt.Printf("Version %s has been started on address '%s' and port %d\n", instanceProfile.Version, instanceProfile.Addr, instanceProfile.Port)
}
fmt.Printf(`---
See more in:
👉Website: %s
👉GitHub: %s
---
`, "https://usememos.com", "https://github.com/usememos/memos")
}
func main() {
err := Execute()
if err != nil {
panic(err)
}
println("---")
println("See more in:")
fmt.Printf("👉Website: %s\n", "https://usememos.com")
fmt.Printf("👉GitHub: %s\n", "https://github.com/usememos/memos")
println("---")
}

View File

@@ -1,386 +0,0 @@
package cmd
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
_profile "github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db"
)
var (
copydbCmdFlagFrom = "from"
copydbCmd = &cobra.Command{
Use: "copydb", // `copydb` is a shortened for 'copy database'
Short: "Copy data between db drivers",
Run: func(cmd *cobra.Command, _ []string) {
s, err := cmd.Flags().GetString(copydbCmdFlagFrom)
if err != nil {
println("fail to get from driver DSN")
println(err)
return
}
ss := strings.Split(s, "://")
if len(ss) != 2 {
println("fail to parse from driver DSN, should be like 'sqlite://memos_prod.db' or 'mysql://user:pass@tcp(host)/memos'")
return
}
fromProfile := &_profile.Profile{Driver: ss[0], DSN: ss[1]}
err = copydb(fromProfile, profile)
if err != nil {
fmt.Printf("fail to copydb: %s\n", err)
return
}
println("done")
},
}
)
func init() {
copydbCmd.Flags().String(copydbCmdFlagFrom, "sqlite://memos_prod.db", "From driver DSN")
rootCmd.AddCommand(copydbCmd)
}
func copydb(fromProfile, toProfile *_profile.Profile) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
toDriver, err := db.NewDBDriver(toProfile)
if err != nil {
return errors.Wrap(err, "fail to create `to` driver")
}
// Check if `to` driver has been created before
if history, err := toDriver.FindMigrationHistoryList(ctx, nil); err != nil {
return errors.New("fail to check migration history of `to` driver")
} else if len(history) == 0 {
return errors.New("migration history of `to` driver should not be empty")
}
if err := toDriver.Migrate(ctx); err != nil {
return errors.Wrap(err, "fail to migrate db")
}
fromDriver, err := db.NewDBDriver(fromProfile)
if err != nil {
return errors.Wrap(err, "fail to create `from` driver")
}
// Register here if any table is added
copyMap := map[string]func(context.Context, store.Driver, store.Driver) error{
"activity": copyActivity,
"idp": copyIdp,
"memo": copyMemo,
"memo_organizer": copyMemoOrganizer,
"memo_relation": copyMemoRelation,
"resource": copyResource,
"storage": copyStorage,
"system_setting": copySystemSettings,
"tag": copyTag,
"user": copyUser,
"user_setting": copyUserSettings,
}
toDb := toDriver.GetDB()
for table := range copyMap {
println("Checking " + table + "...")
var cnt int
err := toDb.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table).Scan(&cnt)
if err != nil {
return errors.Wrapf(err, "fail to check '%s'", table)
}
if cnt > 0 && table != "system_setting" {
return errors.Errorf("table '%s' is not empty", table)
}
}
for _, f := range copyMap {
err = f(ctx, fromDriver, toDriver)
if err != nil {
return errors.Wrap(err, "fail to copy data")
}
}
return nil
}
func copyActivity(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying Activity...")
list, err := fromDriver.ListActivities(ctx, &store.FindActivity{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.CreateActivity(ctx, &store.Activity{
ID: item.ID,
CreatorID: item.CreatorID,
CreatedTs: item.CreatedTs,
Level: item.Level,
Type: item.Type,
Payload: item.Payload,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyIdp(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying IdentityProvider...")
list, err := fromDriver.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.CreateIdentityProvider(ctx, &store.IdentityProvider{
ID: item.ID,
Name: item.Name,
Type: item.Type,
IdentifierFilter: item.IdentifierFilter,
Config: item.Config,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyMemo(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying Memo...")
list, err := fromDriver.ListMemos(ctx, &store.FindMemo{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.CreateMemo(ctx, &store.Memo{
ID: item.ID,
CreatorID: item.CreatorID,
CreatedTs: item.CreatedTs,
UpdatedTs: item.UpdatedTs,
RowStatus: item.RowStatus,
Content: item.Content,
Visibility: item.Visibility,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyMemoOrganizer(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying MemoOrganizer...")
list, err := fromDriver.ListMemoOrganizer(ctx, &store.FindMemoOrganizer{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.UpsertMemoOrganizer(ctx, &store.MemoOrganizer{
MemoID: item.MemoID,
UserID: item.UserID,
Pinned: item.Pinned,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyMemoRelation(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying MemoRelation...")
list, err := fromDriver.ListMemoRelations(ctx, &store.FindMemoRelation{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: item.MemoID,
RelatedMemoID: item.RelatedMemoID,
Type: item.Type,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyResource(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying Resource...")
list, err := fromDriver.ListResources(ctx, &store.FindResource{GetBlob: true})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.CreateResource(ctx, &store.Resource{
ID: item.ID,
CreatorID: item.CreatorID,
CreatedTs: item.CreatedTs,
UpdatedTs: item.UpdatedTs,
Filename: item.Filename,
Blob: item.Blob,
ExternalLink: item.ExternalLink,
Type: item.Type,
Size: item.Size,
InternalPath: item.InternalPath,
MemoID: item.MemoID,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyStorage(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying Storage...")
list, err := fromDriver.ListStorages(ctx, &store.FindStorage{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.CreateStorage(ctx, &store.Storage{
ID: item.ID,
Name: item.Name,
Type: item.Type,
Config: item.Config,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copySystemSettings(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying SystemSettings...")
list, err := fromDriver.ListSystemSettings(ctx, &store.FindSystemSetting{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: item.Name,
Value: item.Value,
Description: item.Description,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyTag(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying Tag...")
list, err := fromDriver.ListTags(ctx, &store.FindTag{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.UpsertTag(ctx, &store.Tag{
Name: item.Name,
CreatorID: item.CreatorID,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyUser(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying User...")
list, err := fromDriver.ListUsers(ctx, &store.FindUser{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.CreateUser(ctx, &store.User{
ID: item.ID,
CreatedTs: item.CreatedTs,
UpdatedTs: item.UpdatedTs,
RowStatus: item.RowStatus,
Username: item.Username,
Role: item.Role,
Email: item.Email,
Nickname: item.Nickname,
PasswordHash: item.PasswordHash,
AvatarURL: item.AvatarURL,
})
if err != nil {
return err
}
}
println("\tDONE")
return nil
}
func copyUserSettings(ctx context.Context, fromDriver, toDriver store.Driver) error {
println("Copying UserSettings...")
list, err := fromDriver.ListUserSettings(ctx, &store.FindUserSetting{})
if err != nil {
return err
}
fmt.Printf("\tTotal %d records\n", len(list))
for _, item := range list {
_, err := toDriver.UpsertUserSetting(ctx, item)
if err != nil {
return err
}
}
println("\tDONE")
return nil
}

View File

@@ -1,99 +0,0 @@
package cmd
import (
"context"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db/sqlite"
)
var (
mvrssCmdFlagFrom = "from"
mvrssCmdFlagTo = "to"
mvrssCmd = &cobra.Command{
Use: "mvrss", // `mvrss` is a shortened for 'means move resource'
Short: "Move resource between storage",
Run: func(cmd *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
from, err := cmd.Flags().GetString(mvrssCmdFlagFrom)
if err != nil {
fmt.Printf("failed to get from storage, error: %+v\n", err)
return
}
to, err := cmd.Flags().GetString(mvrssCmdFlagTo)
if err != nil {
fmt.Printf("failed to get to storage, error: %+v\n", err)
return
}
if from != "local" || to != "db" {
fmt.Printf("only local=>db be supported currently\n")
return
}
driver, err := sqlite.NewDB(profile)
if err != nil {
fmt.Printf("failed to create db driver, error: %+v\n", err)
return
}
if err := driver.Migrate(ctx); err != nil {
fmt.Printf("failed to migrate db, error: %+v\n", err)
return
}
s := store.New(driver, profile)
resources, err := s.ListResources(ctx, &store.FindResource{})
if err != nil {
fmt.Printf("failed to list resources, error: %+v\n", err)
return
}
var emptyString string
for _, res := range resources {
if res.InternalPath == "" {
continue
}
buf, err := os.ReadFile(res.InternalPath)
if err != nil {
fmt.Printf("Resource %5d failed to read file: %s\n", res.ID, err)
continue
}
if len(buf) != int(res.Size) {
fmt.Printf("Resource %5d size of file %d != %d\n", res.ID, len(buf), res.Size)
continue
}
update := store.UpdateResource{
ID: res.ID,
Blob: buf,
InternalPath: &emptyString,
}
_, err = s.UpdateResource(ctx, &update)
if err != nil {
fmt.Printf("Resource %5d failed to update: %s\n", res.ID, err)
continue
}
fmt.Printf("Resource %5d copy %12d bytes from %s\n", res.ID, len(buf), res.InternalPath)
}
println("done")
},
}
)
func init() {
mvrssCmd.Flags().String(mvrssCmdFlagFrom, "local", "From storage")
mvrssCmd.Flags().String(mvrssCmdFlagTo, "db", "To Storage")
rootCmd.AddCommand(mvrssCmd)
}

View File

@@ -1,142 +0,0 @@
package cmd
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store/db/sqlite"
)
var (
setupCmdFlagHostUsername = "host-username"
setupCmdFlagHostPassword = "host-password"
setupCmd = &cobra.Command{
Use: "setup",
Short: "Make initial setup for memos",
Run: func(cmd *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
hostUsername, err := cmd.Flags().GetString(setupCmdFlagHostUsername)
if err != nil {
fmt.Printf("failed to get owner username, error: %+v\n", err)
return
}
hostPassword, err := cmd.Flags().GetString(setupCmdFlagHostPassword)
if err != nil {
fmt.Printf("failed to get owner password, error: %+v\n", err)
return
}
driver, err := sqlite.NewDB(profile)
if err != nil {
fmt.Printf("failed to create db driver, error: %+v\n", err)
return
}
if err := driver.Migrate(ctx); err != nil {
fmt.Printf("failed to migrate db, error: %+v\n", err)
return
}
store := store.New(driver, profile)
if err := ExecuteSetup(ctx, store, hostUsername, hostPassword); err != nil {
fmt.Printf("failed to setup, error: %+v\n", err)
return
}
},
}
)
func init() {
setupCmd.Flags().String(setupCmdFlagHostUsername, "", "Owner username")
setupCmd.Flags().String(setupCmdFlagHostPassword, "", "Owner password")
rootCmd.AddCommand(setupCmd)
}
func ExecuteSetup(ctx context.Context, store *store.Store, hostUsername, hostPassword string) error {
s := setupService{store: store}
return s.Setup(ctx, hostUsername, hostPassword)
}
type setupService struct {
store *store.Store
}
func (s setupService) Setup(ctx context.Context, hostUsername, hostPassword string) error {
if err := s.makeSureHostUserNotExists(ctx); err != nil {
return err
}
if err := s.createUser(ctx, hostUsername, hostPassword); err != nil {
return errors.Wrap(err, "create user")
}
return nil
}
func (s setupService) makeSureHostUserNotExists(ctx context.Context) error {
hostUserType := store.RoleHost
existedHostUsers, err := s.store.ListUsers(ctx, &store.FindUser{Role: &hostUserType})
if err != nil {
return errors.Wrap(err, "find user list")
}
if len(existedHostUsers) != 0 {
return errors.New("host user already exists")
}
return nil
}
func (s setupService) createUser(ctx context.Context, hostUsername, hostPassword string) error {
userCreate := &store.User{
Username: hostUsername,
// The new signup user should be normal user by default.
Role: store.RoleHost,
Nickname: hostUsername,
}
if len(userCreate.Username) < 3 {
return errors.New("username is too short, minimum length is 3")
}
if len(userCreate.Username) > 32 {
return errors.New("username is too long, maximum length is 32")
}
if len(hostPassword) < 3 {
return errors.New("password is too short, minimum length is 3")
}
if len(hostPassword) > 512 {
return errors.New("password is too long, maximum length is 512")
}
if len(userCreate.Nickname) > 64 {
return errors.New("nickname is too long, maximum length is 64")
}
if userCreate.Email != "" {
if len(userCreate.Email) > 256 {
return errors.New("email is too long, maximum length is 256")
}
if !util.ValidateEmail(userCreate.Email) {
return errors.New("invalid email format")
}
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(hostPassword), bcrypt.DefaultCost)
if err != nil {
return errors.Wrap(err, "failed to hash password")
}
userCreate.PasswordHash = string(passwordHash)
if _, err := s.store.CreateUser(ctx, userCreate); err != nil {
return errors.Wrap(err, "failed to create user")
}
return nil
}

View File

@@ -1,70 +0,0 @@
services:
db:
image: mysql
volumes:
- ./.air/mysql:/var/lib/mysql
api:
image: cosmtrek/air
working_dir: /work
command: ["-c", "./scripts/.air.toml"]
environment:
- "MEMOS_DSN=root@tcp(db)/memos"
- "MEMOS_DRIVER=mysql"
volumes:
- .:/work/
- .air/go-build:/root/.cache/go-build
- $HOME/go/pkg/:/go/pkg/ # Cache for go mod shared with the host
web:
image: node:18-alpine
working_dir: /work
depends_on: ["api"]
ports: ["3001:3001"]
environment: ["DEV_PROXY_SERVER=http://api:8081/"]
entrypoint: ["/bin/sh", "-c"]
command: ["corepack enable && pnpm install && pnpm dev"]
volumes:
- ./web:/work
- ./.air/node_modules/:/work/node_modules/ # Cache for Node Modules
# Services below are used for developers to run once
#
# You can just run `docker compose run --rm SERVICE_NAME` to use
# For example:
# To regenerate typescript code of gRPC proto
# Just run `docker compose run --rm buf`
#
# All of theses services belongs to profile 'tools'
# This will prevent to launch by normally `docker compose up` unexpectly
# Generate typescript code of gRPC proto
buf:
profiles: ["tools"]
image: bufbuild/buf
working_dir: /work/proto
command: generate
volumes:
- ./proto:/work/proto
- ./web/src/types/:/work/web/src/types/
# Do golang static code check before create PR
golangci-lint:
profiles: ["tools"]
image: golangci/golangci-lint:v1.54.2
working_dir: /work/
entrypoint: golangci-lint
command: run -v
volumes:
- $HOME/go/pkg/:/go/pkg/ # Cache for go mod shared with the host
- .air/go-build:/root/.cache/go-build
- .:/work/
# run npm
npm:
profiles: ["tools"]
image: node:18-alpine
working_dir: /work
environment: ["NPM_CONFIG_UPDATE_NOTIFIER=false"]
entrypoint: "npm"
volumes:
- ./web:/work
- ./.air/node_modules/:/work/node_modules/

File diff suppressed because it is too large Load Diff

3072
docs/apidocs.swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -56,12 +56,12 @@ Memos should now be running at [http://localhost:3001](http://localhost:3001) an
## Building
Frontend must be built before backend. The built frontend must be placed in the backend ./server/dist directory. Otherwise, you will get a "No frontend embeded" error.
Frontend must be built before backend. The built frontend must be placed in the backend ./server/frontend/dist directory. Otherwise, you will get a "No frontend embedded" error.
### Frontend
```powershell
Move-Item "./server/dist" "./server/dist.bak"
Move-Item "./server/frontend/dist" "./server/frontend/dist.bak"
cd web; pnpm i --frozen-lockfile; pnpm build; cd ..;
Move-Item "./web/dist" "./server/" -Force
```
@@ -69,7 +69,7 @@ Move-Item "./web/dist" "./server/" -Force
### Backend
```powershell
go build -o ./build/memos.exe ./main.go
go build -o ./build/memos.exe ./bin/memos/main.go
```
## ❕ Notes

View File

@@ -21,7 +21,7 @@ Memos is built with a curated tech stack. It is optimized for developer experien
git clone https://github.com/usememos/memos
```
2. Start backend server with `air`(with live reload)
2. Start backend server with [`air`](https://github.com/cosmtrek/air) (with live reload)
```bash
air -c scripts/.air.toml
@@ -30,7 +30,7 @@ Memos is built with a curated tech stack. It is optimized for developer experien
3. Install frontend dependencies and generate TypeScript code from protobuf
```
cd web && pnpm i && pnpm type-gen
cd web && pnpm i
```
4. Start the dev server of frontend

View File

@@ -1,113 +0,0 @@
# Documenting the API
## Principles
- The documentation is generated by [swaggo/swag](https://github.com/swaggo/swag) from comments in the API code.
- Documentation is written using [Declarative Comments Format](https://github.com/swaggo/swag#declarative-comments-format).
- The documentation is generated in the `./api/v1` folder as `docs.go`.
- [echo-swagger](https://github.com/swaggo/echo-swagger) is used to integrate with Echo framework and serve the documentation with [Swagger-UI](https://swagger.io/tools/swagger-ui/) at `http://memos.host:5230/api/index.html`
## Updating the documentation
1. Update or add API-related comments in the code. Make sure to follow the [Declarative Comments Format](https://github.com/swaggo/swag#declarative-comments-format):
```go
// signIn godoc
//
// @Summary Sign-in to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignIn true "Sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
// @Failure 403 {object} nil "User has been archived with username {username}"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin [POST]
func (s *APIV1Service) signIn(c echo.Context) error {
...
```
> Sample from [api/v1/auth.go](https://github.com/usememos/memos/tree/main/api/v1/auth.go)
> You can check existing comments at [api/v1](https://github.com/usememos/memos/tree/main/api/v1)
2. Run one of the following provided scripts:
- Linux: `./scripts/gen-api-v1-docs.sh` (remember to `chmod +x` the script first)
- Windows: `./scripts/gen-api-v1-docs.ps1`
> The scripts will install swag if needed (via go install), then run `swag fmt` and `swag init` commands.
3. That's it! The documentation is updated. You can check it at `http://memos.host:5230/api/index.html`
### Extra tips
- If you reference a custom Go struct from outside the API file, use a relative definition, like `store.IdentityProvider`. This works because `./` is passed to swag at `--dir` argument. If swag can't resolve the reference, it will fail.
- If the API grows or you need to reference some type from another location, remember to update ./scripts/gen-api-v1-docs.cfg file with the new paths.
- It's possible to list multiple errors for the same code using enum-like structs, that will show a proper, spec-conformant model with all entries at Swagger-UI. The drawback is that this approach requires a major refactoring and will add a lot of boilerplate code, as there are inconsistencies between API methods error responses.
```go
type signInInternalServerError string
const signInErrorFailedToFindSystemSetting signInInternalServerError = "Failed to find system setting"
const signInErrorFailedToUnmarshalSystemSetting signInInternalServerError = "Failed to unmarshal system setting"
const signInErrorIncorrectLoginCredentials signInInternalServerError = "Incorrect login credentials, please try again"
const signInErrorFailedToGenerateTokens signInInternalServerError = "Failed to generate tokens"
const signInErrorFailedToCreateActivity signInInternalServerError = "Failed to create activity"
type signInUnauthorized string
const signInErrorPasswordLoginDeactivated signInUnauthorized = "Password login is deactivated"
const signInErrorIncorrectCredentials signInUnauthorized = "Incorrect login credentials, please try again"
// signIn godoc
//
// @Summary Sign-in to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignIn true "Sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} signInUnauthorized
// @Failure 403 {object} nil "User has been archived with username {username}"
// @Failure 500 {object} signInInternalServerError
// @Router /api/v1/auth/signin [POST]
func (s *APIV1Service) signIn(c echo.Context) error {
...
```
### Step-by-step (no scripts)
#### Required tools
```bash
# Swag v1.8.12 or newer
# Also updates swag if needed
$ go install github.com/swaggo/swag/cmd/swag@latest
```
If `$HOME/go/bin` is not in your `PATH`, you can call `swag` directly at `$HOME/go/bin/swag`.
#### Generate the documentation
1. Run `swag fmt` to format the comments
```bash
swag fmt --dir ./api/v1 && go fmt
```
2. Run `swag init` to generate the documentation
```bash
cd <project-root>
swag init --output ./api/v1 --generalInfo ./api/v1/v1.go --dir ./,./api/v1
```
> If the API gets a new version, you'll need to add the file system path to swag's `--dir` parameter.

View File

@@ -1,98 +0,0 @@
# Installing memos as a service on Windows
While memos first-class support is for Docker, you may also install memos as a Windows service. It will run under SYSTEM account and start automatically at system boot.
❗ All service management methods requires admin privileges. Use [gsudo](https://gerardog.github.io/gsudo/docs/install), or open a new PowerShell terminal as admin:
```powershell
Start-Process powershell -Verb RunAs
```
## Choose one of the following methods
### 1. Using [NSSM](https://nssm.cc/download)
NSSM is a lightweight service wrapper.
You may put `nssm.exe` in the same directory as `memos.exe`, or add its directory to your system PATH. Prefer the latest 64-bit version of `nssm.exe`.
```powershell
# Install memos as a service
nssm install memos "C:\path\to\memos.exe" --mode prod --port 5230
# Delay auto start
nssm set memos DisplayName "memos service"
# Configure extra service parameters
nssm set memos Description "A lightweight, self-hosted memo hub. https://usememos.com/"
# Delay auto start
nssm set memos Start SERVICE_DELAYED_AUTO_START
# Edit service using NSSM GUI
nssm edit memos
# Start the service
nssm start memos
# Remove the service, if ever needed
nssm remove memos confirm
```
### 2. Using [WinSW](https://github.com/winsw/winsw)
Find the latest release tag and download the asset `WinSW-net46x.exe`. Then, put it in the same directory as `memos.exe` and rename it to `memos-service.exe`.
Now, in the same directory, create the service configuration file `memos-service.xml`:
```xml
<service>
<id>memos</id>
<name>memos service</name>
<description>A lightweight, self-hosted memo hub. https://usememos.com/</description>
<onfailure action="restart" delay="10 sec"/>
<executable>%BASE%\memos.exe</executable>
<arguments>--mode prod --port 5230</arguments>
<delayedAutoStart>true</delayedAutoStart>
<log mode="none" />
</service>
```
Then, install the service:
```powershell
# Install the service
.\memos-service.exe install
# Start the service
.\memos-service.exe start
# Remove the service, if ever needed
.\memos-service.exe uninstall
```
### Manage the service
You may use the `net` command to manage the service:
```powershell
net start memos
net stop memos
```
Also, by using one of the provided methods, the service will appear in the Windows Services Manager `services.msc`.
## Notes
- On Windows, memos store its data in the following directory:
```powershell
$env:ProgramData\memos
# Typically, this will resolve to C:\ProgramData\memos
```
You may specify a custom directory by appending `--data <path>` to the service command line.
- If the service fails to start, you should inspect the Windows Event Viewer `eventvwr.msc`.
- Memos will be accessible at [http://localhost:5230](http://localhost:5230) by default.

144
go.mod
View File

@@ -1,127 +1,107 @@
module github.com/usememos/memos
go 1.21
go 1.22
toolchain go1.22.2
require (
github.com/Masterminds/squirrel v1.5.4
github.com/aws/aws-sdk-go-v2 v1.22.1
github.com/aws/aws-sdk-go-v2/config v1.22.1
github.com/aws/aws-sdk-go-v2/credentials v1.15.1
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.13.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.42.0
github.com/disintegration/imaging v1.6.2
github.com/go-sql-driver/mysql v1.7.1
github.com/google/cel-go v0.18.1
github.com/google/uuid v1.4.0
github.com/aws/aws-sdk-go-v2 v1.26.1
github.com/aws/aws-sdk-go-v2/config v1.27.13
github.com/aws/aws-sdk-go-v2/credentials v1.17.13
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2
github.com/go-sql-driver/mysql v1.8.1
github.com/google/cel-go v0.20.1
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/improbable-eng/grpc-web v0.15.0
github.com/labstack/echo/v4 v4.11.2
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.12.0
github.com/lib/pq v1.10.9
github.com/microcosm-cc/bluemonday v1.0.26
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
github.com/swaggo/echo-swagger v1.4.1
github.com/swaggo/swag v1.16.2
github.com/yuin/goldmark v1.6.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/mod v0.14.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.13.0
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17
google.golang.org/grpc v1.59.0
modernc.org/sqlite v1.27.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/yourselfhosted/gomark v0.0.0-20240228170507-6a73bfad2eb6
golang.org/x/crypto v0.23.0
golang.org/x/mod v0.17.0
golang.org/x/net v0.25.0
golang.org/x/oauth2 v0.20.0
google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be
google.golang.org/grpc v1.63.2
modernc.org/sqlite v1.29.9
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rs/cors v1.10.1 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.30.0 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
nhooyr.io/websocket v1.8.10 // indirect
nhooyr.io/websocket v1.8.11 // indirect
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.5.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.17.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.25.0 // indirect
github.com/aws/smithy-go v1.16.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/soheilhy/cmux v0.1.5
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.1
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

644
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -150,7 +150,7 @@ func parseCronSegment(segment string, min int, max int) (map[int]struct{}, error
switch len(rangeParts) {
case 1:
if step != 1 {
return nil, errors.New("invalid segement step - step > 1 could be used only with the wildcard or range format")
return nil, errors.New("invalid segment step - step > 1 could be used only with the wildcard or range format")
}
parsed, err := strconv.Atoi(rangeParts[0])
if err != nil {

View File

@@ -1,66 +0,0 @@
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

@@ -0,0 +1,7 @@
package util
import "regexp"
var (
UIDMatcher = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]{1,30}[a-zA-Z0-9])$")
)

View File

@@ -41,13 +41,6 @@ func GenUUID() string {
return uuid.New().String()
}
func Min(x, y int) int {
if x < y {
return x
}
return y
}
var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
// RandomString returns a random string with length n.
@@ -68,3 +61,13 @@ func RandomString(n int) (string, error) {
}
return sb.String(), nil
}
// ReplaceString replaces all occurrences of old in slice with new.
func ReplaceString(slice []string, old, new string) []string {
for i, s := range slice {
if s == old {
slice[i] = new
}
}
return slice
}

15
main.go
View File

@@ -1,15 +0,0 @@
package main
import (
_ "github.com/go-sql-driver/mysql"
_ "modernc.org/sqlite"
"github.com/usememos/memos/cmd"
)
func main() {
err := cmd.Execute()
if err != nil {
panic(err)
}
}

View File

@@ -1,3 +0,0 @@
# gomark
A markdown parser for memos. WIP

View File

@@ -1,19 +0,0 @@
package ast
type Node struct {
Type string
Text string
Children []*Node
}
type Document struct {
Nodes []*Node
}
func NewDocument() *Document {
return &Document{}
}
func (d *Document) AddNode(node *Node) {
d.Nodes = append(d.Nodes, node)
}

View File

@@ -1,12 +0,0 @@
package ast
func NewNode(tp, text string) *Node {
return &Node{
Type: tp,
Text: text,
}
}
func (n *Node) AddChild(child *Node) {
n.Children = append(n.Children, child)
}

View File

@@ -1 +0,0 @@
package gomark

View File

@@ -1,49 +0,0 @@
package parser
import (
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type BoldParser struct {
ContentTokens []*tokenizer.Token
}
func NewBoldParser() *BoldParser {
return &BoldParser{}
}
func (*BoldParser) Match(tokens []*tokenizer.Token) *BoldParser {
if len(tokens) < 5 {
return nil
}
prefixTokens := tokens[:2]
if prefixTokens[0].Type != prefixTokens[1].Type {
return nil
}
prefixTokenType := prefixTokens[0].Type
if prefixTokenType != tokenizer.Star && prefixTokenType != tokenizer.Underline {
return nil
}
contentTokens := []*tokenizer.Token{}
cursor, matched := 2, false
for ; cursor < len(tokens)-1; cursor++ {
token, nextToken := tokens[cursor], tokens[cursor+1]
if token.Type == tokenizer.Newline || nextToken.Type == tokenizer.Newline {
return nil
}
if token.Type == prefixTokenType && nextToken.Type == prefixTokenType {
matched = true
break
}
contentTokens = append(contentTokens, token)
}
if !matched {
return nil
}
return &BoldParser{
ContentTokens: contentTokens,
}
}

View File

@@ -1,89 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestBoldParser(t *testing.T) {
tests := []struct {
text string
bold *BoldParser
}{
{
text: "*Hello world!",
bold: nil,
},
{
text: "**Hello**",
bold: &BoldParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
},
},
},
{
text: "** Hello **",
bold: &BoldParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
},
},
},
{
text: "** Hello * *",
bold: nil,
},
{
text: "* * Hello **",
bold: nil,
},
{
text: `** Hello
**`,
bold: nil,
},
{
text: `**Hello \n**`,
bold: &BoldParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: `\n`,
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
bold := NewBoldParser()
require.Equal(t, test.bold, bold.Match(tokens))
}
}

View File

@@ -1,38 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type CodeParser struct {
Content string
}
func NewCodeParser() *CodeParser {
return &CodeParser{}
}
func (*CodeParser) Match(tokens []*tokenizer.Token) *CodeParser {
if len(tokens) < 3 {
return nil
}
if tokens[0].Type != tokenizer.Backtick {
return nil
}
content, matched := "", false
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline {
return nil
}
if token.Type == tokenizer.Backtick {
matched = true
break
}
content += token.Value
}
if !matched || len(content) == 0 {
return nil
}
return &CodeParser{
Content: content,
}
}

View File

@@ -1,52 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type CodeBlockParser struct {
Language string
Content string
}
func NewCodeBlockParser() *CodeBlockParser {
return &CodeBlockParser{}
}
func (*CodeBlockParser) Match(tokens []*tokenizer.Token) *CodeBlockParser {
if len(tokens) < 9 {
return nil
}
if tokens[0].Type != tokenizer.Backtick || tokens[1].Type != tokenizer.Backtick || tokens[2].Type != tokenizer.Backtick {
return nil
}
if tokens[3].Type != tokenizer.Newline && tokens[4].Type != tokenizer.Newline {
return nil
}
cursor, language := 4, ""
if tokens[3].Type != tokenizer.Newline {
language = tokens[3].Value
cursor = 5
}
content, matched := "", false
for ; cursor < len(tokens)-3; cursor++ {
if tokens[cursor].Type == tokenizer.Newline && tokens[cursor+1].Type == tokenizer.Backtick && tokens[cursor+2].Type == tokenizer.Backtick && tokens[cursor+3].Type == tokenizer.Backtick {
if cursor+3 == len(tokens)-1 {
matched = true
break
} else if tokens[cursor+4].Type == tokenizer.Newline {
matched = true
break
}
}
content += tokens[cursor].Value
}
if !matched {
return nil
}
return &CodeBlockParser{
Language: language,
Content: content,
}
}

View File

@@ -1,63 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestCodeBlockParser(t *testing.T) {
tests := []struct {
text string
codeBlock *CodeBlockParser
}{
{
text: "```Hello world!```",
codeBlock: nil,
},
{
text: "```\nHello\n```",
codeBlock: &CodeBlockParser{
Language: "",
Content: "Hello",
},
},
{
text: "```\nHello world!\n```",
codeBlock: &CodeBlockParser{
Language: "",
Content: "Hello world!",
},
},
{
text: "```java\nHello \n world!\n```",
codeBlock: &CodeBlockParser{
Language: "java",
Content: "Hello \n world!",
},
},
{
text: "```java\nHello \n world!\n```111",
codeBlock: nil,
},
{
text: "```java\nHello \n world!\n``` 111",
codeBlock: nil,
},
{
text: "```java\nHello \n world!\n```\n123123",
codeBlock: &CodeBlockParser{
Language: "java",
Content: "Hello \n world!",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
codeBlock := NewCodeBlockParser()
require.Equal(t, test.codeBlock, codeBlock.Match(tokens))
}
}

View File

@@ -1,37 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestCodeParser(t *testing.T) {
tests := []struct {
text string
code *CodeParser
}{
{
text: "`Hello world!",
code: nil,
},
{
text: "`Hello world!`",
code: &CodeParser{
Content: "Hello world!",
},
},
{
text: "`Hello \nworld!`",
code: nil,
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
code := NewCodeParser()
require.Equal(t, test.code, code.Match(tokens))
}
}

View File

@@ -1,53 +0,0 @@
package parser
import (
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
type HeadingParser struct {
Level int
ContentTokens []*tokenizer.Token
}
func NewHeadingParser() *HeadingParser {
return &HeadingParser{}
}
func (*HeadingParser) Match(tokens []*tokenizer.Token) *HeadingParser {
cursor := 0
for _, token := range tokens {
if token.Type == tokenizer.Hash {
cursor++
} else {
break
}
}
if len(tokens) <= cursor+1 {
return nil
}
if tokens[cursor].Type != tokenizer.Space {
return nil
}
level := cursor
if level == 0 || level > 6 {
return nil
}
cursor++
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[cursor:] {
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
cursor++
}
if len(contentTokens) == 0 {
return nil
}
return &HeadingParser{
Level: level,
ContentTokens: contentTokens,
}
}

View File

@@ -1,96 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestHeadingParser(t *testing.T) {
tests := []struct {
text string
heading *HeadingParser
}{
{
text: "*Hello world",
heading: nil,
},
{
text: "## Hello World",
heading: &HeadingParser{
Level: 2,
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "World",
},
},
},
},
{
text: "# # Hello World",
heading: &HeadingParser{
Level: 1,
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Hash,
Value: "#",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "World",
},
},
},
},
{
text: " # 123123 Hello World",
heading: nil,
},
{
text: `# 123
Hello World`,
heading: &HeadingParser{
Level: 1,
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "123",
},
{
Type: tokenizer.Space,
Value: " ",
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
heading := NewHeadingParser()
require.Equal(t, test.heading, heading.Match(tokens))
}
}

View File

@@ -1,55 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type ImageParser struct {
AltText string
URL string
}
func NewImageParser() *ImageParser {
return &ImageParser{}
}
func (*ImageParser) Match(tokens []*tokenizer.Token) *ImageParser {
if len(tokens) < 5 {
return nil
}
if tokens[0].Type != tokenizer.ExclamationMark {
return nil
}
if tokens[1].Type != tokenizer.LeftSquareBracket {
return nil
}
cursor, altText := 2, ""
for ; cursor < len(tokens)-2; cursor++ {
if tokens[cursor].Type == tokenizer.Newline {
return nil
}
if tokens[cursor].Type == tokenizer.RightSquareBracket {
break
}
altText += tokens[cursor].Value
}
if tokens[cursor+1].Type != tokenizer.LeftParenthesis {
return nil
}
matched, url := false, ""
for _, token := range tokens[cursor+2:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
return nil
}
if token.Type == tokenizer.RightParenthesis {
matched = true
break
}
url += token.Value
}
if !matched || url == "" {
return nil
}
return &ImageParser{
AltText: altText,
URL: url,
}
}

View File

@@ -1,43 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestImageParser(t *testing.T) {
tests := []struct {
text string
image *ImageParser
}{
{
text: "![](https://example.com)",
image: &ImageParser{
AltText: "",
URL: "https://example.com",
},
},
{
text: "! [](https://example.com)",
image: nil,
},
{
text: "![alte]( htt ps :/ /example.com)",
image: nil,
},
{
text: "![al te](https://example.com)",
image: &ImageParser{
AltText: "al te",
URL: "https://example.com",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
require.Equal(t, test.image, NewImageParser().Match(tokens))
}
}

View File

@@ -1,42 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type ItalicParser struct {
ContentTokens []*tokenizer.Token
}
func NewItalicParser() *ItalicParser {
return &ItalicParser{}
}
func (*ItalicParser) Match(tokens []*tokenizer.Token) *ItalicParser {
if len(tokens) < 3 {
return nil
}
prefixTokens := tokens[:1]
if prefixTokens[0].Type != tokenizer.Star && prefixTokens[0].Type != tokenizer.Underline {
return nil
}
prefixTokenType := prefixTokens[0].Type
contentTokens := []*tokenizer.Token{}
matched := false
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline {
return nil
}
if token.Type == prefixTokenType {
matched = true
break
}
contentTokens = append(contentTokens, token)
}
if !matched || len(contentTokens) == 0 {
return nil
}
return &ItalicParser{
ContentTokens: contentTokens,
}
}

View File

@@ -1,95 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestItalicParser(t *testing.T) {
tests := []struct {
text string
italic *ItalicParser
}{
{
text: "*Hello world!",
italic: nil,
},
{
text: "*Hello*",
italic: &ItalicParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
},
},
},
{
text: "* Hello *",
italic: &ItalicParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
},
},
},
{
text: "** Hello * *",
italic: nil,
},
{
text: "*1* Hello * *",
italic: &ItalicParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "1",
},
},
},
},
{
text: `* \n * Hello * *`,
italic: &ItalicParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: `\n`,
},
{
Type: tokenizer.Space,
Value: " ",
},
},
},
},
{
text: "* \n * Hello * *",
italic: nil,
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
italic := NewItalicParser()
require.Equal(t, test.italic, italic.Match(tokens))
}
}

View File

@@ -1,58 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type LinkParser struct {
ContentTokens []*tokenizer.Token
URL string
}
func NewLinkParser() *LinkParser {
return &LinkParser{}
}
func (*LinkParser) Match(tokens []*tokenizer.Token) *LinkParser {
if len(tokens) < 4 {
return nil
}
if tokens[0].Type != tokenizer.LeftSquareBracket {
return nil
}
cursor, contentTokens := 1, []*tokenizer.Token{}
for ; cursor < len(tokens)-2; cursor++ {
if tokens[cursor].Type == tokenizer.Newline {
return nil
}
if tokens[cursor].Type == tokenizer.RightSquareBracket {
break
}
contentTokens = append(contentTokens, tokens[cursor])
}
if tokens[cursor+1].Type != tokenizer.LeftParenthesis {
return nil
}
matched, url := false, ""
for _, token := range tokens[cursor+2:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space {
return nil
}
if token.Type == tokenizer.RightParenthesis {
matched = true
break
}
url += token.Value
}
if !matched || url == "" {
return nil
}
if len(contentTokens) == 0 {
contentTokens = append(contentTokens, &tokenizer.Token{
Type: tokenizer.Text,
Value: url,
})
}
return &LinkParser{
ContentTokens: contentTokens,
URL: url,
}
}

View File

@@ -1,61 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestLinkParser(t *testing.T) {
tests := []struct {
text string
link *LinkParser
}{
{
text: "[](https://example.com)",
link: &LinkParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "https://example.com",
},
},
URL: "https://example.com",
},
},
{
text: "! [](https://example.com)",
link: nil,
},
{
text: "[alte]( htt ps :/ /example.com)",
link: nil,
},
{
text: "[hello world](https://example.com)",
link: &LinkParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "world",
},
},
URL: "https://example.com",
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
require.Equal(t, test.link, NewLinkParser().Match(tokens))
}
}

View File

@@ -1,30 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type ParagraphParser struct {
ContentTokens []*tokenizer.Token
}
func NewParagraphParser() *ParagraphParser {
return &ParagraphParser{}
}
func (*ParagraphParser) Match(tokens []*tokenizer.Token) *ParagraphParser {
contentTokens := []*tokenizer.Token{}
cursor := 0
for ; cursor < len(tokens); cursor++ {
token := tokens[cursor]
if token.Type == tokenizer.Newline {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return nil
}
return &ParagraphParser{
ContentTokens: contentTokens,
}
}

View File

@@ -1,86 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestParagraphParser(t *testing.T) {
tests := []struct {
text string
paragraph *ParagraphParser
}{
{
text: "",
paragraph: nil,
},
{
text: "Hello world",
paragraph: &ParagraphParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: "world",
},
},
},
},
{
text: `Hello
world`,
paragraph: &ParagraphParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
},
},
},
{
text: `Hello \n
world`,
paragraph: &ParagraphParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "Hello",
},
{
Type: tokenizer.Space,
Value: " ",
},
{
Type: tokenizer.Text,
Value: `\n`,
},
{
Type: tokenizer.Space,
Value: " ",
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
paragraph := NewParagraphParser()
require.Equal(t, test.paragraph, paragraph.Match(tokens))
}
}

View File

@@ -1 +0,0 @@
package parser

View File

@@ -1,34 +0,0 @@
package parser
import "github.com/usememos/memos/plugin/gomark/parser/tokenizer"
type TagParser struct {
ContentTokens []*tokenizer.Token
}
func NewTagParser() *TagParser {
return &TagParser{}
}
func (*TagParser) Match(tokens []*tokenizer.Token) *TagParser {
if len(tokens) < 2 {
return nil
}
if tokens[0].Type != tokenizer.Hash {
return nil
}
contentTokens := []*tokenizer.Token{}
for _, token := range tokens[1:] {
if token.Type == tokenizer.Newline || token.Type == tokenizer.Space || token.Type == tokenizer.Hash {
break
}
contentTokens = append(contentTokens, token)
}
if len(contentTokens) == 0 {
return nil
}
return &TagParser{
ContentTokens: contentTokens,
}
}

View File

@@ -1,52 +0,0 @@
package parser
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
)
func TestTagParser(t *testing.T) {
tests := []struct {
text string
tag *TagParser
}{
{
text: "*Hello world",
tag: nil,
},
{
text: "# Hello World",
tag: nil,
},
{
text: "#tag",
tag: &TagParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "tag",
},
},
},
},
{
text: "#tag/subtag",
tag: &TagParser{
ContentTokens: []*tokenizer.Token{
{
Type: tokenizer.Text,
Value: "tag/subtag",
},
},
},
},
}
for _, test := range tests {
tokens := tokenizer.Tokenize(test.text)
require.Equal(t, test.tag, NewTagParser().Match(tokens))
}
}

View File

@@ -1,74 +0,0 @@
package tokenizer
type TokenType = string
const (
Underline TokenType = "_"
Star TokenType = "*"
Hash TokenType = "#"
Backtick TokenType = "`"
LeftSquareBracket TokenType = "["
RightSquareBracket TokenType = "]"
LeftParenthesis TokenType = "("
RightParenthesis TokenType = ")"
ExclamationMark TokenType = "!"
Newline TokenType = "\n"
Space TokenType = " "
)
const (
Text TokenType = ""
)
type Token struct {
Type TokenType
Value string
}
func NewToken(tp, text string) *Token {
return &Token{
Type: tp,
Value: text,
}
}
func Tokenize(text string) []*Token {
tokens := []*Token{}
for _, c := range text {
switch c {
case '_':
tokens = append(tokens, NewToken(Underline, "_"))
case '*':
tokens = append(tokens, NewToken(Star, "*"))
case '#':
tokens = append(tokens, NewToken(Hash, "#"))
case '`':
tokens = append(tokens, NewToken(Backtick, "`"))
case '[':
tokens = append(tokens, NewToken(LeftSquareBracket, "["))
case ']':
tokens = append(tokens, NewToken(RightSquareBracket, "]"))
case '(':
tokens = append(tokens, NewToken(LeftParenthesis, "("))
case ')':
tokens = append(tokens, NewToken(RightParenthesis, ")"))
case '!':
tokens = append(tokens, NewToken(ExclamationMark, "!"))
case '\n':
tokens = append(tokens, NewToken(Newline, "\n"))
case ' ':
tokens = append(tokens, NewToken(Space, " "))
default:
var lastToken *Token
if len(tokens) > 0 {
lastToken = tokens[len(tokens)-1]
}
if lastToken == nil || lastToken.Type != Text {
tokens = append(tokens, NewToken(Text, string(c)))
} else {
lastToken.Value += string(c)
}
}
}
return tokens
}

View File

@@ -1,79 +0,0 @@
package tokenizer
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTokenize(t *testing.T) {
tests := []struct {
text string
tokens []*Token
}{
{
text: "*Hello world!",
tokens: []*Token{
{
Type: Star,
Value: "*",
},
{
Type: Text,
Value: "Hello",
},
{
Type: Space,
Value: " ",
},
{
Type: Text,
Value: "world",
},
{
Type: ExclamationMark,
Value: "!",
},
},
},
{
text: `# hello
world`,
tokens: []*Token{
{
Type: Hash,
Value: "#",
},
{
Type: Space,
Value: " ",
},
{
Type: Text,
Value: "hello",
},
{
Type: Space,
Value: " ",
},
{
Type: Newline,
Value: "\n",
},
{
Type: Space,
Value: " ",
},
{
Type: Text,
Value: "world",
},
},
},
}
for _, test := range tests {
result := Tokenize(test.text)
require.Equal(t, test.tokens, result)
}
}

View File

@@ -1 +0,0 @@
package renderer

View File

@@ -1,4 +0,0 @@
// Package getter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package getter

View File

@@ -1,4 +1,4 @@
package getter
package httpgetter
import (
"errors"
@@ -32,7 +32,7 @@ func GetHTMLMeta(urlStr string) (*HTMLMeta, error) {
return nil, err
}
if mediatype != "text/html" {
return nil, errors.New("Wrong website mediatype")
return nil, errors.New("not a HTML page")
}
htmlMeta := extractHTMLMeta(response.Body)

View File

@@ -1,4 +1,4 @@
package getter
package httpgetter
import (
"testing"

View File

@@ -0,0 +1,4 @@
// Package httpgetter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package httpgetter

View File

@@ -1,4 +1,4 @@
package getter
package httpgetter
import (
"errors"
@@ -6,8 +6,6 @@ import (
"net/http"
"net/url"
"strings"
"github.com/microcosm-cc/bluemonday"
)
type Image struct {
@@ -39,21 +37,9 @@ func GetImage(urlStr string) (*Image, error) {
return nil, err
}
bodyBytes, err = SanitizeContent(bodyBytes)
if err != nil {
return nil, err
}
image := &Image{
Blob: bodyBytes,
Mediatype: mediatype,
}
return image, nil
}
func SanitizeContent(content []byte) ([]byte, error) {
bodyString := string(content)
bm := bluemonday.UGCPolicy()
return []byte(bm.Sanitize(bodyString)), nil
}

View File

@@ -1,4 +1,4 @@
package getter
package httpgetter
import (
"mime"

View File

@@ -12,21 +12,21 @@ import (
"golang.org/x/oauth2"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/store"
storepb "github.com/usememos/memos/proto/gen/store"
)
// IdentityProvider represents an OAuth2 Identity Provider.
type IdentityProvider struct {
config *store.IdentityProviderOAuth2Config
config *storepb.OAuth2Config
}
// NewIdentityProvider initializes a new OAuth2 Identity Provider with the given configuration.
func NewIdentityProvider(config *store.IdentityProviderOAuth2Config) (*IdentityProvider, error) {
func NewIdentityProvider(config *storepb.OAuth2Config) (*IdentityProvider, error) {
for v, field := range map[string]string{
config.ClientID: "clientId",
config.ClientId: "clientId",
config.ClientSecret: "clientSecret",
config.TokenURL: "tokenUrl",
config.UserInfoURL: "userInfoUrl",
config.TokenUrl: "tokenUrl",
config.UserInfoUrl: "userInfoUrl",
config.FieldMapping.Identifier: "fieldMapping.identifier",
} {
if v == "" {
@@ -42,13 +42,13 @@ func NewIdentityProvider(config *store.IdentityProviderOAuth2Config) (*IdentityP
// 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,
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,
AuthURL: p.config.AuthUrl,
TokenURL: p.config.TokenUrl,
AuthStyle: oauth2.AuthStyleInParams,
},
}
@@ -69,7 +69,7 @@ func (p *IdentityProvider) ExchangeToken(ctx context.Context, redirectURL, code
// 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)
req, err := http.NewRequest(http.MethodGet, p.config.UserInfoUrl, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to new http request")
}

View File

@@ -14,24 +14,24 @@ import (
"github.com/stretchr/testify/require"
"github.com/usememos/memos/plugin/idp"
"github.com/usememos/memos/store"
storepb "github.com/usememos/memos/proto/gen/store"
)
func TestNewIdentityProvider(t *testing.T) {
tests := []struct {
name string
config *store.IdentityProviderOAuth2Config
config *storepb.OAuth2Config
containsErr string
}{
{
name: "no tokenUrl",
config: &store.IdentityProviderOAuth2Config{
ClientID: "test-client-id",
config: &storepb.OAuth2Config{
ClientId: "test-client-id",
ClientSecret: "test-client-secret",
AuthURL: "",
TokenURL: "",
UserInfoURL: "https://example.com/api/user",
FieldMapping: &store.FieldMapping{
AuthUrl: "",
TokenUrl: "",
UserInfoUrl: "https://example.com/api/user",
FieldMapping: &storepb.FieldMapping{
Identifier: "login",
},
},
@@ -39,13 +39,13 @@ func TestNewIdentityProvider(t *testing.T) {
},
{
name: "no userInfoUrl",
config: &store.IdentityProviderOAuth2Config{
ClientID: "test-client-id",
config: &storepb.OAuth2Config{
ClientId: "test-client-id",
ClientSecret: "test-client-secret",
AuthURL: "",
TokenURL: "https://example.com/token",
UserInfoURL: "",
FieldMapping: &store.FieldMapping{
AuthUrl: "",
TokenUrl: "https://example.com/token",
UserInfoUrl: "",
FieldMapping: &storepb.FieldMapping{
Identifier: "login",
},
},
@@ -53,13 +53,13 @@ func TestNewIdentityProvider(t *testing.T) {
},
{
name: "no field mapping identifier",
config: &store.IdentityProviderOAuth2Config{
ClientID: "test-client-id",
config: &storepb.OAuth2Config{
ClientId: "test-client-id",
ClientSecret: "test-client-secret",
AuthURL: "",
TokenURL: "https://example.com/token",
UserInfoURL: "https://example.com/api/user",
FieldMapping: &store.FieldMapping{
AuthUrl: "",
TokenUrl: "https://example.com/token",
UserInfoUrl: "https://example.com/api/user",
FieldMapping: &storepb.FieldMapping{
Identifier: "",
},
},
@@ -132,12 +132,12 @@ func TestIdentityProvider(t *testing.T) {
s := newMockServer(t, testCode, testAccessToken, userInfo)
oauth2, err := NewIdentityProvider(
&store.IdentityProviderOAuth2Config{
ClientID: testClientID,
&storepb.OAuth2Config{
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{
TokenUrl: fmt.Sprintf("%s/oauth2/token", s.URL),
UserInfoUrl: fmt.Sprintf("%s/oauth2/userinfo", s.URL),
FieldMapping: &storepb.FieldMapping{
Identifier: "sub",
DisplayName: "name",
Email: "email",

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