Compare commits

...

378 Commits

Author SHA1 Message Date
haitaoo 0aa98fb2af fix(path_ignore.yaml): Update the path_ignore.yaml list
Build DockerHub Image / build (push) Has been cancelled
Goreleaser / build-goreleaser (push) Has been cancelled
2023-05-09 17:53:45 +08:00
haitaoo dd14fa7b52 fix(navigateToLogin): navigateToLogin only jumps to the internal login page 2023-05-09 17:41:11 +08:00
haitaoo adc75083b2 fix(Login): Dealing with external login links not jumping properly 2023-05-09 16:56:58 +08:00
LinkinStars 26ad0dd3c3 chore(Dockerfile): add ANSWER_MODULE ENV 2023-05-09 11:36:39 +08:00
LinkinStars fb3b6a862f chore(gomod): update go mod and swagger doc 2023-05-09 10:54:42 +08:00
LinkinStars b0a04bbf11 Merge remote-tracking branch 'github/feat/1.1.2/user-center' into beta.2/1.1.0
# Conflicts:
#	docs/docs.go
#	docs/swagger.json
#	docs/swagger.yaml
#	go.mod
#	i18n/en_US.yaml
#	internal/base/reason/reason.go
#	internal/migrations/init.go
#	internal/migrations/migrations.go
2023-05-09 10:49:00 +08:00
LinkinStars 1de9ef6a89 Merge remote-tracking branch 'github/feat/1.1.1/user-center' into beta.2/1.1.0
# Conflicts:
#	go.mod
#	i18n/en_US.yaml
#	internal/base/reason/reason.go
2023-05-09 10:39:04 +08:00
haitaoo d61e0dab9e Merge branch 'feat/1.1.2/ui' into beta.2/1.1.0 2023-05-09 10:31:41 +08:00
LinkinStars dacc27e373 Merge branch 'beta/1.1.0' into beta.2/1.1.0
# Conflicts:
#	internal/migrations/migrations.go
#	internal/schema/user_schema.go
#	ui/src/common/interface.ts
2023-05-09 10:15:58 +08:00
LinkinStars 3a6439a65d Merge branch 'feat/1.1.0/update-email' into test 2023-05-08 17:53:25 +08:00
LinkinStars 4f468b58d0 feat(password): logout other user when update password 2023-05-08 17:51:29 +08:00
aichy126 b06695f2e1 Merge branch 'feat/1.1.0/fix' into test 2023-05-08 16:02:22 +08:00
aichy126 58bec8ddd1 upgrade question post time 2023-05-08 16:01:57 +08:00
LinkinStars ba66ae9d4d Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-05-08 15:56:20 +08:00
LinkinStars efb78124a9 feat(notification): remove user info when meet down vote notification. 2023-05-08 15:55:35 +08:00
shuai 016fd7b412 fix: add someone for anonymous user display 2023-05-08 15:54:01 +08:00
dashuai 140550795f Merge pull request #343 from answerdev/feat/ui-1.1.0
fix: add someone for anonymous user display
2023-05-08 15:40:36 +08:00
shuai 54c07ba829 fix: add someone for anonymous user display 2023-05-08 15:39:44 +08:00
dashuai fcb84b7dfb Merge pull request #342 from answerdev/feat/ui-1.1.0
fix: add preventdefault for form submit
2023-05-06 18:48:47 +08:00
shuai 225d766f9c fix: add preventdefault for form submit 2023-05-06 18:48:14 +08:00
dashuai 7547d9f91e Merge pull request #341 from answerdev/feat/ui-1.1.0
fix: change email lable
2023-05-06 16:39:05 +08:00
shuai bc9c4ef1b9 fix: change email lable 2023-05-06 16:38:18 +08:00
dashuai 5f113285a1 Merge pull request #340 from answerdev/feat/ui-1.1.0
Feat/UI 1.1.0
2023-05-06 16:15:20 +08:00
LinkinStars 16f2de0ab4 Merge branch 'feat/1.1.0/update-email' into test 2023-05-06 15:53:49 +08:00
LinkinStars b50835f04e feat(email): verify password when update email 2023-05-06 15:53:10 +08:00
shuai 73598e8af6 fix: change emial need confirm password 2023-05-06 15:44:25 +08:00
shuai dd47eeced6 fix: change emial need confirm password 2023-05-06 15:41:00 +08:00
LinkinStars e45a5f151f Merge branch 'fix/1.1.0/personal-page' into test 2023-05-06 11:59:04 +08:00
LinkinStars fafdc5435a fix(personal): rename all wrong name about page size 2023-05-06 11:57:47 +08:00
LinkinStars 276e87af08 feat(notification): self-questioning does not generate notifications 2023-05-06 10:30:06 +08:00
haitaoo 2672f43525 fix(loggedUserInfoStore): Avoiding errors in update operations 2023-05-05 17:39:24 +08:00
LinkinStars 85e370d7ff Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-05-04 19:24:07 +08:00
LinkinStars c56d54b0fe feat(notification): add upvote and down vote notification 2023-05-04 19:22:25 +08:00
haitaoo 7d722cde4f fix(HotQuestions): Automatically hide the HotQuestions module when no data is available 2023-05-04 19:21:19 +08:00
haitaoo 9afb26fcba feat(SchemaForm): UIAction support for refreshing form configuration functions 2023-05-04 19:19:11 +08:00
LinkinStars 3952439d49 feat(agent): add site info url for agent 2023-05-04 15:40:05 +08:00
haitaoo c02709a337 Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-29 12:38:48 +08:00
haitaoo f46af67b2c fix(setupApp): Update setupApp logic to prevent invalid Promise.allSettled from being triggered 2023-04-29 12:37:30 +08:00
haitaoo c843dddf0b feat(SchemaForm): Optimising the action feedback of the Button component 2023-04-29 12:25:54 +08:00
LinkinStars ba475ddfbb feat(user): add externalID for login user 2023-04-28 20:08:22 +08:00
LinkinStars cba45d1eaa feat(user): check username after check user center config 2023-04-28 15:46:47 +08:00
shuai c7c2aad205 fix: meta theme-color support primary color 2023-04-28 11:09:43 +08:00
LinkinStars 9d304f0186 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-27 14:40:03 +08:00
shuai 9f2607cf35 fix: qrcode max width 240 2023-04-27 14:33:53 +08:00
LinkinStars e67dd59ab2 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-27 14:14:39 +08:00
shuai 40ac54eb46 fix: logo max height changed 2023-04-27 14:09:20 +08:00
shuai e3a49132b2 fix: supplement i18n for admin flags page 2023-04-27 11:32:06 +08:00
LinkinStars db50e33db8 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-27 11:06:47 +08:00
shuai 65e5af6482 style: logo remove roundes class 2023-04-27 11:04:50 +08:00
aichy126 eeb67dd21e update add question postUpdateTime 2023-04-26 18:25:24 +08:00
aichy126 b56aa16641 update makdown kbd 2023-04-26 18:11:26 +08:00
aichy126 78850be999 update markdown 2023-04-26 17:30:32 +08:00
LinkinStars 3eaa2b6293 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-26 15:11:19 +08:00
shuai b509739b35 fix: oauth bind emial styles adjustment 2023-04-26 15:02:54 +08:00
shuai 64f2fc70cd style: change uses pages style 2023-04-26 14:59:37 +08:00
LinkinStars b26da16ced Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-26 11:05:01 +08:00
LinkinStars ef47d3da01 feat(user-center): allow to create user in admin page 2023-04-26 11:01:45 +08:00
haitaoo 29fddeca35 refactor(admin/users): Adjustment of the way to determine the availability of administrator actions 2023-04-26 10:33:45 +08:00
LinkinStars 758a7cde95 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-25 16:54:51 +08:00
haitaoo 05898157c8 fix(Question): Make the focus style of the content editor follow the theme configuration 2023-04-25 16:37:43 +08:00
haitaoo bdd00e636b fix(auth-landing): Improve the stability of the process of obtaining login user information 2023-04-25 16:22:32 +08:00
aichy126 f28b743519 update gpgrade pin list 2023-04-25 15:49:54 +08:00
LinkinStars e377daaac9 fix(plugin): fix rank config about question show/hide 2023-04-25 15:34:35 +08:00
haitaoo f89687d021 fix(ImgViewer): Make ImgViewer clickable to close the image 2023-04-25 15:09:02 +08:00
LinkinStars df9b08abcf fix(plugin): fix update plugin status to default 2023-04-25 14:45:54 +08:00
LinkinStars 496df590e8 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-25 14:33:34 +08:00
LinkinStars 0f8c4c79b4 fix(plugin): fix rank config about question show/hide 2023-04-25 14:32:27 +08:00
aichy126 6b53609caf update upgrade pin list 2023-04-25 14:31:46 +08:00
haitaoo ddda7e6d5a Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-25 14:29:43 +08:00
haitaoo 9a07d5092a fix(Question): add ImgViewer for answer 2023-04-25 14:29:30 +08:00
shuai 6b2f191d49 fix: tags sort btn style adjustment 2023-04-25 14:18:44 +08:00
aichy126 201a776cd6 update upgrade pin list 2023-04-25 14:16:51 +08:00
LinkinStars f391d3a8e8 chore(log): remove auth log 2023-04-25 14:14:21 +08:00
LinkinStars 76dfc362c9 fix(plugin): fix rank config about question show/hide 2023-04-25 14:09:06 +08:00
haitaoo 031954520c fix(guard): Supplementary IGNORE_PATH_LIST 2023-04-25 13:42:34 +08:00
LinkinStars aaeca560e5 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-25 13:17:27 +08:00
haitaoo 18d25fddbe debug: auth-landing 2023-04-25 13:17:17 +08:00
haitaoo a09e2fbe8a debug: guard 2023-04-25 13:17:17 +08:00
haitaoo 5a4c22c393 fix(guard): Supplementary IGNORE_PATH_LIST 2023-04-25 13:16:37 +08:00
aichy126 f024f7aa86 Merge branch 'feat/1.1.2/user-center' of github.com:answerdev/answer into feat/1.1.2/user-center 2023-04-25 12:29:33 +08:00
aichy126 6f32e51319 update siteinfo 2023-04-25 12:29:29 +08:00
haitaoo d672e31815 debug: auth-landing 2023-04-25 12:24:08 +08:00
haitaoo 55aeec174c debug: guard 2023-04-25 12:17:12 +08:00
aichy126 779bd86e1d add static cache-control 2023-04-25 10:57:34 +08:00
LinkinStars 3f4ed4527a Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-25 10:55:30 +08:00
LinkinStars f04d5a03f8 fix(plugin): fix plugin auth router 2023-04-25 10:54:35 +08:00
haitaoo ddaf4b7863 fix(guard): Unifying the definition and use of IGNORE_PATH_LIST 2023-04-25 10:52:37 +08:00
aichy126 587f0aa1e3 add static cache-control 2023-04-24 18:23:39 +08:00
aichy126 c3c7eb962d update tag search 2023-04-24 17:54:47 +08:00
LinkinStars c37987683b chore(i18n): update translation 2023-04-24 15:20:33 +08:00
LinkinStars 2115a4a4ba Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-24 15:19:14 +08:00
haitaoo 8be0b2bffd Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-24 15:11:36 +08:00
haitaoo 096c949ae0 fix(Header): Suppress padding in nav-link 2023-04-24 15:11:19 +08:00
LinkinStars 16bb4d89c1 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-24 14:57:14 +08:00
shuai f844978662 fix: remove logo max width class 2023-04-24 14:42:18 +08:00
LinkinStars f6b98f65be Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center
# Conflicts:
#	ui/src/common/interface.ts
#	ui/src/services/client/question.ts
2023-04-24 14:22:24 +08:00
shuai 4fa0949abc chore: bootstrap icons upgrade to 1.10.4 2023-04-24 14:20:52 +08:00
shuai 0292cbb92b feat: use susppened page add contact_email 2023-04-24 14:20:52 +08:00
haitaoo 3b5c3fc3ff fix(Header): Fixed the colour of the user centre icon in the Header 2023-04-24 14:20:52 +08:00
aichy126 e95e2f3a2f update search question 2023-04-24 14:17:52 +08:00
shuai 1d95b3b510 fix: hot questions add params in_days 2023-04-24 14:16:44 +08:00
shuai 6349f42234 chore: bootstrap icons upgrade to 1.10.4 2023-04-23 18:28:43 +08:00
haitaoo 971bf4da61 Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-23 17:04:47 +08:00
haitaoo 62b5008bc5 fix(Header): Fixed the colour of the user centre icon in the Header 2023-04-23 17:04:08 +08:00
aichy126 f0ddd30e5d update github action 2023-04-23 15:51:12 +08:00
shuai b24fc1ff22 feat: use susppened page add contact_email 2023-04-23 15:16:47 +08:00
aichy126 d7c6172cb4 update github action 2023-04-23 15:13:02 +08:00
aichy126 bed55985dd Merge branch 'feat/1.1.2/user-center' of github.com:answerdev/answer into feat/1.1.2/user-center 2023-04-23 15:10:52 +08:00
aichy126 499cfe9917 update search question 2023-04-23 15:10:44 +08:00
aichy126 2e77289428 update github action 2023-04-23 11:54:47 +08:00
LinkinStars 54497ed932 Merge branch 'github-main' into beta/1.1.0 2023-04-23 11:06:52 +08:00
LinkinStars 7824797648 fix(rank): update answer accept rank config 2023-04-21 17:24:23 +08:00
LinkinStars f500da413c feat(init): add users init config 2023-04-21 17:23:31 +08:00
LinkinStars d5f95c5bde Merge branch 'github-main' into beta/1.1.0
# Conflicts:
#	internal/migrations/migrations.go
2023-04-21 16:54:04 +08:00
LinkinStars 9f9258fccd chore(migration): rename upgrade version 2023-04-21 16:45:06 +08:00
LinkinStars e49b6fbb54 fix(rank): remove accept answer rank config 2023-04-21 16:43:07 +08:00
LinkinStars c5126b4fe0 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-21 15:17:15 +08:00
shuai 95a58e19a7 Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-21 14:22:32 +08:00
shuai 90cd9de7fe fix: add safe-area-inset-bottom for footer 2023-04-21 14:22:23 +08:00
haitaoo 9edfde5759 fix(user-center): Set the pageTitle for the user center page 2023-04-21 12:36:48 +08:00
haitaoo 2498ab139a fix(user-center/auth): Add route guards to allow access only to non-logged-in users 2023-04-21 11:45:36 +08:00
LinkinStars 083c5a8a4c Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-20 19:10:46 +08:00
LinkinStars af08b3e392 feat(usercenter): plugin user center add i18n 2023-04-20 19:10:24 +08:00
haitaoo 40cdca8adf feat(SchemaForm): All form components support the readOnly property 2023-04-20 18:56:37 +08:00
LinkinStars 3c96abfcc5 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-20 18:06:09 +08:00
haitaoo cec02e8000 fix(guard.js): Delaying the UserAgent request so that the Language is correctly obtained 2023-04-20 18:02:10 +08:00
LinkinStars 045cd0b1e8 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-20 17:52:25 +08:00
haitaoo 944e773392 fix(Login): Improve the prompt text for the Login process 2023-04-20 17:51:43 +08:00
haitaoo 2db93af38b Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-20 17:04:02 +08:00
haitaoo 86c4c685ee fix(SchemaForm): Extending the Button component 2023-04-20 17:03:52 +08:00
aichy126 0f5e9845f9 Merge branch 'dev' into beta/1.1.0 2023-04-20 16:56:21 +08:00
LinkinStars 1bbe005f66 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-20 16:13:52 +08:00
haitaoo 128d5c9f87 fix(WeComAuth): Optimise QR code margin styles 2023-04-20 16:11:46 +08:00
haitaoo d3f02a35d9 fix(ImgViewer): Migrate ImgViewer from hooks to component 2023-04-20 16:00:27 +08:00
shuai 2253a87aea fix: error content support custome title 2023-04-20 15:26:43 +08:00
shuai f859aa2fba fix: meta attributes add viewport-fit cover 2023-04-20 15:08:55 +08:00
LinkinStars c8cf70c112 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-20 11:19:43 +08:00
haitaoo 61cb84ac06 fix(Admin/users): Administrator password reset access to user_password_agent_enabled control 2023-04-20 11:18:20 +08:00
LinkinStars 8adc975699 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center
# Conflicts:
#	internal/migrations/init.go
2023-04-20 10:36:54 +08:00
LinkinStars cfafd3b3fb Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-20 10:35:15 +08:00
haitaoo 67cbe2bd83 Merge branch 'dev' into feat/1.1.2/ui 2023-04-20 10:34:16 +08:00
LinkinStars b215a5b738 Merge remote-tracking branch 'github/main' into beta/1.1.0 2023-04-20 10:18:48 +08:00
LinkinStars 8c45d75968 fix(rank): fix the wrong reputation authority configuration 2023-04-20 10:04:43 +08:00
haitaoo 726fab77ac fix: Adding logical control to the signup button on the home page 2023-04-19 18:46:26 +08:00
LinkinStars 703e76ba7b Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-19 18:26:15 +08:00
haitaoo 24afb5e21b feat: Add enabled_original_user_system to the login address logic 2023-04-19 18:25:34 +08:00
LinkinStars 8fb303d024 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center
# Conflicts:
#	Makefile
#	i18n/en_US.yaml
#	internal/migrations/migrations.go
2023-04-19 17:50:35 +08:00
haitaoo a045060b76 Merge branch 'feat/ui-1.1.0' into feat/1.1.2/ui 2023-04-19 17:47:39 +08:00
haitaoo f297330249 Merge branch 'dev' into feat/ui-1.1.0 2023-04-19 17:27:33 +08:00
LinkinStars fc9de356d6 Merge branch 'github-main' into beta/1.1.0
# Conflicts:
#	Makefile
#	internal/migrations/migrations.go
#	ui/src/common/interface.ts
2023-04-19 16:57:48 +08:00
LinkinStars 1d3f61e8a6 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-19 16:33:07 +08:00
LinkinStars b5ef7912d1 feat(usercenter): add user login API agent 2023-04-19 16:32:43 +08:00
haitaoo 901f2cb232 feat: Adding an auth page to the user center 2023-04-19 16:31:28 +08:00
haitaoo fd2c5f9946 refactor(SchemaForm): Making form components more independent 2023-04-19 11:13:34 +08:00
haitaoo 1799f9e7db fix(Personal): Replace Button link with Link 2023-04-18 19:35:15 +08:00
LinkinStars 9e0999ba55 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-18 18:22:09 +08:00
LinkinStars 86685202ac feat(usercenter): add user center email auth check 2023-04-18 18:21:50 +08:00
haitaoo 54b7de30a0 fix(RouteGuard): Handle the issue of RouteGuard not rendering children due to unexpected parent guard results not being handled correctly 2023-04-18 18:10:55 +08:00
LinkinStars f50ccea7e5 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-18 15:50:03 +08:00
haitaoo 095bc79758 feat: SchemaForm supports Button components 2023-04-18 15:48:43 +08:00
haitaoo 1402d9e5dc Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-18 11:53:28 +08:00
haitaoo 6350cb861e feat: The account form supports the accountAgent logic 2023-04-18 11:53:13 +08:00
haitaoo 7445dbc336 feat: Profile forms support read-only logic 2023-04-18 11:42:30 +08:00
haitaoo a38c6a833f feat: Show privilege form dynamic fields 2023-04-18 10:56:55 +08:00
LinkinStars 673ebfbb78 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-18 10:49:44 +08:00
shuai 3def6cabd8 fix: add getUserAgentType function 2023-04-18 10:48:23 +08:00
haitaoo 357d474503 Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-17 18:37:39 +08:00
haitaoo 42faac87ee feat: user-center/auth-failed 2023-04-17 18:37:27 +08:00
shuai fa0efd82a8 Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-17 18:32:52 +08:00
shuai cdf5ffa0a8 feat: add custome sidebar fucntion 2023-04-17 18:32:43 +08:00
LinkinStars 4c4381e140 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-17 18:04:49 +08:00
LinkinStars 8800b376d5 feat(privilege): add privileges API 2023-04-17 18:04:28 +08:00
aichy126 910484013c update custom_sidebar 2023-04-17 17:59:53 +08:00
haitaoo 7bfb5f0126 Merge branch 'feat/1.1.2/ui' of github.com:answerdev/answer into feat/1.1.2/ui 2023-04-17 17:55:09 +08:00
haitaoo fbb29a0b74 feat: admin/privilege 2023-04-17 17:54:51 +08:00
haitaoo 9da53361a7 feat: admin/settings-users 2023-04-17 16:42:53 +08:00
haitaoo 10afa0a47b refactor: AccordionNav, SchemaForm 2023-04-17 15:08:51 +08:00
shuai 285a44be1b feat: add login fail page 2023-04-17 14:24:00 +08:00
LinkinStars 2bd0ef8a24 Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center 2023-04-17 11:08:54 +08:00
haitaoo c62436a703 feat(admin/users): Determine if user status changes are allowed via adminUcAgent 2023-04-16 15:56:20 +08:00
haitaoo 36e183dd79 fix(RouteGuard): Avoid performing route guarding at the wrong time 2023-04-16 15:19:09 +08:00
LinkinStars 336f54ff49 fix(siteinfo): rename GetSiteInfoByType 2023-04-16 12:42:45 +08:00
LinkinStars a5a3664f90 Merge branch 'feat/1.1.2/permission' into feat/1.1.2/user-center
# Conflicts:
#	internal/base/constant/constant.go
2023-04-16 12:40:16 +08:00
LinkinStars a2ba1144ee feat(plugin): add user center config user role API 2023-04-16 12:39:02 +08:00
haitaoo f71ece495c feat(admin/login): Add Email Registration and Allowed Email Domains control options 2023-04-16 11:59:17 +08:00
LinkinStars 0f7121d6fe fix(merge): update go mod 2023-04-14 18:24:09 +08:00
LinkinStars e280358b7c Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center
# Conflicts:
#	Makefile
#	cmd/answer/main.go
#	go.mod
#	go.sum
#	i18n/en_US.yaml
#	internal/base/reason/reason.go
#	internal/service/uploader/upload.go
2023-04-14 18:17:28 +08:00
LinkinStars ec4c7ab6a7 feat(site-users): add site info config about users 2023-04-14 18:12:04 +08:00
haitaoo f02f62112a feat: Access to WeCome user login 2023-04-14 17:42:07 +08:00
LinkinStars a3cc0ac38a feat(usercenter): add user center plugin and agent 2023-04-14 17:33:22 +08:00
haitaoo 2e8e6be9af fix(guard): Ignore user information interface errors triggered in guard 2023-04-12 15:18:11 +08:00
haitaoo 8584a0e77b Merge branch 'feat/ui-1.1.0' into sf-site-migration/ui 2023-04-12 12:26:38 +08:00
haitaoo f37218bc70 Merge branch 'dev' into feat/ui-1.1.0 2023-04-12 12:25:41 +08:00
haitaoo cfd017de26 Merge branch 'feat/ui-1.1.0' into sf-site-migration/ui 2023-04-12 12:22:46 +08:00
haitaoo b68212e840 fix(Comment): Make comment boxes the same width 2023-04-12 12:08:48 +08:00
LinkinStars d641bdf11e Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-04-12 10:54:16 +08:00
LinkinStars 4cfa0cd8fe feat(permission): add privileges API 2023-04-12 10:35:40 +08:00
aichy126 6531cf9e6c update goproxy 2023-04-11 17:31:28 +08:00
aichy126 6ac96948c3 Merge branch 'fix/ui-revision' into beta/1.1.0 2023-04-11 16:28:42 +08:00
aichy126 83c2c24683 update cmd Version 2023-04-11 16:17:35 +08:00
aichy126 f50414adcf update Revision 2023-04-11 14:49:11 +08:00
aichy126 a9381056e9 swagger 2023-04-11 14:21:34 +08:00
haitaoo 9868b2d9c0 Merge branch 'feat/ui-1.1.0' into beta/1.1.0 2023-04-11 11:59:59 +08:00
haitaoo 0c3efb66ab Merge branch 'feat/1.0.8/ui' into feat/ui-1.1.0 2023-04-11 11:56:10 +08:00
LinkinStars 7527b62d28 chore(gomod): update go mod 2023-04-11 11:31:47 +08:00
LinkinStars 759f1491c9 Merge branch 'github-main' into beta/1.1.0
# Conflicts:
#	Makefile
#	cmd/answer/main.go
#	go.mod
#	go.sum
#	internal/base/reason/reason.go
#	internal/service/uploader/upload.go
#	ui/src/utils/common.ts
2023-04-11 11:28:57 +08:00
LinkinStars 1c235dbd9b feat(permission): add privileges 2023-04-11 10:27:18 +08:00
haitaoo 09e4ee10cb Merge branch 'feat/ui-1.1.0' into sf-site-migration/ui 2023-04-10 17:22:48 +08:00
LinkinStars 6211b468b3 Merge remote-tracking branch 'github/feat/ui-1.1.0' into beta/1.1.0 2023-04-10 16:18:06 +08:00
haitaoo b4870d066c fix(admin/smtp): Make the smtp_authentication field value more accurate 2023-04-10 15:59:21 +08:00
haitaoo 9a6f770062 fix(SchemaForm): Make setDefaultValueAsDomBehaviour compatible with empty string values 2023-04-10 11:43:42 +08:00
LinkinStars 7721946922 Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-04-06 18:19:29 +08:00
haitaoo fd31a26b7b fix(Questions): Optimising the width of videos in question content 2023-04-06 18:04:12 +08:00
LinkinStars 388d1691b0 Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-04-04 17:30:13 +08:00
haitaoo 056fbd8990 fix(login): Does not auto proxy the ucAgent.login_redirect_url jump on the users/login page 2023-04-04 17:20:29 +08:00
LinkinStars 71537ec62c Merge remote-tracking branch 'github/beta/1.1.0' into feat/1.1.1/user-center 2023-04-04 15:15:48 +08:00
LinkinStars e914db6a9b Merge branch 'feature-plugin' into feat/1.1.1/user-center 2023-04-04 15:13:58 +08:00
LinkinStars e3ff77a8ef docs(i18n): update zh_CN.yaml 2023-04-04 11:03:30 +08:00
LinkinStars ad13b97eaf feat(rank): removed reputation-related leaderboard and notification content 2023-04-03 18:13:35 +08:00
LinkinStars b046c8005b Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-04-03 17:13:25 +08:00
haitaoo 391ec35fac fix(ucAgent): Correct handling of login/signUp clicks 2023-04-03 17:12:31 +08:00
LinkinStars 2a121c573b feat(router): add middleware for user router 2023-04-03 11:17:38 +08:00
LinkinStars eb0e54c724 fix(activity): show the trigger user info when display timeline 2023-04-03 11:14:28 +08:00
LinkinStars 6daaa69a93 Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-03-31 11:38:32 +08:00
haitaoo a7d39a1c81 fix(QuestionDetail): Compatible with user_info being null 2023-03-31 11:24:40 +08:00
LinkinStars 31af841777 Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-03-30 12:19:36 +08:00
haitaoo 24d9b8a641 fix(admin/users): User state management support ucAgent logic 2023-03-30 11:55:45 +08:00
haitaoo fd13b3580d fix(UserProfile): Optimising the typography of ucBranding 2023-03-30 11:28:35 +08:00
LinkinStars e92e7070f0 Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-03-30 11:12:31 +08:00
haitaoo ad733dc505 fix(UserProfile): Optimising the presentation logic of ucBranding 2023-03-30 11:09:47 +08:00
haitaoo 8d4bbfc90b fix(LoggedUserInfo): Make the logic for getting information about the currently logged in user more robust 2023-03-30 10:53:33 +08:00
haitaoo be22b49139 Merge branch 'feat/ui-1.1.0' into sf-site-migration/ui 2023-03-30 10:15:03 +08:00
LinkinStars 5d1bfeac6d Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-29 18:09:21 +08:00
haitaoo b7977b1367 fix(admin): The page under the root path can get the correct width 2023-03-29 17:47:49 +08:00
LinkinStars e5df07ad7c feat(plugin): add user center plugin 2023-03-29 17:12:45 +08:00
LinkinStars aa13657e8d Merge remote-tracking branch 'github/sf-site-migration/ui' into feat/1.1.1/user-center 2023-03-29 17:07:54 +08:00
haitaoo 481901efd7 fix(Editor): Enhancing the robust of setting editor values 2023-03-29 17:03:28 +08:00
haitaoo 9fc8511bf5 feat: Make admin/users support UserCenterAgent 2023-03-29 16:51:31 +08:00
haitaoo 934b82de30 feat: Make user detail support UserCenterAgent 2023-03-29 16:10:01 +08:00
haitaoo b47fb3da1c feat: Make user settings support UserCenterAgent 2023-03-29 15:24:18 +08:00
haitaoo 4b1f985be7 fix(localize): Correct merging of local and remote language lists 2023-03-28 17:56:04 +08:00
haitaoo 528cba58e1 feat(Header): Show userCenterAgent info 2023-03-28 17:29:16 +08:00
haitaoo e8e5bdf181 feat: Implementing userCenter related functions 2023-03-28 16:19:53 +08:00
aichy126 4de996ed07 update script
Build DockerHub Image / build (push) Has been cancelled
Goreleaser / build-goreleaser (push) Has been cancelled
2023-03-27 17:28:51 +08:00
aichy126 2abc096176 update script 2023-03-27 17:28:26 +08:00
aichy126 ad20a8ff1f go mod add replace 2023-03-27 17:04:47 +08:00
LinkinStars c976b79bfd feat(build): merge build script from feature plugin 2023-03-27 14:43:17 +08:00
LinkinStars 072bf002c2 chore(plugin): remove sample 2023-03-27 13:37:32 +08:00
LinkinStars 689297e14d docs(makefile): upgrade 1.1.0 2023-03-27 13:34:18 +08:00
LinkinStars 28e9916468 refactor(merge): merge 1.0.7 2023-03-21 11:53:15 +08:00
LinkinStars 9bd9b475f4 Merge branch 'feat/ui-1.1.0' into feature-plugin
# Conflicts:
#	internal/base/reason/reason.go
#	internal/schema/user_schema.go
#	internal/service/user_service.go
2023-03-21 11:45:50 +08:00
shuai f4212e2cd0 fix: resolve conflict 2023-03-21 11:37:22 +08:00
LinkinStars 9950ed5a40 feat(plugin): add label ui option for switch config type 2023-03-17 17:43:35 +08:00
LinkinStars 2aae2f771c Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-17 17:21:45 +08:00
haitaoo 41404070a7 fix(SchemaForm): Move the label field to the switch component configuration item, which is only used by this component 2023-03-17 17:20:12 +08:00
LinkinStars a7ff4cf3b7 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-17 15:17:12 +08:00
haitaoo 38226bf3a0 fix(Admin): Set the default form width for admin pages to 6 columns 2023-03-17 15:15:08 +08:00
LinkinStars 1ec3f2b704 feat(plugin): add dispatcher for connector login plugin 2023-03-16 15:39:04 +08:00
LinkinStars 5e6fb1096e feat(plugin): upgrade add user external login table 2023-03-16 14:58:03 +08:00
LinkinStars 098873eea6 feat(plugin): update connector receiver interface 2023-03-16 14:57:17 +08:00
LinkinStars 824b68c152 feat(plugin): remove debug log 2023-03-16 14:56:44 +08:00
LinkinStars 40fb10b4d9 feat(plugin): set plugin default status is disabled 2023-03-16 14:56:06 +08:00
LinkinStars 210d26289a Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-15 11:42:23 +08:00
haitaoo 71d439cca0 fix(AccordionNav): Set text-trancate for link content 2023-03-15 11:33:46 +08:00
LinkinStars 4b78e3583f Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-15 11:27:15 +08:00
haitaoo bfc0bb5ac2 fix(SchemaForm): Make SchemaForm correctly support ui:widget of PluginForm and have the correct default values. 2023-03-15 10:54:36 +08:00
LinkinStars 6b07d08838 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-10 18:01:24 +08:00
LinkinStars 91f5f3290c feat(plugin): Return whether to set password status at login 2023-03-10 18:01:16 +08:00
shuai 89ed70f5a8 fix: user settings oauth add redirect path 2023-03-10 17:58:49 +08:00
LinkinStars 19108b2124 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-10 15:38:34 +08:00
shuai ecf24b5a96 fix: account no password to change password shuold hidden old pass input 2023-03-10 15:10:04 +08:00
shuai 8a57057d9a fix: oauth plugin icon style 2023-03-10 14:44:12 +08:00
LinkinStars b44824cf78 feat(plugin): If user has only one external login and never set password, he can't unbind it 2023-03-10 10:42:45 +08:00
LinkinStars 9c68284623 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-10 09:59:33 +08:00
shuai 675c2868e4 fix: oauth icon add styles adjustment 2023-03-10 09:46:38 +08:00
LinkinStars adcb47a5e1 fix(plugin): Add cache plugin interface 2023-03-08 17:14:55 +08:00
LinkinStars 1f05338aae fix(plugin): Add storage plugin interface 2023-03-08 16:29:55 +08:00
LinkinStars d5b5d9a51c fix(plugin): Exception when registering a plugin and finding that the slug_name is not unique. 2023-03-07 17:54:03 +08:00
LinkinStars de4d156ac9 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-03-07 15:50:50 +08:00
haitaoo 3312b352d2 Merge branch 'dev' into feat/ui-1.1.0 2023-03-07 15:40:34 +08:00
LinkinStars 4a8ae2b166 Merge branch 'dev' into feature-plugin 2023-03-07 15:39:07 +08:00
LinkinStars 2ebf057d53 feat(plugin): add display name and username for connector 2023-03-02 12:13:58 +08:00
LinkinStars b876b102e2 Merge branch 'feat/ui-1.1.0' into feature-plugin 2023-03-02 11:56:30 +08:00
haitaoo 896e204ae0 feat(InstalledPlugins): Handling the link address of a plugin by its name 2023-03-02 11:34:53 +08:00
LinkinStars 666471ec24 feat(plugin): add link address for plugin 2023-03-02 11:03:24 +08:00
LinkinStars a1bdb2bd80 feat(plugin): synchronize avatar when bind user 2023-03-02 11:03:06 +08:00
LinkinStars 4c38c73017 feat(plugin): Third-party login to synchronize user avatars 2023-03-02 10:24:32 +08:00
LinkinStars 1065946c43 fix(plugin): If user login with external account and email is exist, active user directly. 2023-03-01 10:48:11 +08:00
LinkinStars 334a1eceaa fix(build): Compile with the current local version instead of the latest version when local mod replacement. 2023-02-28 16:01:14 +08:00
LinkinStars 66ace7edcd Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-02-27 17:00:40 +08:00
LinkinStars 8fe50e93f2 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-02-27 16:37:25 +08:00
shuai 1e955a3495 fix: plugin oauth ui changed 2023-02-27 16:35:33 +08:00
shuai 7ded5056f0 fix: register show oauth plugin 2023-02-27 15:12:28 +08:00
LinkinStars 17c621bfbb Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-02-22 17:54:55 +08:00
LinkinStars 822a96721a feat(plugin): remove plugin translator context 2023-02-22 17:48:05 +08:00
joyqi 73dc7e744f Change Translator receiver from pointer to struct. 2023-02-22 17:32:13 +08:00
joyqi 4809bab5c7 Check whether the translator struct is nil before translating. 2023-02-22 17:01:48 +08:00
LinkinStars 5bd8b1895b refactor(plugin): try to refactor plugin translation struct 2023-02-22 16:49:03 +08:00
joyqi ffb1e07acb Make plugin translator as a struct. 2023-02-22 16:21:02 +08:00
LinkinStars 9409db902a docs(i18n): add zh_CN plugin translation 2023-02-22 15:05:32 +08:00
LinkinStars 29da80a11e fix(tr): return original data when translator not exist 2023-02-22 15:05:03 +08:00
LinkinStars f7e3e59822 feat(init): set default time zone 2023-02-22 15:04:03 +08:00
haitaoo 26c9ff6998 fix(Plugins NavMenu): Getting the Plugins navigation menu to correctly match i18n 2023-02-22 11:42:03 +08:00
haitaoo 57843bc0ce fix(TIMEZONE): Set UTC+0 to UTC(dayjs does not recognize UTC+0) 2023-02-22 11:34:38 +08:00
haitaoo 3b9070cf4f fix(Interface): Set the correct default values for Interface forms(time_zone) 2023-02-22 11:05:30 +08:00
LinkinStars a166ea8eb3 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin
# Conflicts:
#	ui/src/hooks/index.ts
2023-02-21 18:14:49 +08:00
haitaoo e899f4b362 fix(router): Set the correct loader return value for the new version of the router component 2023-02-21 18:12:05 +08:00
haitaoo 8df84b7684 Merge branch 'main' into feat/ui-1.1.0 2023-02-21 18:07:03 +08:00
LinkinStars 6c70bc814e refactor(gomod): update go mod 2023-02-21 17:41:26 +08:00
LinkinStars 593ffad92d Merge branch 'github-main' into feature-plugin
# Conflicts:
#	go.mod
#	go.sum
#	ui/src/hooks/index.ts
#	ui/src/router/index.tsx
2023-02-21 17:37:36 +08:00
LinkinStars 373b454728 refactor(plugin): remove comment code 2023-02-21 17:32:52 +08:00
LinkinStars b229d12bb4 feat(plugin): plugin internationalization 2023-02-21 17:28:05 +08:00
LinkinStars a148607763 feat(plugin): Modify the plugin base description information to the internationalization field 2023-02-21 17:27:14 +08:00
joyqi 3c2c13f7c4 Add sample codes for plugin i18n 2023-02-20 11:46:44 +08:00
LinkinStars 12109fdb24 Merge branch 'github-main' into feature-plugin
# Conflicts:
#	ui/src/utils/guard.ts
2023-02-15 15:43:19 +08:00
LinkinStars daa2a785e9 fix(plugin): Adding missing table. 2023-02-15 15:34:11 +08:00
LinkinStars ff64dabd16 Merge remote-tracking branch 'github/feat/ui-1.1.0' into feature-plugin 2023-02-15 15:03:51 +08:00
LinkinStars 3fbecefdf3 feat(plugin): Support for specifying different plugin version 2023-02-14 14:23:35 +08:00
LinkinStars f9a1ffb7e5 Merge branch 'github-dev' into feat/ui-1.1.0
# Conflicts:
#	i18n/zh_CN.yaml
2023-02-08 14:40:39 +08:00
LinkinStars 2fe00d4630 Merge branch 'github-dev' into feature-plugin 2023-02-08 12:01:27 +08:00
LinkinStars 9c2d2b1df4 Merge branch 'github-main' into feature-plugin 2023-02-06 11:25:53 +08:00
shuai dfcd64e2a4 fix: optimize third-party login display name 2023-02-03 17:49:48 +08:00
haitao(lj) e8246433a2 Merge branch 'feat/ui-1.0.4' into feat/ui-1.1.0 2023-02-03 16:49:14 +08:00
shuai ea4c67cf2a fix: add key 2023-02-02 18:06:04 +08:00
shuai 28e17d95c3 fix: oauth button styles for uses setting 2023-02-02 18:04:26 +08:00
shuai 47695b90cf fix: oauth plugin icons used base64 data 2023-02-02 15:23:28 +08:00
aichy126 9de92822f3 update make plugin 2023-02-02 14:59:02 +08:00
robin 7a78428cd5 fix(ui): Fix the problem of abnormal loading of form data 2023-02-02 14:17:29 +08:00
robin 4f0ec66710 refactor(ui): Modify the input type name 2023-02-02 11:44:30 +08:00
LinkinStar d538b531bd fix(plugin): remove quotation mark about version and add default revision 2023-02-01 18:20:33 +08:00
LinkinStar b4ae373309 fix(plugin): fix incorrect build version info package name 2023-02-01 18:14:54 +08:00
robin e3bc029fd2 docs(ui): update schema form doc 2023-02-01 17:48:07 +08:00
LinkinStar ebaaaff4f1 fix(plugin): add plugin migrations 2023-02-01 17:19:27 +08:00
LinkinStar 6a8d5e5984 Merge branch 'feat/1.0.4/comment' into feat/plugin/connector/github
# Conflicts:
#	internal/migrations/migrations.go
#	internal/migrations/v6.go
#	internal/service/user_common/user.go
#	internal/service/user_service.go
#	pkg/checker/reserved_username.go
#	ui/build/index.html
2023-02-01 17:02:00 +08:00
LinkinStar 44480d8986 feat(plugin): add build plugin commend 2023-02-01 16:50:18 +08:00
haitao(lj) e0fb22a62d refactor(guard): Initialize the app by route entry 2023-01-31 15:40:33 +08:00
robin 7e3adc9795 refactor(ui): Improve the schema form type declaration 2023-01-30 18:08:06 +08:00
robin 4db1250193 fix(ui): fix the exception prompt of uncontrolled components in form 2023-01-29 15:45:33 +08:00
shuai d608145266 fix: welcome title 2023-01-28 11:27:42 +08:00
shuai d0bd2f4a98 fix: install pages step innital 2023-01-28 11:07:35 +08:00
shuai bdaf254af1 fix: username verification format #181 2023-01-28 11:05:31 +08:00
LinkinStar 1802e6fa2e refactor(plugin): Directory movement 2023-01-28 10:07:13 +08:00
LinkinStar 87f649faf7 docs(makefile): update main pkg to answercmd 2023-01-18 15:38:26 +08:00
LinkinStar 3ccdc8670e feat(plugin): Extracting the main function to answercmd pkg 2023-01-18 15:37:22 +08:00
LinkinStar 406ee34fa8 feat(plugin): rename plugin config repo 2023-01-18 12:08:16 +08:00
LinkinStar 8d0bd2be20 feat(plugin): Add plugin config repo 2023-01-18 10:39:55 +08:00
haitao(lj) ace2e72e77 fix(PluginConfig): Set plugin config form can update 2023-01-18 10:31:03 +08:00
LinkinStar 62bfb2b3f6 feat(user): Add a have password flag 2023-01-17 17:27:20 +08:00
shuai 487c6def61 Merge branch 'feat/ui-1.1.0' of git.backyard.segmentfault.com:opensource/answer into feat/ui-1.1.0 2023-01-17 11:51:47 +08:00
shuai 3466da2b0c feat(ui): unified management welcome 2023-01-17 11:51:42 +08:00
haitao(lj) 703ff79ed8 feat(Account): Allow password change when the original password is empty 2023-01-17 11:49:11 +08:00
shuai 2e5eca08f1 fix(ui): add control for my logins component 2023-01-16 18:41:04 +08:00
shuai b35d2564b5 fix(ui): resolve conflict 2023-01-16 18:38:27 +08:00
haitao(lj) be1da88036 chore: Remove debug code and fix defaultOpenKey for AccordionNav 2023-01-16 18:23:11 +08:00
haitao(lj) be1811294e feat(Plugins): Installed Plugins and plugin settings done 2023-01-16 17:00:06 +08:00
LinkinStar 29aa3f7ef2 feat(plugin): add connector API 2023-01-16 16:27:23 +08:00
robin 152d8d2f68 feat(ui): Add plugin configuration 2023-01-13 16:14:59 +08:00
robin b8cfde0425 feat(ui): add installed plugins page 2023-01-13 16:13:56 +08:00
shuai 0529c10c54 fix: oauth asjustment 2023-01-13 15:11:26 +08:00
shuai 3285d54ee9 fix: remove debugger 2023-01-13 12:27:43 +08:00
shuai 479892b156 feat: add oauth plugin 2023-01-13 11:38:01 +08:00
LinkinStar 3199693d44 feat(plugin): add update plugin status API 2023-01-11 10:02:45 +08:00
shuai a88b476803 Merge branch 'feat/ui-1.1.0' of git.backyard.segmentfault.com:opensource/answer into feat/ui-1.1.0 2023-01-10 16:49:56 +08:00
shuai 0a1f1b1372 chore: add oauth pages 2023-01-10 16:49:50 +08:00
LinkinStar 7331b49623 feat(plugin): update plugin interface 2023-01-10 16:43:42 +08:00
robin c08051ec27 feat(ui): add border and responsive on table 2023-01-10 15:35:21 +08:00
LinkinStar 8575bbf8c8 feat(plugin): external user login add access token 2023-01-10 14:24:32 +08:00
LinkinStar fe6a53fb62 fix(plugin): connector use site url to redirect 2023-01-10 12:20:14 +08:00
robin 6a3e6da237 refactor(ui): add default option 2023-01-10 11:26:54 +08:00
robin d02d086d45 refactor: update en_US.yaml 2023-01-10 11:22:59 +08:00
robin d3a803d8a6 refactor: add defaultValue to the form 2023-01-09 18:46:41 +08:00
LinkinStar ec01c3e047 feat(plugin): remove external user login authentication route 2023-01-09 18:43:52 +08:00
LinkinStar 06db2bd091 feat(plugin): The third-party login send binding email 2023-01-09 16:54:20 +08:00
LinkinStar 1db4f5586b fix(plugin): add external login binding send email interface 2023-01-06 17:22:09 +08:00
LinkinStar 01c02ae2b7 fix(plugin): add user external login repo 2023-01-06 14:48:41 +08:00
LinkinStar 81d786ff66 feat(plugin): add user external login service 2023-01-06 14:34:53 +08:00
LinkinStar 1664eb7d16 feat(plugin): google connector 2023-01-04 17:43:16 +08:00
LinkinStar 154fa84bbb feat(plugin): github connector 2023-01-03 18:31:32 +08:00
249 changed files with 12201 additions and 2234 deletions
+8 -3
View File
@@ -4,6 +4,7 @@ LABEL maintainer="mingcheng<mc@sf.com>"
COPY . /answer
WORKDIR /answer
RUN node -v
RUN make install-ui-packages ui && mv ui/build /tmp
# stage2 build the main binary within static resource
@@ -11,12 +12,14 @@ FROM golang:1.19-alpine AS golang-builder
LABEL maintainer="aichy@sf.com"
ARG GOPROXY
# ENV GOPROXY ${GOPROXY:-direct}
ENV GOPROXY=https://goproxy.io,direct
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/answerdev/answer
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ENV ANSWER_MODULE ${BUILD_DIR}
ARG TAGS="sqlite sqlite_unlock_notify"
ENV TAGS "bindata timetzdata $TAGS"
@@ -25,9 +28,11 @@ ARG CGO_EXTRA_CFLAGS
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN apk --no-cache add build-base git \
&& make clean build \
&& cp answer /usr/bin/answer
RUN apk --no-cache add build-base git bash \
&& make clean build
RUN chmod 755 answer
RUN ["/bin/bash","-c","script/build_plugin.sh"]
RUN cp answer /usr/bin/answer
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
+1
View File
@@ -26,5 +26,6 @@ tmp
vendor/
/answer-data/
/answer
/new_answer
dist/
+8 -4
View File
@@ -13,12 +13,14 @@ FROM golang:1.19-alpine AS golang-builder
LABEL maintainer="aichy@sf.com"
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}
# ENV GOPROXY ${GOPROXY:-direct}
ENV GOPROXY=https://goproxy.io,direct
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/answerdev/answer
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ENV ANSWER_MODULE ${BUILD_DIR}
ARG TAGS="sqlite sqlite_unlock_notify"
ENV TAGS "bindata timetzdata $TAGS"
@@ -27,9 +29,11 @@ ARG CGO_EXTRA_CFLAGS
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN apk --no-cache add build-base git \
&& make clean build \
&& cp answer /usr/bin/answer
RUN apk --no-cache add build-base git bash \
&& make clean build
RUN chmod 755 answer
RUN ["/bin/bash","-c","script/build_plugin.sh"]
RUN cp answer /usr/bin/answer
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
+2 -2
View File
@@ -1,13 +1,13 @@
.PHONY: build clean ui
VERSION=1.0.9
VERSION=1.1.0
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
GO_ENV=CGO_ENABLED=0 GO111MODULE=on
Revision=$(shell git rev-parse --short HEAD)
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Revision=$(Revision)' -X 'main.Time=`date`' -extldflags -static"
GO_FLAGS=-ldflags="-X github.com/answerdev/answer/cmd.Version=$(VERSION) -X 'github.com/answerdev/answer/cmd.Revision=$(Revision)' -X 'github.com/answerdev/answer/cmd.Time=`date +%s`' -extldflags -static"
GO=$(GO_ENV) $(shell which go)
build: generate
+2 -62
View File
@@ -1,72 +1,12 @@
package main
import (
"os"
"time"
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/contrib/log/zap"
"github.com/segmentfault/pacman/contrib/server/http"
"github.com/segmentfault/pacman/log"
)
// go build -ldflags "-X main.Version=x.y.z"
var (
// Name is the name of the project
Name = "answer"
// Version is the version of the project
Version = "0.0.0"
// Revision is the git short commit revision number
Revision = ""
// Time is the build time of the project
Time = ""
// log level
logLevel = os.Getenv("LOG_LEVEL")
// log path
logPath = os.Getenv("LOG_PATH")
answercmd "github.com/answerdev/answer/cmd"
)
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
func main() {
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
Execute()
}
func runApp() {
c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil {
panic(err)
}
conf.GetPathIgnoreList()
app, cleanup, err := initApplication(
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
if err != nil {
panic(err)
}
constant.Version = Version
constant.Revision = Revision
schema.AppStartTime = time.Now()
defer cleanup()
if err := app.Run(); err != nil {
panic(err)
}
}
func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {
manager.Run()
return pacman.NewApp(
pacman.WithName(Name),
pacman.WithVersion(Version),
pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)),
)
answercmd.Main()
}
+47 -3
View File
@@ -1,13 +1,15 @@
package main
package answercmd
import (
"fmt"
"os"
"strings"
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/install"
"github.com/answerdev/answer/internal/migrations"
"github.com/answerdev/answer/plugin"
"github.com/spf13/cobra"
)
@@ -16,6 +18,10 @@ var (
dataDirPath string
// dumpDataPath dump data path
dumpDataPath string
// plugins needed to build in answer application
buildWithPlugins []string
// build output path
buildOutput string
)
func init() {
@@ -25,7 +31,11 @@ func init() {
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd} {
buildCmd.Flags().StringSliceVarP(&buildWithPlugins, "with", "w", []string{}, "plugins needed to build")
buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path")
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd} {
rootCmd.AddCommand(cmd)
}
}
@@ -160,10 +170,44 @@ To run answer, use:
fmt.Println("check environment all done")
},
}
// buildCmd used to build another answer with plugins
buildCmd = &cobra.Command{
Use: "build",
Short: "used to build answer with plugins",
Long: `Build a new Answer with plugins that you need`,
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("try to build a new answer with plugins:\n%s\n", strings.Join(buildWithPlugins, "\n"))
err := cli.BuildNewAnswer(buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{
Version: Version,
Revision: Revision,
Time: Time,
})
if err != nil {
fmt.Printf("build failed %v", err)
} else {
fmt.Printf("build new answer successfully %s\n", buildOutput)
}
},
}
// pluginCmd prints all plugins packed in the binary
pluginCmd = &cobra.Command{
Use: "plugin",
Short: "prints all plugins packed in the binary",
Long: `prints all plugins packed in the binary`,
Run: func(_ *cobra.Command, _ []string) {
_ = plugin.CallBase(func(base plugin.Base) error {
info := base.Info()
fmt.Printf("%s[%s] made by %s\n", info.SlugName, info.Version, info.Author)
return nil
})
},
}
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
// This is called by main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
+75
View File
@@ -0,0 +1,75 @@
package answercmd
import (
"fmt"
"os"
"time"
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/contrib/log/zap"
"github.com/segmentfault/pacman/contrib/server/http"
"github.com/segmentfault/pacman/log"
)
// go build -ldflags "-X github.com/answerdev/answer/cmd.Version=x.y.z"
var (
// Name is the name of the project
Name = "answer"
// Version is the version of the project
Version = "0.0.0"
// Revision is the git short commit revision number
Revision = "-"
// Time is the build time of the project
Time = "-"
// log level
logLevel = os.Getenv("LOG_LEVEL")
// log path
logPath = os.Getenv("LOG_PATH")
)
// Main
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
func Main() {
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
Execute()
}
func runApp() {
c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil {
panic(err)
}
conf.GetPathIgnoreList()
app, cleanup, err := initApplication(
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
if err != nil {
panic(err)
}
constant.Version = Version
constant.Revision = Revision
schema.AppStartTime = time.Now()
fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision)
defer cleanup()
if err := app.Run(); err != nil {
panic(err)
}
}
func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {
manager.Run()
return pacman.NewApp(
pacman.WithName(Name),
pacman.WithVersion(Version),
pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)),
)
}
+1 -1
View File
@@ -3,7 +3,7 @@
// The build tag makes sure the stub is not built in the final build.
package main
package answercmd
import (
"github.com/answerdev/answer/internal/base/conf"
+19 -6
View File
@@ -4,7 +4,7 @@
//go:build !wireinject
// +build !wireinject
package main
package answercmd
import (
"github.com/answerdev/answer/internal/base/conf"
@@ -28,6 +28,7 @@ import (
"github.com/answerdev/answer/internal/repo/export"
"github.com/answerdev/answer/internal/repo/meta"
"github.com/answerdev/answer/internal/repo/notification"
"github.com/answerdev/answer/internal/repo/plugin_config"
"github.com/answerdev/answer/internal/repo/question"
"github.com/answerdev/answer/internal/repo/rank"
"github.com/answerdev/answer/internal/repo/reason"
@@ -40,6 +41,7 @@ import (
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/repo/user_external_login"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
@@ -57,6 +59,7 @@ import (
notification2 "github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/plugin_common"
"github.com/answerdev/answer/internal/service/question_common"
rank2 "github.com/answerdev/answer/internal/service/rank"
reason2 "github.com/answerdev/answer/internal/service/reason"
@@ -74,6 +77,7 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/answerdev/answer/internal/service/user_common"
user_external_login2 "github.com/answerdev/answer/internal/service/user_external_login"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/log"
)
@@ -117,8 +121,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
roleRepo := role.NewRoleRepo(dataData)
roleService := role2.NewRoleService(roleRepo)
userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService)
userCommon := usercommon.NewUserCommon(userRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon)
userCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService)
userExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData)
userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
@@ -187,7 +193,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_admin.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService)
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configRepo)
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
@@ -202,7 +208,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
activityController := controller.NewActivityController(activityCommon, activityService)
roleController := controller_admin.NewRoleController(roleService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController)
pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData)
pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, configRepo)
pluginController := controller_admin.NewPluginController(pluginCommonService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
@@ -210,7 +219,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService)
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter)
connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService)
pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter, pluginAPIRouter)
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService)
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
return application, func() {
+3
View File
@@ -1,5 +1,6 @@
# url path reserves the keywords list
users:
- unsubscribe
- settings
- login
- register
@@ -9,5 +10,7 @@ users:
- account-activation
- confirm-new-email
- account-suspended
- confirm-email
- auth-landing
questions:
- ask
+945 -40
View File
File diff suppressed because it is too large Load Diff
+945 -40
View File
File diff suppressed because it is too large Load Diff
+595 -32
View File
@@ -1,4 +1,13 @@
definitions:
constant.Privilege:
properties:
key:
type: string
label:
type: string
value:
type: integer
type: object
handler.RespBody:
properties:
code:
@@ -305,6 +314,104 @@ definitions:
switch:
type: boolean
type: object
schema.ConfigField:
properties:
description:
type: string
name:
type: string
options:
items:
$ref: '#/definitions/schema.ConfigFieldOption'
type: array
required:
type: boolean
title:
type: string
type:
type: string
ui_options:
$ref: '#/definitions/schema.ConfigFieldUIOptions'
value: {}
type: object
schema.ConfigFieldOption:
properties:
label:
type: string
value:
type: string
type: object
schema.ConfigFieldUIOptions:
properties:
action:
$ref: '#/definitions/schema.UIOptionAction'
input_type:
type: string
label:
type: string
placeholder:
type: string
rows:
type: string
text:
type: string
variant:
type: string
type: object
schema.ConnectorInfoResp:
properties:
icon:
type: string
link:
type: string
name:
type: string
type: object
schema.ConnectorUserInfoResp:
properties:
binding:
type: boolean
external_id:
type: string
icon:
type: string
link:
type: string
name:
type: string
type: object
schema.ExternalLoginBindingUserSendEmailReq:
properties:
binding_key:
maxLength: 100
type: string
email:
maxLength: 512
type: string
must:
description: |-
If must is true, whatever email if exists, try to bind user.
If must is false, when email exist, will only be prompted with a warning.
type: boolean
required:
- binding_key
- email
type: object
schema.ExternalLoginBindingUserSendEmailResp:
properties:
access_token:
type: string
email_exist_and_must_be_confirmed:
type: boolean
type: object
schema.ExternalLoginUnbindingReq:
properties:
external_id:
maxLength: 128
type: string
required:
- external_id
type: object
schema.FollowReq:
properties:
is_cancel:
@@ -507,6 +614,47 @@ definitions:
info:
$ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp'
type: object
schema.GetPluginConfigResp:
properties:
config_fields:
items:
$ref: '#/definitions/schema.ConfigField'
type: array
description:
type: string
name:
type: string
slug_name:
type: string
version:
type: string
type: object
schema.GetPluginListResp:
properties:
description:
type: string
enabled:
type: boolean
have_config:
type: boolean
link:
type: string
name:
type: string
slug_name:
type: string
version:
type: string
type: object
schema.GetPrivilegesConfigResp:
properties:
options:
items:
$ref: '#/definitions/schema.PrivilegeOption'
type: array
selected_level:
type: integer
type: object
schema.GetRankPersonalWithPageResp:
properties:
answer_id:
@@ -820,6 +968,9 @@ definitions:
follow_count:
description: follow count
type: integer
have_password:
description: user have password
type: boolean
id:
description: user id
type: string
@@ -894,6 +1045,8 @@ definitions:
follow_count:
description: follow count
type: integer
have_password:
type: boolean
id:
description: user id
type: string
@@ -972,6 +1125,13 @@ definitions:
description: vote type
type: string
type: object
schema.LoadingAction:
properties:
state:
type: string
text:
type: string
type: object
schema.NotificationClearIDRequest:
properties:
id:
@@ -983,6 +1143,13 @@ definitions:
description: inbox achievement
type: string
type: object
schema.OnCompleteAction:
properties:
refresh_form_config:
type: boolean
toast_return_message:
type: boolean
type: object
schema.OperationQuestionReq:
properties:
id:
@@ -1007,6 +1174,17 @@ definitions:
content:
type: string
type: object
schema.PrivilegeOption:
properties:
level:
type: integer
level_desc:
type: string
privileges:
items:
$ref: '#/definitions/constant.Privilege'
type: array
type: object
schema.QuestionAdd:
properties:
content:
@@ -1058,6 +1236,9 @@ definitions:
type: object
schema.QuestionPageReq:
properties:
inDays:
minimum: 1
type: integer
orderCond:
enum:
- newest
@@ -1325,6 +1506,9 @@ definitions:
custom_header:
maxLength: 65536
type: string
custom_sidebar:
maxLength: 65536
type: string
type: object
schema.SiteCustomCssHTMLResp:
properties:
@@ -1340,6 +1524,9 @@ definitions:
custom_header:
maxLength: 65536
type: string
custom_sidebar:
maxLength: 65536
type: string
type: object
schema.SiteGeneralReq:
properties:
@@ -1401,6 +1588,8 @@ definitions:
type: string
site_seo:
$ref: '#/definitions/schema.SiteSeoReq'
site_users:
$ref: '#/definitions/schema.SiteUsersResp'
theme:
$ref: '#/definitions/schema.SiteThemeResp'
version:
@@ -1408,11 +1597,6 @@ definitions:
type: object
schema.SiteInterfaceReq:
properties:
default_avatar:
enum:
- system
- gravatar
type: string
language:
maxLength: 128
type: string
@@ -1420,17 +1604,11 @@ definitions:
maxLength: 128
type: string
required:
- default_avatar
- language
- time_zone
type: object
schema.SiteInterfaceResp:
properties:
default_avatar:
enum:
- system
- gravatar
type: string
language:
maxLength: 128
type: string
@@ -1438,7 +1616,6 @@ definitions:
maxLength: 128
type: string
required:
- default_avatar
- language
- time_zone
type: object
@@ -1466,6 +1643,12 @@ definitions:
type: object
schema.SiteLoginReq:
properties:
allow_email_domains:
items:
type: string
type: array
allow_email_registrations:
type: boolean
allow_new_registrations:
type: boolean
login_required:
@@ -1473,6 +1656,12 @@ definitions:
type: object
schema.SiteLoginResp:
properties:
allow_email_domains:
items:
type: string
type: array
allow_email_registrations:
type: boolean
allow_new_registrations:
type: boolean
login_required:
@@ -1525,6 +1714,50 @@ definitions:
$ref: '#/definitions/schema.ThemeOption'
type: array
type: object
schema.SiteUsersReq:
properties:
allow_update_avatar:
type: boolean
allow_update_bio:
type: boolean
allow_update_display_name:
type: boolean
allow_update_location:
type: boolean
allow_update_username:
type: boolean
allow_update_website:
type: boolean
default_avatar:
enum:
- system
- gravatar
type: string
required:
- default_avatar
type: object
schema.SiteUsersResp:
properties:
allow_update_avatar:
type: boolean
allow_update_bio:
type: boolean
allow_update_display_name:
type: boolean
allow_update_location:
type: boolean
allow_update_username:
type: boolean
allow_update_website:
type: boolean
default_avatar:
enum:
- system
- gravatar
type: string
required:
- default_avatar
type: object
schema.SiteWriteReq:
properties:
recommend_tags:
@@ -1603,6 +1836,17 @@ definitions:
value:
type: string
type: object
schema.UIOptionAction:
properties:
loading:
$ref: '#/definitions/schema.LoadingAction'
method:
type: string
on_complete:
$ref: '#/definitions/schema.OnCompleteAction'
url:
type: string
type: object
schema.UnreviewedRevisionInfoInfo:
properties:
content:
@@ -1665,8 +1909,36 @@ definitions:
description: website
maxLength: 500
type: string
type: object
schema.UpdatePluginConfigReq:
properties:
config_fields:
additionalProperties: {}
type: object
plugin_slug_name:
maxLength: 100
type: string
required:
- display_name
- plugin_slug_name
type: object
schema.UpdatePluginStatusReq:
properties:
enabled:
type: boolean
plugin_slug_name:
maxLength: 100
type: string
required:
- plugin_slug_name
type: object
schema.UpdatePrivilegesConfigReq:
properties:
level:
maximum: 3
minimum: 1
type: integer
required:
- level
type: object
schema.UpdateSMTPConfigReq:
properties:
@@ -1824,6 +2096,10 @@ definitions:
e_mail:
maxLength: 500
type: string
pass:
maxLength: 32
minLength: 8
type: string
required:
- e_mail
type: object
@@ -1856,14 +2132,18 @@ definitions:
- e_mail
- pass
type: object
schema.UserModifyPassWordRequest:
schema.UserModifyPasswordReq:
properties:
old_pass:
description: old password
maxLength: 32
minLength: 8
type: string
pass:
description: password
maxLength: 32
minLength: 8
type: string
required:
- pass
type: object
schema.UserNoticeSetRequest:
properties:
@@ -2104,6 +2384,112 @@ paths:
summary: Get language options
tags:
- Lang
/answer/admin/api/plugin/config:
get:
description: get plugin config
parameters:
- description: plugin_slug_name
in: query
name: plugin_slug_name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetPluginConfigResp'
type: object
security:
- ApiKeyAuth: []
summary: get plugin config
tags:
- AdminPlugin
put:
consumes:
- application/json
description: update plugin config
parameters:
- description: UpdatePluginConfigReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdatePluginConfigReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update plugin config
tags:
- AdminPlugin
/answer/admin/api/plugin/status:
put:
consumes:
- application/json
description: update plugin status
parameters:
- description: UpdatePluginStatusReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdatePluginStatusReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update plugin status
tags:
- AdminPlugin
/answer/admin/api/plugins:
get:
consumes:
- application/json
description: get plugin list
parameters:
- description: 'status: active/inactive'
in: query
name: status
type: string
- description: have config
in: query
name: have_config
type: boolean
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.GetPluginListResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get plugin list
tags:
- AdminPlugin
/answer/admin/api/question/page:
get:
consumes:
@@ -2294,6 +2680,47 @@ paths:
summary: get role list
tags:
- admin
/answer/admin/api/setting/privileges:
get:
description: GetPrivilegesConfig get privileges config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetPrivilegesConfigResp'
type: object
security:
- ApiKeyAuth: []
summary: GetPrivilegesConfig get privileges config
tags:
- admin
put:
description: update privileges config
parameters:
- description: config
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdatePrivilegesConfigReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update privileges config
tags:
- admin
/answer/admin/api/setting/smtp:
get:
description: GetSMTPConfig get smtp config
@@ -2663,6 +3090,47 @@ paths:
summary: update site custom css html config
tags:
- admin
/answer/admin/api/siteinfo/users:
get:
description: get site user config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteUsersResp'
type: object
security:
- ApiKeyAuth: []
summary: get site user config
tags:
- admin
put:
description: update site info config about users
parameters:
- description: users info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteUsersReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site info config about users
tags:
- admin
/answer/admin/api/siteinfo/write:
get:
description: get site interface
@@ -3263,6 +3731,101 @@ paths:
summary: get comment page
tags:
- Comment
/answer/api/v1/connector/binding/email:
post:
consumes:
- application/json
description: external login binding user send email
parameters:
- description: external login binding user send email
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailResp'
type: object
summary: external login binding user send email
tags:
- PluginConnector
/answer/api/v1/connector/info:
get:
description: get all enabled connectors
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.ConnectorInfoResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get all enabled connectors
tags:
- PluginConnector
/answer/api/v1/connector/user/info:
get:
description: get all connectors info about user
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.ConnectorUserInfoResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get all connectors info about user
tags:
- PluginConnector
/answer/api/v1/connector/user/unbinding:
delete:
consumes:
- application/json
description: unbind external user login
parameters:
- description: ExternalLoginUnbindingReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.ExternalLoginUnbindingReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: unbind external user login
tags:
- PluginConnector
/answer/api/v1/file:
post:
consumes:
@@ -3509,7 +4072,7 @@ paths:
get:
consumes:
- application/json
description: UserAnswerList
description: list personal answers
parameters:
- default: string
description: username
@@ -3532,9 +4095,9 @@ paths:
required: true
type: string
- default: "20"
description: pagesize
description: page_size
in: query
name: pagesize
name: page_size
required: true
type: string
produces:
@@ -3546,14 +4109,14 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserAnswerList
summary: list personal answers
tags:
- api-answer
- Personal
/answer/api/v1/personal/collection/page:
get:
consumes:
- application/json
description: UserCollectionList
description: list personal collections
parameters:
- default: "0"
description: page
@@ -3562,9 +4125,9 @@ paths:
required: true
type: string
- default: "20"
description: pagesize
description: page_size
in: query
name: pagesize
name: page_size
required: true
type: string
produces:
@@ -3576,7 +4139,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserCollectionList
summary: list personal collections
tags:
- Collection
/answer/api/v1/personal/comment/page:
@@ -4893,12 +5456,12 @@ paths:
- application/json
description: UserModifyPassWord
parameters:
- description: UserModifyPassWordRequest
- description: UserModifyPasswordReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UserModifyPassWordRequest'
$ref: '#/definitions/schema.UserModifyPasswordReq'
produces:
- application/json
responses:
@@ -5210,7 +5773,7 @@ paths:
get:
consumes:
- application/json
description: UserList
description: list personal questions
parameters:
- default: string
description: username
@@ -5233,9 +5796,9 @@ paths:
required: true
type: string
- default: "20"
description: pagesize
description: page_size
in: query
name: pagesize
name: page_size
required: true
type: string
produces:
@@ -5247,9 +5810,9 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserList
summary: list personal questions
tags:
- Question
- Personal
/robots.txt:
get:
description: get site robots information
+4
View File
@@ -4,6 +4,7 @@ go 1.18
require (
github.com/Chain-Zhang/pinyin v0.1.3
github.com/Masterminds/semver/v3 v3.1.1
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/bwmarrin/snowflake v0.3.0
@@ -80,6 +81,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/geo v0.0.0-20190812012225-f41920e961ce // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
@@ -146,3 +148,5 @@ require (
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1
+5 -3
View File
@@ -52,6 +52,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU=
github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -64,6 +65,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/aichy126/uint128 v1.1.1 h1:xH1bCWDzq7Ebm4lpXCeIiWco0VWi7UmiKkvTQSWBmb0=
github.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -287,7 +290,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -1202,8 +1206,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+177 -12
View File
@@ -12,6 +12,8 @@ backend:
other: Unauthorized.
database_error:
other: Data server error.
forbidden_error:
other: Forbidden.
action:
report:
other: Flag
@@ -23,6 +25,8 @@ backend:
other: Close
reopen:
other: Reopen
forbidden_error:
other: Forbidden.
pin:
other: Pin
hide:
@@ -46,6 +50,58 @@ backend:
other: Have the full power to access the site.
moderator:
other: Has access to all posts except admin settings.
privilege:
level_1:
description:
other: Level 1 (less reputation required for private team, group)
level_2:
description:
other: Level 2 (low reputation required for startup community)
level_3:
description:
other: Level 3 (high reputation required for mature community)
rank_question_add_label:
other: Ask question
rank_answer_add_label:
other: Write answer
rank_comment_add_label:
other: Write comment
rank_report_add_label:
other: Flag
rank_comment_vote_up_label:
other: Upvote comment
rank_link_url_limit_label:
other: Post more than 2 links at a time
rank_question_vote_up_label:
other: Upvote question
rank_answer_vote_up_label:
other: Upvote answer
rank_question_vote_down_label:
other: Downvote question
rank_answer_vote_down_label:
other: Downvote answer
rank_tag_add_label:
other: Create new tag
rank_tag_edit_label:
other: Edit tag description (need to review)
rank_question_edit_label:
other: Edit other's question (need to review)
rank_answer_edit_label:
other: Edit other's answer (need to review)
rank_question_edit_without_review_label:
other: Edit other's question without review
rank_answer_edit_without_review_label:
other: Edit other's answer without review
rank_question_audit_label:
other: Review question edits
rank_answer_audit_label:
other: Review answer edits
rank_tag_audit_label:
other: Review tag edits
rank_tag_edit_without_review_label:
other: Edit tag description without review
rank_tag_synonym_label:
other: Manage tag synonyms
email:
other: Email
password:
@@ -83,6 +139,8 @@ backend:
other: Email should be verified.
verify_url_expired:
other: Email verified URL has expired, please resend the email.
illegal_email_domain_error:
other: Email is not allowed from that email domain. Please use another one.
lang:
not_found:
other: Language file not found.
@@ -155,6 +213,8 @@ backend:
no_permission:
other: No permission to Revision.
user:
external_login_unbinding_forbidden:
other: Please set a login password for your account before you remove this login.
email_or_password_wrong:
other:
other: Email and password do not match.
@@ -172,6 +232,10 @@ backend:
other: You cannot modify your role.
not_allowed_registration:
other: Currently the site is not open for registration
access_denied:
other: Access denied
page_access_denied:
other: You do not have access to this page.
config:
read_config_failed:
other: Read config failed
@@ -277,6 +341,16 @@ backend:
other: Your answer has been deleted
your_comment_was_deleted:
other: Your comment has been deleted
up_voted_question:
other: upvoted question
down_voted_question:
other: downvoted question
up_voted_answer:
other: upvoted answer
down_voted_answer:
other: downvoted answer
up_voted_comment:
other: upvoted comment
# The following fields are used for interface presentation(Front-end)
ui:
@@ -322,6 +396,7 @@ ui:
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
oauth_callback: Processing
http_404: HTTP Error 404
http_50X: HTTP Error 500
http_403: HTTP Error 403
@@ -331,11 +406,13 @@ ui:
achievement: Achievements
all_read: Mark all as read
show_more: Show more
someone: Someone
suspended:
title: Your Account has been Suspended
until_time: "Your account was suspended until {{ time }}."
forever: This user was suspended forever.
end: You don't meet a community guideline.
contact_us: Contact us
editor:
blockquote:
text: Blockquote
@@ -547,7 +624,7 @@ ui:
tip_answer: >-
Use comments to reply to other users or notify them of changes. If you are
adding new information, edit your post instead of commenting.
tip_vote: It adds something useful to the post
tip_vote: It adds something useful to the post
edit_answer:
title: Edit Answer
default_reason: Edit answer
@@ -658,7 +735,6 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
@@ -669,6 +745,7 @@ ui:
msg:
empty: Name cannot be empty.
range: Name up to 30 characters.
character: 'Must use the character set "a-z", "0-9", " - . _"'
email:
label: Email
msg:
@@ -689,7 +766,6 @@ ui:
msg:
empty: Email cannot be empty.
change_email:
page_title: Welcome to {{site_name}}
btn_cancel: Cancel
btn_update: Update email address
send_success: >-
@@ -699,6 +775,17 @@ ui:
label: New Email
msg:
empty: Email cannot be empty.
oauth_bind_email:
subtitle: Add a recovery email to your account.
btn_update: Update email address
email:
label: Email
msg:
empty: Email cannot be empty.
modal_title: Email already existes.
modal_content: This email address already registered. Are you sure you want to connect to the existing account?
modal_cancel: Change email
modal_confirm: Connect to the existing account
password_reset:
page_title: Password Reset
btn_name: Reset my password
@@ -719,6 +806,7 @@ ui:
label: Confirm New Password
settings:
page_title: Settings
goto_modify: Go to Modify
nav:
profile: Profile
notification: Notifications
@@ -768,8 +856,11 @@ ui:
We've sent an email to that address. Please follow the confirmation
instructions.
email:
label: Email
msg: Email cannot be empty.
label: New Email
msg: New Email cannot be empty.
pass:
label: Current Password
msg: Password cannot be empty.
password_title: Password
current_pass:
label: Current Password
@@ -786,6 +877,13 @@ ui:
lang:
label: Interface Language
text: User interface language. It will change when you refresh the page.
my_logins:
title: My Logins
label: Log in or sign up on this site using these accounts.
modal_title: Remove Login
modal_content: Are you sure you want to remove this login from your account?
modal_confirm_btn: Remove
remove_success: Removed successfully
toast:
update: update success
update_password: Password changed successfully.
@@ -809,8 +907,8 @@ ui:
closed_in: Closed in
show_exist: Show existing question.
useful: Useful
question_useful: It is useful and clear
question_un_useful: It is unclear or not useful
question_useful: It is useful and clear
question_un_useful: It is unclear or not useful
answer_useful: It is useful
answer_un_useful: It is not useful
answers:
@@ -873,6 +971,10 @@ ui:
skip: Skip
discard_draft: Discard draft
pinned: Pinned
all: All
question: Question
answer: Answer
comment: Comment
search:
title: Search Results
keywords: Keywords
@@ -907,7 +1009,6 @@ ui:
modal_confirm:
title: Error...
account_result:
page_title: Welcome to {{site_name}}
success: Your new account is confirmed; you will be redirected to the home page.
link: Continue to homepage
invalid: >-
@@ -1034,6 +1135,7 @@ ui:
admin_name:
label: Name
msg: Name cannot be empty.
character: 'Must use the character set "a-z", "0-9", " - . _"'
admin_password:
label: Password
text: >-
@@ -1095,8 +1197,20 @@ ui:
seo: SEO
customize: Customize
themes: Themes
css-html: CSS/HTML
css_html: CSS/HTML
login: Login
privileges: Privileges
plugins: Plugins
installed_plugins: Installed Plugins
website_welcome: Welcome to {{site_name}}
plugins:
login: Login
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
oauth:
connect: Connect with {{ auth_name }}
remove: Remove {{ auth_name }}
admin:
admin_header:
title: Admin
@@ -1139,6 +1253,7 @@ ui:
pending: Pending
completed: Completed
flagged: Flagged
flagged_type: Flagged {{ type }}
created: Created
action: Action
review: Review
@@ -1289,9 +1404,6 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:
@@ -1401,16 +1513,67 @@ ui:
footer:
label: Footer
text: This will insert before </html>.
sidebar:
label: Sidebar
text: This will insert in sidebar.
login:
page_title: Login
membership:
title: Membership
label: Allow new registrations
text: Turn off to prevent anyone from creating a new account.
email_registration:
title: Email registration
label: Allow email registration
text: Turn off to prevent anyone creating new account through email.
allowed_email_domains:
title: Allowed email domains
text: Email domains that users must register accounts with. One domain per line. Ignored when empty.
private:
title: Private
label: Login required
text: Only logged in users can access this community.
installed_plugins:
title: Installed Plugins
filter:
all: All
active: Active
inactive: Inactive
outdated: Outdated
plugins:
label: Plugins
text: Select an existing plugin.
name: Name
version: Version
status: Status
action: Action
deactivate: Deactivate
activate: Activate
settings: Settings
settings_users:
title: Users
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
profile_editable:
title: Profile Editable
allow_update_display_name:
label: Allow users to change their display name
allow_update_username:
label: Allow users to change their username
allow_update_avatar:
label: Allow users to change their profile image
allow_update_bio:
label: Allow users to change their about me
allow_update_website:
label: Allow users to change their website
allow_update_location:
label: Allow users to change their location
privilege:
title: Privileges
level:
label: Reputation required level
text: Choose the reputation required for the privileges
form:
optional: (optional)
@@ -1418,6 +1581,8 @@ ui:
invalid: is invalid
btn_submit: Save
not_found_props: "Required property {{ key }} not found."
select: Select
page_review:
review: Review
proposed: proposed
+126
View File
@@ -45,6 +45,58 @@ backend:
other: 拥有管理网站的全部权限。
moderator:
other: 拥有访问除管理员设置以外的所有权限。
privilege:
level_1:
description:
other: 等级1(创业社区所需的声望最低)
level_2:
description:
other: 等级2(创业社区所需的声望较低)
level_3:
description:
other: 等级3(成熟社区所需的声望较高)
rank_question_add_label:
other: 提问
rank_answer_add_label:
other: 回答问题
rank_comment_add_label:
other: 发表评论
rank_report_add_label:
other: 举报
rank_comment_vote_up_label:
other: 评论点赞
rank_link_url_limit_label:
other: 一次发布超过两个链接
rank_question_vote_up_label:
other: 问题点赞
rank_answer_vote_up_label:
other: 答案点赞
rank_question_vote_down_label:
other: 问题点踩
rank_answer_vote_down_label:
other: 答案点踩
rank_tag_add_label:
other: 创建新标签
rank_tag_edit_label:
other: 编辑标签描述(需要审核)
rank_question_edit_label:
other: 编辑他人提问(需要审核)
rank_answer_edit_label:
other: 编辑他人回答(需要审核)
rank_question_edit_without_review_label:
other: 编辑他人提问(无需审核)
rank_answer_edit_without_review_label:
other: 编辑他人回答(无需审核)
rank_question_audit_label:
other: 审核问题编辑
rank_answer_audit_label:
other: 审核答案编辑
rank_tag_audit_label:
other: 审核标签编辑
rank_tag_edit_without_review_label:
other: 编辑标签描述(无需审核)
rank_tag_synonym_label:
other: 管理标签同义词
email:
other: 邮箱
password:
@@ -82,6 +134,8 @@ backend:
other: 邮箱需要验证。
verify_url_expired:
other: 邮箱验证的网址已过期,请重新发送邮件。
illegal_email_domain_error:
other: 该域名的邮箱无法使用。请尝试更换其他邮箱。
lang:
not_found:
other: 语言未找到
@@ -171,6 +225,10 @@ backend:
other: 您不能修改自己的角色。
not_allowed_registration:
other: 目前该站点未开放注册
access_denied:
other: 访问被拒绝
page_access_denied:
other: 你没有权限进入这个页面。
config:
read_config_failed:
other: 读取配置失败
@@ -271,6 +329,16 @@ backend:
other: 你的答案已被删除
your_comment_was_deleted:
other: 你的评论已被删除
up_voted_question:
other: 赞了问题
down_voted_question:
other: 踩了问题
up_voted_answer:
other: 赞了答案
down_voted_answer:
other: 踩了答案
up_voted_comment:
other: 赞了评论
#The following fields are used for interface presentation(Front-end)
ui:
how_to_format:
@@ -316,6 +384,7 @@ ui:
achievement: 成就
all_read: 全部标记为已读
show_more: 显示更多
someone: 有人
suspended:
title: 账号已封禁
until_time: "你的账号被封禁至{{ time }}。"
@@ -690,6 +759,7 @@ ui:
label: 确认新密码
settings:
page_title: 设置
goto_modify: 前往修改
nav:
profile: 我的资料
notification: 通知
@@ -836,6 +906,10 @@ ui:
skip: 略过
discard_draft: 丢弃草稿
pinned: 已置顶
all: 所有
question: 问题
answer: 回答
comment: 评论
search:
title: 搜索结果
keywords: 关键词
@@ -1053,6 +1127,16 @@ ui:
themes: 主题
css-html: CSS/HTML
login: 登录
plugins: 插件
installed_plugins: 插件列表
website_welcome: 欢迎来到 {{site_name}}
plugins:
login: 登录
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
oauth:
connect: 连接到 {{ auth_name }}
remove: 解绑 {{ auth_name }}
admin:
admin_header:
title: 后台管理
@@ -1095,6 +1179,7 @@ ui:
pending: 等待处理
completed: 已完成
flagged: 被举报内容
flagged_type: 被举报的{{ type }}
created: 创建于
action: 操作
review: 审查
@@ -1365,6 +1450,47 @@ ui:
title: 非公开的
label: 需要登录
text: 只有登录用户才能访问这个社区。
installed_plugins:
title: 插件列表
filter:
all: 全部
active: 启用
inactive: 未启用
outdated: 已过期
plugins:
label: 插件
text: 选择一个插件
name: 插件名称
version: 插件版本
status: 状态
action: 操作
deactivate: 停用
activate: 启用
settings: 设置
settings_users:
title: 用户
avatar:
label: 默认头像
text: 未设置自定义头像的用户所展示的头像。
profile_editable:
title: 可编辑的个人资料
allow_update_display_name:
label: 允许用户更改显示名称
allow_update_username:
label: 允许用户更改用户名
allow_update_avatar:
label: 允许用户更改头像
allow_update_bio:
label: 允许用户更改自我介绍
allow_update_website:
label: 允许用户更改个人网站
allow_update_location:
label: 允许用户更改所在地
privilege:
title: 声望权限
level:
label: 所需声望等级
text: 选择所需的声望等级以获取权限
form:
optional: (选填)
empty: 不能为空
+5
View File
@@ -0,0 +1,5 @@
package constant
const (
PluginStatus = "plugin.status"
)
+8
View File
@@ -0,0 +1,8 @@
package constant
import "time"
const (
ConnectorUserExternalInfoCacheKey = "answer:connector:"
ConnectorUserExternalInfoCacheTime = 10 * time.Minute
)
+2
View File
@@ -66,6 +66,8 @@ const (
SiteTypeLogin = "login"
SiteTypeCustomCssHTML = "css-html"
SiteTypeTheme = "theme"
SiteTypePrivileges = "privileges"
SiteTypeUsers = "users"
)
func ExistInPathIgnore(name string) bool {
+34 -24
View File
@@ -1,28 +1,38 @@
package constant
const (
// UpdateQuestion update question
UpdateQuestion = "notification.action.update_question"
// AnswerTheQuestion answer the question
AnswerTheQuestion = "notification.action.answer_the_question"
// UpdateAnswer update answer
UpdateAnswer = "notification.action.update_answer"
// AcceptAnswer accept answer
AcceptAnswer = "notification.action.accept_answer"
// CommentQuestion comment question
CommentQuestion = "notification.action.comment_question"
// CommentAnswer comment answer
CommentAnswer = "notification.action.comment_answer"
// ReplyToYou reply to you
ReplyToYou = "notification.action.reply_to_you"
// MentionYou mention you
MentionYou = "notification.action.mention_you"
// YourQuestionIsClosed your question is closed
YourQuestionIsClosed = "notification.action.your_question_is_closed"
// YourQuestionWasDeleted your question was deleted
YourQuestionWasDeleted = "notification.action.your_question_was_deleted"
// YourAnswerWasDeleted your answer was deleted
YourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// YourCommentWasDeleted your comment was deleted
YourCommentWasDeleted = "notification.action.your_comment_was_deleted"
// NotificationUpdateQuestion update question
NotificationUpdateQuestion = "notification.action.update_question"
// NotificationAnswerTheQuestion answer the question
NotificationAnswerTheQuestion = "notification.action.answer_the_question"
// NotificationUpVotedTheQuestion up voted the question
NotificationUpVotedTheQuestion = "notification.action.up_voted_question"
// NotificationDownVotedTheQuestion down voted the question
NotificationDownVotedTheQuestion = "notification.action.down_voted_question"
// NotificationUpdateAnswer update answer
NotificationUpdateAnswer = "notification.action.update_answer"
// NotificationAcceptAnswer accept answer
NotificationAcceptAnswer = "notification.action.accept_answer"
// NotificationUpVotedTheAnswer up voted the answer
NotificationUpVotedTheAnswer = "notification.action.up_voted_answer"
// NotificationDownVotedTheAnswer down voted the answer
NotificationDownVotedTheAnswer = "notification.action.down_voted_answer"
// NotificationCommentQuestion comment question
NotificationCommentQuestion = "notification.action.comment_question"
// NotificationCommentAnswer comment answer
NotificationCommentAnswer = "notification.action.comment_answer"
// NotificationUpVotedTheComment up voted the comment
NotificationUpVotedTheComment = "notification.action.up_voted_comment"
// NotificationReplyToYou reply to you
NotificationReplyToYou = "notification.action.reply_to_you"
// NotificationMentionYou mention you
NotificationMentionYou = "notification.action.mention_you"
// NotificationYourQuestionIsClosed your question is closed
NotificationYourQuestionIsClosed = "notification.action.your_question_is_closed"
// NotificationYourQuestionWasDeleted your question was deleted
NotificationYourQuestionWasDeleted = "notification.action.your_question_was_deleted"
// NotificationYourAnswerWasDeleted your answer was deleted
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// NotificationYourCommentWasDeleted your comment was deleted
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
)
+70
View File
@@ -0,0 +1,70 @@
package constant
import "github.com/answerdev/answer/internal/base/reason"
type Privilege struct {
Key string `json:"key"`
Label string `json:"label"`
Value int `json:"value"`
}
const (
RankQuestionAddKey = "rank.question.add"
RankQuestionEditKey = "rank.question.edit"
RankQuestionDeleteKey = "rank.question.delete"
RankQuestionVoteUpKey = "rank.question.vote_up"
RankQuestionVoteDownKey = "rank.question.vote_down"
RankAnswerAddKey = "rank.answer.add"
RankAnswerEditKey = "rank.answer.edit"
RankAnswerDeleteKey = "rank.answer.delete"
RankAnswerAcceptKey = "rank.answer.accept"
RankAnswerVoteUpKey = "rank.answer.vote_up"
RankAnswerVoteDownKey = "rank.answer.vote_down"
RankCommentAddKey = "rank.comment.add"
RankCommentEditKey = "rank.comment.edit"
RankCommentDeleteKey = "rank.comment.delete"
RankReportAddKey = "rank.report.add"
RankTagAddKey = "rank.tag.add"
RankTagEditKey = "rank.tag.edit"
RankTagDeleteKey = "rank.tag.delete"
RankTagSynonymKey = "rank.tag.synonym"
RankLinkUrlLimitKey = "rank.link.url_limit"
RankVoteDetailKey = "rank.vote.detail"
RankCommentVoteUpKey = "rank.comment.vote_up"
RankCommentVoteDownKey = "rank.comment.vote_down"
RankQuestionEditWithoutReviewKey = "rank.question.edit_without_review"
RankAnswerEditWithoutReviewKey = "rank.answer.edit_without_review"
RankTagEditWithoutReviewKey = "rank.tag.edit_without_review"
RankAnswerAuditKey = "rank.answer.audit"
RankQuestionAuditKey = "rank.question.audit"
RankTagAuditKey = "rank.tag.audit"
RankQuestionCloseKey = "rank.question.close"
RankQuestionReopenKey = "rank.question.reopen"
RankTagUseReservedTagKey = "rank.tag.use_reserved_tag"
)
var (
RankAllPrivileges = []*Privilege{
{Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey},
{Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey},
{Label: reason.RankCommentAddLabel, Key: RankCommentAddKey},
{Label: reason.RankReportAddLabel, Key: RankReportAddKey},
{Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey},
{Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey},
{Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey},
{Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},
{Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},
{Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},
{Label: reason.RankTagAddLabel, Key: RankTagAddKey},
{Label: reason.RankTagEditLabel, Key: RankTagEditKey},
{Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey},
{Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey},
{Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey},
{Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey},
{Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey},
{Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey},
{Label: reason.RankTagAuditLabel, Key: RankTagAuditKey},
{Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey},
{Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey},
}
)
+2 -1
View File
@@ -1,5 +1,6 @@
package constant
var (
DefaultAvatar = "system"
DefaultAvatar = "system"
DefaultSiteURL = ""
)
+10
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/plugin"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/segmentfault/pacman/cache"
@@ -76,6 +77,15 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
// NewCache new cache instance
func NewCache(c *CacheConf) (cache.Cache, func(), error) {
var pluginCache plugin.Cache
_ = plugin.CallCache(func(fn plugin.Cache) error {
pluginCache = fn
return nil
})
if pluginCache != nil {
return pluginCache, func() {}, nil
}
// TODO What cache type should be initialized according to the configuration file
memCache := memory.NewCache()
+15
View File
@@ -0,0 +1,15 @@
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
)
func HeadersByRequestURI() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.RequestURI, "/static/") {
c.Header("cache-control", "public, max-age=31536000")
}
}
}
@@ -0,0 +1,23 @@
package middleware
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
// BanAPIForUserCenter ban api for user center
func BanAPIForUserCenter(ctx *gin.Context) {
uc, ok := plugin.GetUserCenter()
if !ok {
return
}
if !uc.Description().EnabledOriginalUserSystem {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
ctx.Abort()
return
}
ctx.Next()
}
+29
View File
@@ -0,0 +1,29 @@
package reason
const (
PrivilegeLevel1Desc = "privilege.level_1.description"
PrivilegeLevel2Desc = "privilege.level_2.description"
PrivilegeLevel3Desc = "privilege.level_3.description"
RankQuestionAddLabel = "privilege.rank_question_add_label"
RankAnswerAddLabel = "privilege.rank_answer_add_label"
RankCommentAddLabel = "privilege.rank_comment_add_label"
RankReportAddLabel = "privilege.rank_report_add_label"
RankCommentVoteUpLabel = "privilege.rank_comment_vote_up_label"
RankLinkUrlLimitLabel = "privilege.rank_link_url_limit_label"
RankQuestionVoteUpLabel = "privilege.rank_question_vote_up_label"
RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label"
RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label"
RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label"
RankTagAddLabel = "privilege.rank_tag_add_label"
RankTagEditLabel = "privilege.rank_tag_edit_label"
RankQuestionEditLabel = "privilege.rank_question_edit_label"
RankAnswerEditLabel = "privilege.rank_answer_edit_label"
RankQuestionEditWithoutReviewLabel = "privilege.rank_question_edit_without_review_label"
RankAnswerEditWithoutReviewLabel = "privilege.rank_answer_edit_without_review_label"
RankQuestionAuditLabel = "privilege.rank_question_audit_label"
RankAnswerAuditLabel = "privilege.rank_answer_audit_label"
RankTagAuditLabel = "privilege.rank_tag_audit_label"
RankTagEditWithoutReviewLabel = "privilege.rank_tag_edit_without_review_label"
RankTagSynonymLabel = "privilege.rank_tag_synonym_label"
)
+62 -56
View File
@@ -11,63 +11,69 @@ const (
UnauthorizedError = "base.unauthorized_error"
// DatabaseError database error
DatabaseError = "base.database_error"
// ForbiddenError forbidden error
ForbiddenError = "base.forbidden_error"
)
const (
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update"
QuestionAlreadyDeleted = "error.question.already_deleted"
AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update"
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
TagCannotUpdate = "error.tag.cannot_update"
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
TagAlreadyExist = "error.tag.already_exist"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update"
QuestionAlreadyDeleted = "error.question.already_deleted"
AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update"
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
EmailIllegalDomainError = "error.email.illegal_email_domain_error"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
TagCannotUpdate = "error.tag.cannot_update"
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
TagAlreadyExist = "error.tag.already_exist"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden"
UserAccessDenied = "error.user.access_denied"
UserPageAccessDenied = "error.user.page_access_denied"
)
+15 -1
View File
@@ -7,6 +7,7 @@ import (
brotli "github.com/anargu/gin-brotli"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/plugin"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
)
@@ -20,6 +21,7 @@ func NewHTTPServer(debug bool,
authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware,
templateRouter *router.TemplateRouter,
pluginAPIRouter *router.PluginAPIRouter,
) *gin.Engine {
if debug {
@@ -34,7 +36,7 @@ func NewHTTPServer(debug bool,
html, _ := fs.Sub(ui.Template, "template")
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
r.SetHTMLTemplate(htmlTemplate)
r.Use(middleware.HeadersByRequestURI())
viewRouter.Register(r)
rootGroup := r.Group("")
@@ -62,5 +64,17 @@ func NewHTTPServer(debug bool,
answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)
templateRouter.RegisterTemplateRouter(rootGroup)
// plugin routes
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1)
pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)
_ = plugin.CallAgent(func(agent plugin.Agent) error {
agent.RegisterUnAuthRouter(mustUnAuthV1)
agent.RegisterAuthUserRouter(authV1)
agent.RegisterAuthAdminRouter(adminauthV1)
return nil
})
return r
}
+5
View File
@@ -59,6 +59,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
originalTr := struct {
Backend map[string]map[string]interface{} `yaml:"backend"`
UI map[string]interface{} `yaml:"ui"`
Plugin map[string]interface{} `yaml:"plugin"`
}{}
if err = yaml.Unmarshal(buf, &originalTr); err != nil {
return nil, err
@@ -69,6 +70,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
}
translation["backend"] = originalTr.Backend
translation["ui"] = originalTr.UI
translation["plugin"] = originalTr.Plugin
content, err := yaml.Marshal(translation)
if err != nil {
@@ -120,6 +122,9 @@ func CheckLanguageIsValid(lang string) bool {
// Tr use language to translate data. If this language translation is not available, return default english translation.
func Tr(lang i18n.Language, data string) string {
if GlobalTrans == nil {
return data
}
translation := GlobalTrans.Tr(lang, data)
if translation == data {
return GlobalTrans.Tr(i18n.DefaultLanguage, data)
+358
View File
@@ -0,0 +1,358 @@
package cli
import (
"bytes"
"embed"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/semver/v3"
"github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/writer"
"github.com/answerdev/answer/ui"
"github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
const (
mainGoTpl = `package main
import (
answercmd "github.com/answerdev/answer/cmd"
// remote plugins
{{- range .remote_plugins}}
_ "{{.}}"
{{- end}}
// local plugins
{{- range .local_plugins}}
_ "answer/{{.}}"
{{- end}}
)
func main() {
answercmd.Main()
}
`
goModTpl = `module answer
go 1.19
`
)
type answerBuilder struct {
buildingMaterial *buildingMaterial
BuildError error
}
type buildingMaterial struct {
answerModuleReplacement string
plugins []*pluginInfo
outputPath string
tmpDir string
originalAnswerInfo OriginalAnswerInfo
}
type OriginalAnswerInfo struct {
Version string
Revision string
Time string
}
type pluginInfo struct {
// Name of the plugin e.g. github.com/answerdev/github-connector
Name string
// Path to the plugin. If path exist, read plugin from local filesystem
Path string
// Version of the plugin
Version string
}
func newAnswerBuilder(outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) *answerBuilder {
material := &buildingMaterial{originalAnswerInfo: originalAnswerInfo}
parentDir, _ := filepath.Abs(".")
material.tmpDir, _ = os.MkdirTemp(parentDir, "answer_build")
if len(outputPath) == 0 {
outputPath = filepath.Join(parentDir, "new_answer")
}
material.outputPath = outputPath
material.plugins = formatPlugins(plugins)
material.answerModuleReplacement = os.Getenv("ANSWER_MODULE")
return &answerBuilder{
buildingMaterial: material,
}
}
func (a *answerBuilder) DoTask(task func(b *buildingMaterial) error) {
if a.BuildError != nil {
return
}
a.BuildError = task(a.buildingMaterial)
}
// BuildNewAnswer builds a new answer with specified plugins
func BuildNewAnswer(outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) (err error) {
builder := newAnswerBuilder(outputPath, plugins, originalAnswerInfo)
builder.DoTask(createMainGoFile)
builder.DoTask(downloadGoModFile)
builder.DoTask(mergeI18nFiles)
builder.DoTask(replaceNecessaryFile)
builder.DoTask(buildBinary)
builder.DoTask(cleanByproduct)
return builder.BuildError
}
func formatPlugins(plugins []string) (formatted []*pluginInfo) {
for _, plugin := range plugins {
plugin = strings.TrimSpace(plugin)
// plugin description like this 'github.com/answerdev/github-connector@latest=/local/path'
info := &pluginInfo{}
plugin, info.Path, _ = strings.Cut(plugin, "=")
info.Name, info.Version, _ = strings.Cut(plugin, "@")
formatted = append(formatted, info)
}
return formatted
}
func createMainGoFile(b *buildingMaterial) (err error) {
fmt.Printf("[build] tmp dir: %s\n", b.tmpDir)
err = dir.CreateDirIfNotExist(b.tmpDir)
if err != nil {
return err
}
var (
remotePlugins []string
)
for _, p := range b.plugins {
remotePlugins = append(remotePlugins, versionedModulePath(p.Name, p.Version))
}
mainGoFile := &bytes.Buffer{}
tmpl, err := template.New("main").Parse(mainGoTpl)
if err != nil {
return err
}
err = tmpl.Execute(mainGoFile, map[string]any{
"remote_plugins": remotePlugins,
})
if err != nil {
return err
}
err = writer.WriteFile(filepath.Join(b.tmpDir, "main.go"), mainGoFile.String())
if err != nil {
return err
}
err = writer.WriteFile(filepath.Join(b.tmpDir, "go.mod"), goModTpl)
if err != nil {
return err
}
for _, p := range b.plugins {
if len(p.Path) == 0 {
continue
}
replacement := fmt.Sprintf("%s@v%s=%s", p.Name, p.Version, p.Path)
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
if err != nil {
return err
}
}
return
}
func downloadGoModFile(b *buildingMaterial) (err error) {
// If user specify a module replacement, use it. Otherwise, use the latest version.
if len(b.answerModuleReplacement) > 0 {
replacement := fmt.Sprintf("%s=%s", "github.com/answerdev/answer", b.answerModuleReplacement)
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
if err != nil {
return err
}
}
err = b.newExecCmd("go", "mod", "tidy").Run()
if err != nil {
return err
}
err = b.newExecCmd("go", "mod", "vendor").Run()
if err != nil {
return err
}
return
}
func replaceNecessaryFile(b *buildingMaterial) (err error) {
fmt.Printf("try to replace ui build directory\n")
uiBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui")
err = copyDirEntries(ui.Build, ".", uiBuildDir)
return err
}
func mergeI18nFiles(b *buildingMaterial) (err error) {
fmt.Printf("try to merge i18n files\n")
type YamlPluginContent struct {
Plugin map[string]any `yaml:"plugin"`
}
pluginAllTranslations := make(map[string]*YamlPluginContent)
for _, plugin := range b.plugins {
i18nDir := filepath.Join(b.tmpDir, fmt.Sprintf("vendor/%s/i18n", plugin.Name))
fmt.Println("i18n dir: ", i18nDir)
if !dir.CheckDirExist(i18nDir) {
continue
}
entries, err := os.ReadDir(i18nDir)
if err != nil {
return err
}
for _, file := range entries {
// ignore directory
if file.IsDir() {
continue
}
// ignore non-YAML file
if filepath.Ext(file.Name()) != ".yaml" {
continue
}
buf, err := os.ReadFile(filepath.Join(i18nDir, file.Name()))
if err != nil {
log.Debugf("read translation file failed: %s %s", file.Name(), err)
continue
}
translation := &YamlPluginContent{}
if err = yaml.Unmarshal(buf, translation); err != nil {
log.Debugf("unmarshal translation file failed: %s %s", file.Name(), err)
continue
}
if pluginAllTranslations[file.Name()] == nil {
pluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)}
}
for k, v := range translation.Plugin {
pluginAllTranslations[file.Name()].Plugin[k] = v
}
}
}
originalI18nDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/i18n")
entries, err := os.ReadDir(originalI18nDir)
if err != nil {
return err
}
for _, file := range entries {
// ignore directory
if file.IsDir() {
continue
}
// ignore non-YAML file
filename := file.Name()
if filepath.Ext(filename) != ".yaml" && filename != "i18n.yaml" {
continue
}
// if plugin don't have this translation file, ignore it
if pluginAllTranslations[filename] == nil {
continue
}
out, _ := yaml.Marshal(pluginAllTranslations[filename])
buf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Debugf("read translation file failed: %s %s", filename, err)
continue
}
_, _ = buf.WriteString("\n")
_, _ = buf.Write(out)
_ = buf.Close()
}
return err
}
func copyDirEntries(sourceFs embed.FS, sourceDir string, targetDir string) (err error) {
entries, err := ui.Build.ReadDir(sourceDir)
if err != nil {
return err
}
err = dir.CreateDirIfNotExist(targetDir)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
err = copyDirEntries(sourceFs, filepath.Join(sourceDir, entry.Name()), filepath.Join(targetDir, entry.Name()))
if err != nil {
return err
}
continue
}
file, err := sourceFs.ReadFile(filepath.Join(sourceDir, entry.Name()))
if err != nil {
return err
}
filename := filepath.Join(targetDir, entry.Name())
err = os.WriteFile(filename, file, 0666)
if err != nil {
return err
}
}
return nil
}
func buildBinary(b *buildingMaterial) (err error) {
versionInfo := b.originalAnswerInfo
cmdPkg := "github.com/answerdev/answer/cmd"
ldflags := fmt.Sprintf("-X %s.Version=%s -X %s.Revision=%s -X %s.Time=%s",
cmdPkg, versionInfo.Version, cmdPkg, versionInfo.Revision, cmdPkg, versionInfo.Time)
err = b.newExecCmd("go", "build",
"-ldflags", ldflags, "-o", b.outputPath, ".").Run()
if err != nil {
return err
}
return
}
func cleanByproduct(b *buildingMaterial) (err error) {
return os.RemoveAll(b.tmpDir)
}
func (b *buildingMaterial) newExecCmd(command string, args ...string) *exec.Cmd {
cmd := exec.Command(command, args...)
fmt.Println(cmd.Args)
cmd.Dir = b.tmpDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}
func versionedModulePath(modulePath, moduleVersion string) string {
if moduleVersion == "" {
return modulePath
}
ver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, "v"))
if err != nil {
return modulePath
}
major := ver.Major()
if major > 1 {
modulePath += fmt.Sprintf("/v%d", major)
}
return path.Clean(modulePath)
}
+254
View File
@@ -0,0 +1,254 @@
package controller
import (
"fmt"
"net/http"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
const (
commonRouterPrefix = "/answer/api/v1"
ConnectorLoginRouterPrefix = "/connector/login/"
ConnectorRedirectRouterPrefix = "/connector/redirect/"
)
// ConnectorController comment controller
type ConnectorController struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
userExternalService *user_external_login.UserExternalLoginService
emailService *export.EmailService
}
// NewConnectorController new controller
func NewConnectorController(
siteInfoService *siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
userExternalService *user_external_login.UserExternalLoginService,
) *ConnectorController {
return &ConnectorController{
siteInfoService: siteInfoService,
userExternalService: userExternalService,
emailService: emailService,
}
}
// ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name
// We can't register specific router for each connector when application start, because the plugin status will be changed by admin.
// If the plugin is disabled, the router should be unavailable.
func (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) {
slugName := ctx.Param("name")
var c plugin.Connector
_ = plugin.CallConnector(func(connector plugin.Connector) error {
if connector.ConnectorSlugName() == slugName {
c = connector
}
return nil
})
if c == nil {
log.Errorf("connector %s not found", slugName)
ctx.Redirect(http.StatusFound, "/50x")
return
}
cc.ConnectorLogin(c)(ctx)
}
func (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) {
slugName := ctx.Param("name")
var c plugin.Connector
_ = plugin.CallConnector(func(connector plugin.Connector) error {
if connector.ConnectorSlugName() == slugName {
c = connector
}
return nil
})
if c == nil {
log.Errorf("connector %s not found", slugName)
ctx.Redirect(http.StatusFound, "/50x")
return
}
cc.ConnectorRedirect(c)(ctx)
}
func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) {
return func(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl,
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
redirectURL := connector.ConnectorSender(ctx, receiverURL)
if len(redirectURL) > 0 {
ctx.Redirect(http.StatusFound, redirectURL)
}
return
}
}
func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) {
return func(ctx *gin.Context) {
siteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
receiverURL := fmt.Sprintf("%s%s%s%s", siteGeneral.SiteUrl,
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
userInfo, err := connector.ConnectorReceiver(ctx, receiverURL)
if err != nil {
log.Errorf("connector received failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
log.Debugf("connector received: %+v", userInfo)
u := &schema.ExternalLoginUserInfoCache{
Provider: connector.ConnectorSlugName(),
ExternalID: userInfo.ExternalID,
DisplayName: userInfo.DisplayName,
Username: userInfo.Username,
Email: userInfo.Email,
Avatar: userInfo.Avatar,
MetaInfo: userInfo.MetaInfo,
}
resp, err := cc.userExternalService.ExternalLogin(ctx, u)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
if len(resp.AccessToken) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
} else {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s",
siteGeneral.SiteUrl, resp.BindingKey))
}
}
}
// ConnectorsInfo get all enabled connectors
// @Summary get all enabled connectors
// @Description get all enabled connectors
// @Tags PluginConnector
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp}
// @Router /answer/api/v1/connector/info [get]
func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
resp := make([]*schema.ConnectorInfoResp, 0)
_ = plugin.CallConnector(func(fn plugin.Connector) error {
connectorName := fn.ConnectorName()
resp = append(resp, &schema.ConnectorInfoResp{
Name: connectorName.Translate(ctx),
Icon: fn.ConnectorLogoSVG(),
Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl,
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
})
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// ExternalLoginBindingUserSendEmail external login binding user send email
// @Summary external login binding user send email
// @Description external login binding user send email
// @Tags PluginConnector
// @Accept json
// @Produce json
// @Param data body schema.ExternalLoginBindingUserSendEmailReq true "external login binding user send email"
// @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp}
// @Router /answer/api/v1/connector/binding/email [post]
func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) {
req := &schema.ExternalLoginBindingUserSendEmailReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// ConnectorsUserInfo get all connectors info about user
// @Summary get all connectors info about user
// @Description get all connectors info about user
// @Tags PluginConnector
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp}
// @Router /answer/api/v1/connector/user/info [get]
func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
userInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
userExternalLoginMapping := make(map[string]string)
for _, userInfo := range userInfoList {
userExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID
}
resp := make([]*schema.ConnectorUserInfoResp, 0)
_ = plugin.CallConnector(func(fn plugin.Connector) error {
externalID := userExternalLoginMapping[fn.ConnectorSlugName()]
connectorName := fn.ConnectorName()
resp = append(resp, &schema.ConnectorUserInfoResp{
Name: connectorName.Translate(ctx),
Icon: fn.ConnectorLogoSVG(),
Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl,
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
Binding: len(externalID) > 0,
ExternalID: externalID,
})
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// ExternalLoginUnbinding unbind external user login
// @Summary unbind external user login
// @Description unbind external user login
// @Tags PluginConnector
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param data body schema.ExternalLoginUnbindingReq true "ExternalLoginUnbindingReq"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/connector/user/unbinding [delete]
func (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) {
req := &schema.ExternalLoginUnbindingReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
+2
View File
@@ -24,4 +24,6 @@ var ProviderSetController = wire.NewSet(
NewUploadController,
NewActivityController,
NewTemplateController,
NewConnectorController,
NewUserCenterController,
)
@@ -0,0 +1,196 @@
package controller
import (
"fmt"
"net/http"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
const (
UserCenterLoginRouter = "/user-center/login/redirect"
UserCenterSignUpRedirectRouter = "/user-center/sign-up/redirect"
)
// UserCenterController comment controller
type UserCenterController struct {
userCenterLoginService *user_external_login.UserCenterLoginService
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewUserCenterController new controller
func NewUserCenterController(
userCenterLoginService *user_external_login.UserCenterLoginService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *UserCenterController {
return &UserCenterController{
userCenterLoginService: userCenterLoginService,
siteInfoService: siteInfoService,
}
}
// UserCenterAgent get user center agent info
func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {
resp := &schema.UserCenterAgentResp{}
resp.Enabled = plugin.UserCenterEnabled()
if !resp.Enabled {
handler.HandleResponse(ctx, nil, resp)
return
}
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
resp.AgentInfo = &schema.AgentInfo{}
resp.AgentInfo.LoginRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
commonRouterPrefix, UserCenterLoginRouter)
resp.AgentInfo.SignUpRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
commonRouterPrefix, UserCenterSignUpRedirectRouter)
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
info := uc.Description()
resp.AgentInfo.Name = info.Name
resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx)
resp.AgentInfo.Icon = info.Icon
resp.AgentInfo.Url = info.Url
resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0)
resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem
items := uc.ControlCenterItems()
for _, item := range items {
resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{
Name: item.Name,
Label: item.Label,
Url: item.Url,
})
}
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// UserCenterPersonalBranding get user center personal user info
func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) {
req := &schema.GetOtherUserInfoByUsernameReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username)
handler.HandleResponse(ctx, err, resp)
}
func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {
var redirectURL string
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
info := userCenter.Description()
redirectURL = info.LoginRedirectURL
return nil
})
ctx.Redirect(http.StatusFound, redirectURL)
}
func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) {
var redirectURL string
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
info := userCenter.Description()
redirectURL = info.LoginRedirectURL
return nil
})
ctx.Redirect(http.StatusFound, redirectURL)
}
func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
ctx.Redirect(http.StatusFound, "/404")
return
}
userInfo, err := userCenter.LoginCallback(ctx)
if err != nil {
log.Error(err)
if !ctx.IsAborted() {
ctx.Redirect(http.StatusFound, "/50x")
}
return
}
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
if len(resp.ErrMsg) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
return
}
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
}
func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
ctx.Redirect(http.StatusFound, "/404")
return
}
userInfo, err := userCenter.SignUpCallback(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
if len(resp.ErrMsg) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
return
}
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
}
// UserCenterUserSettings user center user settings
func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
resp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID)
handler.HandleResponse(ctx, err, resp)
}
// UserCenterAdminFunctionAgent user center admin function agent
func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) {
resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx)
handler.HandleResponse(ctx, err, resp)
}
+45 -52
View File
@@ -11,7 +11,6 @@ import (
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
@@ -552,84 +551,75 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) {
})
}
// UserList godoc
// @Summary UserList
// @Description UserList
// @Tags Question
// PersonalQuestionPage list personal questions
// @Summary list personal questions
// @Description list personal questions
// @Tags Personal
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Param order query string true "order" Enums(newest,score)
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Param page_size query string true "page_size" default(20)
// @Success 200 {object} handler.RespBody
// @Router /personal/question/page [get]
func (qc *QuestionController) UserList(ctx *gin.Context) {
userName := ctx.Query("username")
order := ctx.Query("order")
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserList(ctx, userName, order, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) {
req := &schema.PersonalQuestionPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.PersonalQuestionPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UserAnswerList godoc
// @Summary UserAnswerList
// @Description UserAnswerList
// @Tags api-answer
// PersonalAnswerPage list personal answers
// @Summary list personal answers
// @Description list personal answers
// @Tags Personal
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Param order query string true "order" Enums(newest,score)
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Param page_size query string true "page_size" default(20)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/answer/page [get]
func (qc *QuestionController) UserAnswerList(ctx *gin.Context) {
userName := ctx.Query("username")
order := ctx.Query("order")
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserAnswerList(ctx, userName, order, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) {
req := &schema.PersonalAnswerPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.PersonalAnswerPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UserCollectionList godoc
// @Summary UserCollectionList
// @Description UserCollectionList
// PersonalCollectionPage list personal collections
// @Summary list personal collections
// @Description list personal collections
// @Tags Collection
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Param page_size query string true "page_size" default(20)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/collection/page [get]
func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserCollectionList(ctx, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
req := &schema.PersonalCollectionPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.PersonalCollectionPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// AdminSearchList godoc
@@ -678,6 +668,9 @@ func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) {
return
}
req.QuestionID = uid.DeShortID(req.QuestionID)
if req.QuestionID == "0" {
req.QuestionID = ""
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
@@ -64,6 +64,10 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
if err != nil {
log.Error(err)
}
resp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx)
if err != nil {
log.Error(err)
}
handler.HandleResponse(ctx, nil, resp)
}
+21 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/pkg/checker"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -183,9 +184,9 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
return
}
resp, err := uc.userService.UseRePassword(ctx, req)
err := uc.userService.UpdatePasswordWhenForgot(ctx, req)
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
handler.HandleResponse(ctx, err, resp)
handler.HandleResponse(ctx, err, nil)
}
// UserLogout user logout
@@ -223,7 +224,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
return
}
if !siteInfo.AllowNewRegistrations {
if !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations {
handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)
return
}
@@ -232,6 +233,10 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
return
}
req.IP = ctx.ClientIP()
captchaPass := uc.actionService.UserRegisterVerifyCaptcha(ctx, req.CaptchaID, req.CaptchaCode)
if !captchaPass {
@@ -334,15 +339,16 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest"
// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/password [put]
func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
req := &schema.UserModifyPassWordRequest{}
req := &schema.UserModifyPasswordReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.AccessToken = middleware.ExtractToken(ctx)
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
if err != nil {
@@ -488,6 +494,16 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
return
}
// check whether email allow register or not
siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
return
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
+1
View File
@@ -9,4 +9,5 @@ var ProviderSetController = wire.NewSet(
NewThemeController,
NewSiteInfoController,
NewRoleController,
NewPluginController,
)
@@ -0,0 +1,184 @@
package controller_admin
import (
"encoding/json"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/plugin_common"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
)
// PluginController role controller
type PluginController struct {
PluginCommonService *plugin_common.PluginCommonService
}
// NewPluginController new controller
func NewPluginController(PluginCommonService *plugin_common.PluginCommonService) *PluginController {
return &PluginController{PluginCommonService: PluginCommonService}
}
// GetPluginList get plugin list
// @Summary get plugin list
// @Description get plugin list
// @Tags AdminPlugin
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param status query string false "status: active/inactive"
// @Param have_config query boolean false "have config"
// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp}
// @Router /answer/admin/api/plugins [get]
func (pc *PluginController) GetPluginList(ctx *gin.Context) {
req := &schema.GetPluginListReq{}
if handler.BindAndCheck(ctx, req) {
return
}
pluginConfigMapping := make(map[string]bool)
_ = plugin.CallConfig(func(fn plugin.Config) error {
if len(fn.ConfigFields()) > 0 {
pluginConfigMapping[fn.Info().SlugName] = true
}
return nil
})
resp := make([]*schema.GetPluginListResp, 0)
_ = plugin.CallBase(func(base plugin.Base) error {
info := base.Info()
resp = append(resp, &schema.GetPluginListResp{
Name: info.Name.Translate(ctx),
SlugName: info.SlugName,
Description: info.Description.Translate(ctx),
Version: info.Version,
Enabled: plugin.StatusManager.IsEnabled(info.SlugName),
HaveConfig: pluginConfigMapping[info.SlugName],
Link: info.Link,
})
return nil
})
if len(req.Status) > 0 {
resp = pc.filterPluginByStatus(resp, req.Status)
}
if req.HaveConfig {
resp = pc.filterNoConfigPlugin(resp)
}
handler.HandleResponse(ctx, nil, resp)
}
func (pc *PluginController) filterNoConfigPlugin(list []*schema.GetPluginListResp) []*schema.GetPluginListResp {
resp := make([]*schema.GetPluginListResp, 0)
for _, t := range list {
if t.HaveConfig {
resp = append(resp, t)
}
}
return resp
}
func (pc *PluginController) filterPluginByStatus(list []*schema.GetPluginListResp, status schema.PluginStatus,
) []*schema.GetPluginListResp {
resp := make([]*schema.GetPluginListResp, 0)
for _, t := range list {
if status == schema.PluginStatusActive && t.Enabled {
resp = append(resp, t)
} else if status == schema.PluginStatusInactive && !t.Enabled {
resp = append(resp, t)
}
}
return resp
}
// UpdatePluginStatus update plugin status
// @Summary update plugin status
// @Description update plugin status
// @Tags AdminPlugin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UpdatePluginStatusReq true "UpdatePluginStatusReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/plugin/status [put]
func (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) {
req := &schema.UpdatePluginStatusReq{}
if handler.BindAndCheck(ctx, req) {
return
}
plugin.StatusManager.Enable(req.PluginSlugName, req.Enabled)
err := pc.PluginCommonService.UpdatePluginStatus(ctx)
handler.HandleResponse(ctx, err, nil)
}
// GetPluginConfig get plugin config
// @Summary get plugin config
// @Description get plugin config
// @Tags AdminPlugin
// @Security ApiKeyAuth
// @Produce json
// @Param plugin_slug_name query string true "plugin_slug_name"
// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp}
// @Router /answer/admin/api/plugin/config [get]
func (pc *PluginController) GetPluginConfig(ctx *gin.Context) {
req := &schema.GetPluginConfigReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp := &schema.GetPluginConfigResp{}
_ = plugin.CallBase(func(base plugin.Base) error {
if base.Info().SlugName != req.PluginSlugName {
return nil
}
info := base.Info()
resp.Name = info.Name.Translate(ctx)
resp.SlugName = info.SlugName
resp.Description = info.Description.Translate(ctx)
resp.Version = info.Version
return nil
})
_ = plugin.CallConfig(func(fn plugin.Config) error {
if fn.Info().SlugName != req.PluginSlugName {
return nil
}
resp.SetConfigFields(ctx, fn.ConfigFields())
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// UpdatePluginConfig update plugin config
// @Summary update plugin config
// @Description update plugin config
// @Tags AdminPlugin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UpdatePluginConfigReq true "UpdatePluginConfigReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/plugin/config [put]
func (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) {
req := &schema.UpdatePluginConfigReq{}
if handler.BindAndCheck(ctx, req) {
return
}
configFields, _ := json.Marshal(req.ConfigFields)
err := plugin.CallConfig(func(fn plugin.Config) error {
if fn.Info().SlugName == req.PluginSlugName {
return fn.ConfigReceiver(configFields)
}
return nil
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
err = pc.PluginCommonService.UpdatePluginConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -139,6 +139,19 @@ func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// GetSiteUsers get site user config
// @Summary get site user config
// @Description get site user config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteUsersResp}
// @Router /answer/admin/api/siteinfo/users [get]
func (sc *SiteInfoController) GetSiteUsers(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteUsers(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetRobots get site robots information
// @Summary get site robots information
// @Description get site robots information
@@ -336,6 +349,24 @@ func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UpdateSiteUsers update site config about users
// @Summary update site info config about users
// @Description update site info config about users
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteUsersReq true "users info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/users [put]
func (sc *SiteInfoController) UpdateSiteUsers(ctx *gin.Context) {
req := &schema.SiteUsersReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteUsers(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetSMTPConfig get smtp config
// @Summary GetSMTPConfig get smtp config
// @Description GetSMTPConfig get smtp config
@@ -366,3 +397,34 @@ func (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) {
err := sc.siteInfoService.UpdateSMTPConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetPrivilegesConfig get privileges config
// @Summary GetPrivilegesConfig get privileges config
// @Description GetPrivilegesConfig get privileges config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.GetPrivilegesConfigResp}
// @Router /answer/admin/api/setting/privileges [get]
func (sc *SiteInfoController) GetPrivilegesConfig(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetPrivilegesConfig(ctx)
handler.HandleResponse(ctx, err, resp)
}
// UpdatePrivilegesConfig update privileges config
// @Summary update privileges config
// @Description update privileges config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.UpdatePrivilegesConfigReq true "config"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/setting/privileges [put]
func (sc *SiteInfoController) UpdatePrivilegesConfig(ctx *gin.Context) {
req := &schema.UpdatePrivilegesConfigReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.UpdatePrivilegesConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -3,9 +3,12 @@ package controller_admin
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
// UserAdminController user controller
@@ -29,6 +32,10 @@ func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdmin
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/status [put]
func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
return
}
req := &schema.UpdateUserStatusReq{}
if handler.BindAndCheck(ctx, req) {
return
+1
View File
@@ -6,4 +6,5 @@ type UserCacheInfo struct {
UserStatus int `json:"user_status"`
EmailStatus int `json:"email_status"`
RoleID int `json:"role_id"`
ExternalID string `json:"external_id"`
}
+13
View File
@@ -0,0 +1,13 @@
package entity
// PluginConfig plugin config
type PluginConfig struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
PluginSlugName string `xorm:"unique VARCHAR(128) plugin_slug_name"`
Value string `xorm:"TEXT value"`
}
// TableName config table name
func (PluginConfig) TableName() string {
return "plugin_config"
}
@@ -0,0 +1,19 @@
package entity
import "time"
// UserExternalLogin user external login
type UserExternalLogin struct {
ID int64 `xorm:"not null pk autoincr BIGINT(20) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
Provider string `xorm:"not null default '' VARCHAR(100) provider"`
ExternalID string `xorm:"not null default '' VARCHAR(128) external_id"`
MetaInfo string `xorm:"TEXT meta_info"`
}
// TableName table name
func (UserExternalLogin) TableName() string {
return "user_external_login"
}
+34 -9
View File
@@ -50,6 +50,8 @@ var tables = []interface{}{
&entity.RolePowerRel{},
&entity.Power{},
&entity.UserRoleRel{},
&entity.PluginConfig{},
&entity.UserExternalLogin{},
}
// InitDB init db
@@ -112,9 +114,8 @@ func initAdminUser(engine *xorm.Engine) error {
func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
interfaceData := map[string]string{
"logo": "",
"theme": "black",
"language": language,
"language": language,
"time_zone": "UTC",
}
interfaceDataBytes, _ := json.Marshal(interfaceData)
_, err := engine.InsertOne(&entity.SiteInfo{
@@ -142,8 +143,9 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
}
loginConfig := map[string]bool{
"allow_new_registrations": true,
"login_required": false,
"allow_new_registrations": true,
"allow_email_registrations": true,
"login_required": false,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
_, err = engine.InsertOne(&entity.SiteInfo{
@@ -177,6 +179,25 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
if err != nil {
return err
}
usersData := map[string]any{
"default_avatar": "gravatar",
"allow_update_display_name": true,
"allow_update_username": true,
"allow_update_avatar": true,
"allow_update_bio": true,
"allow_update_website": true,
"allow_update_location": true,
}
usersDataBytes, _ := json.Marshal(usersData)
_, err = engine.InsertOne(&entity.SiteInfo{
Type: "users",
Content: string(usersDataBytes),
Status: 1,
})
if err != nil {
return err
}
return err
}
@@ -345,10 +366,14 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
{ID: 118, Key: "plugin.status", Value: `{}`},
{ID: 119, Key: "question.pin", Value: `-1`},
{ID: 120, Key: "question.unpin", Value: `-1`},
{ID: 121, Key: "question.show", Value: `-1`},
{ID: 122, Key: "question.hide", Value: `-1`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
_, err := engine.Insert(defaultConfigTable)
return err
+4
View File
@@ -58,6 +58,10 @@ var migrations = []Migration{
NewMigration("add new answer notification", addNewAnswerNotification, true),
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
NewMigration("update accept answer rank", updateAcceptAnswerRank, true),
NewMigration("add plugin", addPlugin, false),
NewMigration("update user pin hide features", updateRolePinAndHideFeatures, true),
NewMigration("update question post time", updateQuestionPostTime, true),
NewMigration("add login limitations", addLoginLimitations, true),
}
// GetCurrentDBVersion returns the current db version
+69
View File
@@ -0,0 +1,69 @@
package migrations
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/tidwall/gjson"
"xorm.io/xorm"
)
func addLoginLimitations(x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeLogin,
}
exist, err := x.Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteLoginReq{}
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
content.AllowEmailRegistrations = true
content.AllowEmailDomains = make([]string, 0)
_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
interfaceSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeInterface,
}
exist, err = x.Get(interfaceSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
siteUsers := &schema.SiteUsersReq{
AllowUpdateDisplayName: true,
AllowUpdateUsername: true,
AllowUpdateAvatar: true,
AllowUpdateBio: true,
AllowUpdateWebsite: true,
AllowUpdateLocation: true,
}
if exist {
siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String()
}
data, _ := json.Marshal(siteUsers)
exist, err = x.Get(&entity.SiteInfo{Type: constant.SiteTypeUsers})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
usersSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
Content: string(data),
Status: 1,
}
_, err = x.InsertOne(usersSiteInfo)
if err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
}
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateRolePinAndHideFeatures(x *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateQuestionPostTime(x *xorm.Engine) error {
questionList := make([]entity.Question, 0)
err := x.Find(&questionList, &entity.Question{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, item := range questionList {
if item.PostUpdateTime.IsZero() {
if !item.UpdatedAt.IsZero() {
item.PostUpdateTime = item.UpdatedAt
} else if !item.CreatedAt.IsZero() {
item.PostUpdateTime = item.CreatedAt
}
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
log.Errorf("update %+v config failed: %s", item, err)
return fmt.Errorf("update question failed: %w", err)
}
}
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addPlugin(x *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 118, Key: "plugin.status", Value: `{}`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return x.Sync(new(entity.PluginConfig), new(entity.UserExternalLogin))
}
+10 -6
View File
@@ -63,18 +63,22 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error {
}
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `-1`},
{ID: 120, Key: "question.unpin", Value: `-1`},
{ID: 121, Key: "question.show", Value: `-1`},
{ID: 122, Key: "question.hide", Value: `-1`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
exist, err := x.Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
+4 -2
View File
@@ -202,7 +202,9 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
}
notice_queue.AddNotification(msg)
if msg.TriggerUserID != msg.ReceiverUserID {
notice_queue.AddNotification(msg)
}
}
for _, act := range addActivityList {
@@ -214,7 +216,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
if act.UserID != questionUserID {
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AcceptAnswer
msg.NotificationAction = constant.NotificationAcceptAnswer
notice_queue.AddNotification(msg)
}
}
+52 -5
View File
@@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/internal/base/pager"
@@ -70,7 +71,9 @@ var LimitDownActions = map[string][]string{
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
notificationUserIDs := make([]string, 0)
achievementNotificationUserIDs := make([]string, 0)
sendInboxNotification := false
upVote := false
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
result = nil
for _, action := range actions {
@@ -127,7 +130,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if isReachStandard {
insertActivity.Rank = 0
}
notificationUserIDs = append(notificationUserIDs, activityUserID)
achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
}
if has {
@@ -142,13 +145,17 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if err != nil {
return nil, err
}
sendInboxNotification = true
}
// update votes
if action == "vote_down" || action == "vote_up" {
if action == constant.ActVoteDown || action == constant.ActVoteUp {
votes := 1
if action == "vote_down" {
if action == constant.ActVoteDown {
upVote = false
votes = -1
} else {
upVote = true
}
err = vr.updateVotes(ctx, session, objectID, votes)
if err != nil {
@@ -165,9 +172,12 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
for _, activityUserID := range notificationUserIDs {
for _, activityUserID := range achievementNotificationUserIDs {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
}
if sendInboxNotification {
vr.sendVoteInboxNotification(userID, objectUserID, objectID, upVote)
}
return
}
@@ -441,3 +451,40 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object
}
notice_queue.AddNotification(msg)
}
func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, objectID string, upvote bool) {
if triggerUserID == receiverUserID {
return
}
objectType, _ := obj.GetObjectTypeStrByObjectID(objectID)
msg := &schema.NotificationMsg{
TriggerUserID: triggerUserID,
ReceiverUserID: receiverUserID,
Type: schema.NotificationTypeInbox,
ObjectID: objectID,
ObjectType: objectType,
}
if objectType == constant.QuestionObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheQuestion
} else {
msg.NotificationAction = constant.NotificationDownVotedTheQuestion
}
}
if objectType == constant.AnswerObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheAnswer
} else {
msg.NotificationAction = constant.NotificationDownVotedTheAnswer
}
}
if objectType == constant.CommentObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheComment
}
}
if len(msg.NotificationAction) > 0 {
notice_queue.AddNotification(msg)
}
}
+4 -1
View File
@@ -148,7 +148,7 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
}
// RemoveUserTokens Log out all users under this user id
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key)
mapping := make(map[string]bool, 0)
@@ -158,6 +158,9 @@ func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
}
for token := range mapping {
if token == remainToken {
continue
}
if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
log.Error(err)
} else {
@@ -0,0 +1,49 @@
package plugin_config
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/plugin_common"
"github.com/segmentfault/pacman/errors"
)
type pluginConfigRepo struct {
data *data.Data
}
// NewPluginConfigRepo new repository
func NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo {
return &pluginConfigRepo{
data: data,
}
}
func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) {
old := &entity.PluginConfig{PluginSlugName: pluginSlugName}
exist, err := ur.data.DB.Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
old.Value = configValue
_, err = ur.data.DB.ID(old.ID).Update(old)
} else {
_, err = ur.data.DB.InsertOne(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) {
pluginConfigs = make([]*entity.PluginConfig, 0)
err = ur.data.DB.Find(&pluginConfigs)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return pluginConfigs, err
}
+4
View File
@@ -14,6 +14,7 @@ import (
"github.com/answerdev/answer/internal/repo/export"
"github.com/answerdev/answer/internal/repo/meta"
"github.com/answerdev/answer/internal/repo/notification"
"github.com/answerdev/answer/internal/repo/plugin_config"
"github.com/answerdev/answer/internal/repo/question"
"github.com/answerdev/answer/internal/repo/rank"
"github.com/answerdev/answer/internal/repo/reason"
@@ -26,6 +27,7 @@ import (
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/repo/user_external_login"
"github.com/google/wire"
)
@@ -72,4 +74,6 @@ var ProviderSetRepo = wire.NewSet(
role.NewUserRoleRelRepo,
role.NewRolePowerRelRepo,
role.NewPowerRepo,
user_external_login.NewUserExternalLoginRepo,
plugin_config.NewPluginConfigRepo,
)
+4 -1
View File
@@ -255,7 +255,7 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
}
// GetQuestionPage query question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
@@ -271,6 +271,9 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
} else {
session.And("question.show = ?", entity.QuestionShow)
}
if inDays > 0 {
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays))
}
switch orderCond {
case "newest":
+5
View File
@@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/plugin"
"github.com/jinzhu/now"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -36,6 +37,10 @@ func NewUserRankRepo(data *data.Data, configRepo config.ConfigRepo) rank.UserRan
func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
session *xorm.Session, userID string, deltaRank int, activityType int,
) (isReachStandard bool, err error) {
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
if plugin.RankAgentEnabled() {
return false, nil
}
if deltaRank == 0 {
return false, nil
}
+1 -1
View File
@@ -46,7 +46,7 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag
// GetTagBySlugName get tag by slug name
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = LOWER(?)", slugName)
session := tr.data.DB.Where("LOWER(slug_name) = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
+17
View File
@@ -86,6 +86,13 @@ func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
if err != nil {
return nil, false, err
}
return
}
@@ -96,6 +103,14 @@ func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if !exist {
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
if err != nil {
return nil, false, err
}
return
}
@@ -127,6 +142,8 @@ func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, us
total, err = pager.Help(page, pageSize, &users, user, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, users)
return
}
+143 -6
View File
@@ -7,9 +7,14 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/config"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
// userRepo user repository
@@ -28,10 +33,21 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR
// AddUser add user
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
userInfo := &entity.User{}
exist, err := session.Where("username = ?", user.Username).Get(userInfo)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
return nil, errors.InternalServer(reason.UsernameDuplicate)
}
_, err = session.Insert(user)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil, nil
})
return
}
@@ -125,6 +141,11 @@ func (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *e
exist, err = ur.data.DB.Where("id = ?", userID).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
if err != nil {
return nil, false, err
}
return
}
@@ -135,6 +156,7 @@ func (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.U
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
return list, nil
}
@@ -144,6 +166,11 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
exist, err = ur.data.DB.Where("username = ?", username).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
if err != nil {
return nil, false, err
}
return
}
@@ -159,11 +186,121 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
return
}
func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.User, 0)
count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
count, err = ur.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
if original == nil {
return nil
}
uc, ok := plugin.GetUserCenter()
if !ok {
return nil
}
userInfo := &entity.UserExternalLogin{}
session := data.DB.Where("user_id = ?", original.ID)
session.Where("provider = ?", uc.Info().SlugName)
exist, err := session.Get(userInfo)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil
}
userCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID)
if err != nil {
log.Error(err)
return errors.BadRequest(reason.UserNotFound).WithError(err).WithStack()
}
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
if original.Username != userCenterBasicUserInfo.Username {
log.Warnf("user %s username is inconsistent with user center", original.ID)
}
decorateByUserCenterUser(original, userCenterBasicUserInfo)
return nil
}
func tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) {
uc, ok := plugin.GetUserCenter()
if !ok {
return
}
ids := make([]string, 0)
originalUserIDMapping := make(map[string]*entity.User, 0)
for _, user := range original {
originalUserIDMapping[user.ID] = user
ids = append(ids, user.ID)
}
userExternalLoginList := make([]*entity.UserExternalLogin, 0)
session := data.DB.Where("provider = ?", uc.Info().SlugName)
session.In("user_id", ids)
err := session.Find(&userExternalLoginList)
if err != nil {
log.Error(err)
return
}
userExternalIDs := make([]string, 0)
originalExternalIDMapping := make(map[string]*entity.User, 0)
for _, u := range userExternalLoginList {
originalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID]
userExternalIDs = append(userExternalIDs, u.ExternalID)
}
if len(userExternalIDs) == 0 {
return
}
ucUsers, err := uc.UserList(userExternalIDs)
if err != nil {
log.Errorf("get user list from user center failed: %v, %v", err, userExternalIDs)
return
}
for _, ucUser := range ucUsers {
decorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser)
}
}
func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) {
if original == nil || ucUser == nil {
return
}
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
if original.Username != ucUser.Username {
log.Warnf("user %s username is inconsistent with user center", original.ID)
}
if len(ucUser.DisplayName) > 0 {
original.DisplayName = ucUser.DisplayName
}
if len(ucUser.Email) > 0 {
original.EMail = ucUser.Email
}
if len(ucUser.Avatar) > 0 {
original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString()
}
if len(ucUser.Mobile) > 0 {
original.Mobile = ucUser.Mobile
}
if len(ucUser.Bio) > 0 {
original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML
}
// If plugin enable rank agent, use rank from user center.
if plugin.RankAgentEnabled() {
original.Rank = ucUser.Rank
}
if ucUser.Status != plugin.UserStatusAvailable {
original.Status = int(ucUser.Status)
}
}
@@ -0,0 +1,94 @@
package user_external_login
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/segmentfault/pacman/errors"
)
type userExternalLoginRepo struct {
data *data.Data
}
// NewUserExternalLoginRepo new repository
func NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo {
return &userExternalLoginRepo{
data: data,
}
}
// AddUserExternalLogin add external login information
func (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) {
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateInfo update user info
func (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) {
_, err = ur.data.DB.ID(userInfo.ID).Update(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetByExternalID get by external ID
func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) (
userInfo *entity.UserExternalLogin, exist bool, err error) {
userInfo = &entity.UserExternalLogin{}
exist, err = ur.data.DB.Where("external_id = ?", externalID).Where("provider = ?", provider).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserExternalLoginList get by external ID
func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) (
resp []*entity.UserExternalLogin, err error) {
resp = make([]*entity.UserExternalLogin, 0)
err = ur.data.DB.Where("user_id = ?", userID).Find(&resp)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// DeleteUserExternalLogin delete external user login info
func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) {
cond := &entity.UserExternalLogin{}
_, err = ur.data.DB.Where("user_id = ? AND external_id = ?", userID, externalID).Delete(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) {
cacheData, _ := json.Marshal(info)
return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key,
string(cacheData), constant.ConnectorUserExternalInfoCacheTime)
}
// GetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(
ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) {
res, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)
if err != nil {
return info, err
}
_ = json.Unmarshal([]byte(res), &info)
return info, nil
}
+41 -26
View File
@@ -1,6 +1,7 @@
package router
import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller_admin"
"github.com/gin-gonic/gin"
@@ -31,6 +32,7 @@ type AnswerAPIRouter struct {
uploadController *controller.UploadController
activityController *controller.ActivityController
roleController *controller_admin.RoleController
pluginController *controller_admin.PluginController
}
func NewAnswerAPIRouter(
@@ -58,6 +60,7 @@ func NewAnswerAPIRouter(
uploadController *controller.UploadController,
activityController *controller.ActivityController,
roleController *controller_admin.RoleController,
pluginController *controller_admin.PluginController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -84,6 +87,7 @@ func NewAnswerAPIRouter(
uploadController: uploadController,
activityController: activityController,
roleController: roleController,
pluginController: pluginController,
}
}
@@ -97,37 +101,38 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
// user
r.POST("/user/login/email", a.userController.UserEmailLogin)
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
r.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
r.POST("/user/email/verification", a.userController.UserVerifyEmail)
r.PUT("/user/email", a.userController.UserChangeEmailVerify)
r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/password/reset", a.userController.RetrievePassWord)
r.POST("/user/password/replacement", a.userController.UseRePassWord)
r.GET("/user/info", a.userController.GetUserInfoByUserID)
r.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
routerGroup := r.Group("", middleware.BanAPIForUserCenter)
routerGroup.POST("/user/login/email", a.userController.UserEmailLogin)
routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail)
routerGroup.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail)
routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify)
routerGroup.GET("/user/action/record", a.userController.ActionRecord)
routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord)
routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
}
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/user/logout", a.userController.UserLogout)
r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
r.POST("/user/email/change/code", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend)
r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
r.GET("/user/ranking", a.userController.UserRanking)
//answer
r.GET("/answer/info", a.answerController.Get)
r.GET("/answer/page", a.answerController.AnswerList)
r.GET("/personal/answer/page", a.questionController.UserAnswerList)
r.GET("/personal/answer/page", a.questionController.PersonalAnswerPage)
//question
r.GET("/question/info", a.questionController.GetQuestion)
r.GET("/question/page", a.questionController.QuestionPage)
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
r.GET("/personal/qa/top", a.questionController.UserTop)
r.GET("/personal/question/page", a.questionController.UserList)
r.GET("/personal/question/page", a.questionController.PersonalQuestionPage)
// comment
r.GET("/comment/page", a.commentController.GetCommentWithPage)
@@ -182,7 +187,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// collection
r.POST("/collection/switch", a.collectionController.CollectionSwitch)
r.GET("/personal/collection/page", a.questionController.UserCollectionList)
r.GET("/personal/collection/page", a.questionController.PersonalCollectionPage)
// question
r.POST("/question", a.questionController.AddQuestion)
@@ -201,7 +206,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.DELETE("/answer", a.answerController.RemoveAnswer)
// user
r.PUT("/user/password", a.userController.UserModifyPassWord)
r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/notice/set", a.userController.UserNoticeSet)
@@ -257,29 +262,39 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
// siteinfo
r.GET("/siteinfo/general", a.siteInfoController.GetGeneral)
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
r.PUT("/siteinfo/seo", a.siteInfoController.UpdateSeo)
r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
r.GET("/siteinfo/users", a.siteInfoController.GetSiteUsers)
r.PUT("/siteinfo/users", a.siteInfoController.UpdateSiteUsers)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
r.GET("/setting/privileges", a.siteInfoController.GetPrivilegesConfig)
r.PUT("/setting/privileges", a.siteInfoController.UpdatePrivilegesConfig)
// dashboard
r.GET("/dashboard", a.dashboardController.DashboardInfo)
// roles
r.GET("/roles", a.roleController.GetRoleList)
// plugin
r.GET("/plugins", a.pluginController.GetPluginList)
r.PUT("/plugin/status", a.pluginController.UpdatePluginStatus)
r.GET("/plugin/config", a.pluginController.GetPluginConfig)
r.PUT("/plugin/config", a.pluginController.UpdatePluginConfig)
}
+50
View File
@@ -0,0 +1,50 @@
package router
import (
"github.com/answerdev/answer/internal/controller"
"github.com/gin-gonic/gin"
)
type PluginAPIRouter struct {
connectorController *controller.ConnectorController
userCenterController *controller.UserCenterController
}
func NewPluginAPIRouter(
connectorController *controller.ConnectorController,
userCenterController *controller.UserCenterController,
) *PluginAPIRouter {
return &PluginAPIRouter{
connectorController: connectorController,
userCenterController: userCenterController,
}
}
func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
// connector plugin
connectorController := pr.connectorController
r.GET(controller.ConnectorLoginRouterPrefix+":name", connectorController.ConnectorLoginDispatcher)
r.GET(controller.ConnectorRedirectRouterPrefix+":name", connectorController.ConnectorRedirectDispatcher)
r.GET("/connector/info", connectorController.ConnectorsInfo)
r.POST("/connector/binding/email", connectorController.ExternalLoginBindingUserSendEmail)
// user center plugin
r.GET("/user-center/agent", pr.userCenterController.UserCenterAgent)
r.GET("/user-center/personal/branding", pr.userCenterController.UserCenterPersonalBranding)
r.GET(controller.UserCenterLoginRouter, pr.userCenterController.UserCenterLoginRedirect)
r.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect)
r.GET("/user-center/login/callback", pr.userCenterController.UserCenterLoginCallback)
r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback)
}
func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) {
connectorController := pr.connectorController
r.GET("/connector/user/info", connectorController.ConnectorsUserInfo)
r.DELETE("/connector/user/unbinding", connectorController.ExternalLoginUnbinding)
r.GET("/user-center/user/settings", pr.userCenterController.UserCenterUserSettings)
}
func (pr *PluginAPIRouter) RegisterAuthAdminConnectorRouter(r *gin.RouterGroup) {
r.GET("/user-center/agent", pr.userCenterController.UserCenterAdminFunctionAgent)
}
+8 -1
View File
@@ -3,4 +3,11 @@ package router
import "github.com/google/wire"
// ProviderSetRouter is providers.
var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter, NewTemplateRouter)
var ProviderSetRouter = wire.NewSet(
NewAnswerAPIRouter,
NewSwaggerRouter,
NewStaticRouter,
NewUIRouter,
NewTemplateRouter,
NewPluginAPIRouter,
)
+15
View File
@@ -0,0 +1,15 @@
package schema
type ConnectorInfoResp struct {
Name string `json:"name"`
Icon string `json:"icon"`
Link string `json:"link"`
}
type ConnectorUserInfoResp struct {
Name string `json:"name"`
Icon string `json:"icon"`
Link string `json:"link"`
Binding bool `json:"binding"`
ExternalID string `json:"external_id"`
}
+11 -8
View File
@@ -3,18 +3,21 @@ package schema
import "encoding/json"
const (
AccountActivationSourceType SourceType = "account-activation"
PasswordResetSourceType SourceType = "password-reset"
ConfirmNewEmailSourceType SourceType = "password-reset"
UnsubscribeSourceType SourceType = "unsubscribe"
AccountActivationSourceType EmailSourceType = "account-activation"
PasswordResetSourceType EmailSourceType = "password-reset"
ConfirmNewEmailSourceType EmailSourceType = "password-reset"
UnsubscribeSourceType EmailSourceType = "unsubscribe"
BindingSourceType EmailSourceType = "binding"
)
type SourceType string
type EmailSourceType string
type EmailCodeContent struct {
SourceType SourceType `json:"source_type"`
Email string `json:"e_mail"`
UserID string `json:"user_id"`
SourceType EmailSourceType `json:"source_type"`
Email string `json:"e_mail"`
UserID string `json:"user_id"`
// Used for third-party login account binding
BindingKey string `json:"binding_key"`
}
func (r *EmailCodeContent) ToJSONString() string {
+141
View File
@@ -0,0 +1,141 @@
package schema
import (
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
)
const (
PluginStatusActive PluginStatus = "active"
PluginStatusInactive PluginStatus = "inactive"
)
type PluginStatus string
type GetPluginListReq struct {
Status PluginStatus `form:"status"`
HaveConfig bool `form:"have_config"`
}
type GetPluginListResp struct {
Name string `json:"name"`
SlugName string `json:"slug_name"`
Description string `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
HaveConfig bool `json:"have_config"`
Link string `json:"link"`
}
type UpdatePluginStatusReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
Enabled bool `json:"enabled"`
}
type GetPluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"`
}
type GetPluginConfigResp struct {
Name string `json:"name"`
SlugName string `json:"slug_name"`
Description string `json:"description"`
Version string `json:"version"`
ConfigFields []ConfigField `json:"config_fields"`
}
func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) {
for _, field := range fields {
configField := ConfigField{
Name: field.Name,
Type: string(field.Type),
Title: field.Title.Translate(ctx),
Description: field.Description.Translate(ctx),
Required: field.Required,
Value: field.Value,
UIOptions: ConfigFieldUIOptions{
Rows: field.UIOptions.Rows,
InputType: string(field.UIOptions.InputType),
Variant: field.UIOptions.Variant,
},
}
configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)
if field.UIOptions.Action != nil {
uiOptionAction := &UIOptionAction{
Url: field.UIOptions.Action.Url,
Method: field.UIOptions.Action.Method,
}
if field.UIOptions.Action.Loading != nil {
uiOptionAction.Loading = &LoadingAction{
Text: field.UIOptions.Action.Loading.Text.Translate(ctx),
State: string(field.UIOptions.Action.Loading.State),
}
}
if field.UIOptions.Action.OnComplete != nil {
uiOptionAction.OnCompleteAction = &OnCompleteAction{
ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,
RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig,
}
}
configField.UIOptions.Action = uiOptionAction
}
for _, option := range field.Options {
configField.Options = append(configField.Options, ConfigFieldOption{
Label: option.Label.Translate(ctx),
Value: option.Value,
})
}
g.ConfigFields = append(g.ConfigFields, configField)
}
}
type ConfigField struct {
Name string `json:"name"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
Required bool `json:"required"`
Value any `json:"value"`
UIOptions ConfigFieldUIOptions `json:"ui_options"`
Options []ConfigFieldOption `json:"options,omitempty"`
}
type ConfigFieldUIOptions struct {
Placeholder string `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType string `json:"input_type,omitempty"`
Label string `json:"label,omitempty"`
Action *UIOptionAction `json:"action,omitempty"`
Variant string `json:"variant,omitempty"`
Text string `json:"text,omitempty"`
}
type ConfigFieldOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
type UIOptionAction struct {
Url string `json:"url"`
Method string `json:"method,omitempty"`
Loading *LoadingAction `json:"loading,omitempty"`
OnCompleteAction *OnCompleteAction `json:"on_complete,omitempty"`
}
type LoadingAction struct {
Text string `json:"text"`
State string `json:"state"`
}
type OnCompleteAction struct {
ToastReturnMessage bool `json:"toast_return_message"`
RefreshFormConfig bool `json:"refresh_form_config"`
}
type UpdatePluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
ConfigFields map[string]any `json:"config_fields"`
}
+35
View File
@@ -0,0 +1,35 @@
package schema
type UserCenterAgentResp struct {
Enabled bool `json:"enabled"`
AgentInfo *AgentInfo `json:"agent_info"`
}
type AgentInfo struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
ControlCenterItems []*ControlCenter `json:"control_center"`
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
}
type ControlCenter struct {
Name string `json:"name"`
Label string `json:"label"`
Url string `json:"url"`
}
type UserCenterPersonalBranding struct {
Enabled bool `json:"enabled"`
PersonalBranding []*PersonalBranding `json:"personal_branding"`
}
type PersonalBranding struct {
Icon string `json:"icon"`
Name string `json:"name"`
Label string `json:"label"`
Url string `json:"url"`
}
+23
View File
@@ -297,6 +297,7 @@ type QuestionPageReq struct {
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
InDays int `validate:"omitempty,min=1" form:"in_days"`
LoginUserID string `json:"-"`
UserIDBeSearched string `json:"-"`
@@ -374,3 +375,25 @@ type SiteMapQuestionInfo struct {
Title string `json:"title"`
UpdateTime string `json:"time"`
}
type PersonalQuestionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
LoginUserID string `json:"-"`
}
type PersonalAnswerPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
LoginUserID string `json:"-"`
}
type PersonalCollectionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
UserID string `json:"-"`
}
+109 -9
View File
@@ -6,6 +6,7 @@ import (
"net/mail"
"net/url"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
@@ -42,9 +43,8 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
// SiteInterfaceReq site interface request
type SiteInterfaceReq struct {
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
}
// SiteBrandingReq site branding request
@@ -92,18 +92,32 @@ type GetSiteLegalInfoResp struct {
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
}
// SiteUsersReq site users config request
type SiteUsersReq struct {
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
AllowUpdateDisplayName bool `form:"allow_update_display_name" json:"allow_update_display_name"`
AllowUpdateUsername bool `form:"allow_update_username" json:"allow_update_username"`
AllowUpdateAvatar bool `form:"allow_update_avatar" json:"allow_update_avatar"`
AllowUpdateBio bool `form:"allow_update_bio" json:"allow_update_bio"`
AllowUpdateWebsite bool `form:"allow_update_website" json:"allow_update_website"`
AllowUpdateLocation bool `form:"allow_update_location" json:"allow_update_location"`
}
// SiteLoginReq site login request
type SiteLoginReq struct {
AllowNewRegistrations bool `json:"allow_new_registrations"`
LoginRequired bool `json:"login_required"`
AllowNewRegistrations bool `json:"allow_new_registrations"`
AllowEmailRegistrations bool `json:"allow_email_registrations"`
LoginRequired bool `json:"login_required"`
AllowEmailDomains []string `json:"allow_email_domains"`
}
// SiteCustomCssHTMLReq site custom css html
type SiteCustomCssHTMLReq struct {
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
CustomSideBar string `validate:"omitempty,gt=0,lte=65536" json:"custom_sidebar"`
}
// SiteThemeReq site theme config
@@ -127,6 +141,9 @@ type SiteLoginResp SiteLoginReq
// SiteCustomCssHTMLResp site custom css html response
type SiteCustomCssHTMLResp SiteCustomCssHTMLReq
// SiteUsersResp site users response
type SiteUsersResp SiteUsersReq
// SiteThemeResp site theme response
type SiteThemeResp struct {
ThemeOptions []*ThemeOption `json:"theme_options"`
@@ -169,6 +186,7 @@ type SiteInfoResp struct {
Theme *SiteThemeResp `json:"theme"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
SiteSeo *SiteSeoReq `json:"site_seo"`
SiteUsers *SiteUsersResp `json:"site_users"`
Version string `json:"version"`
Revision string `json:"revision"`
}
@@ -235,3 +253,85 @@ type GetManifestJsonResp struct {
ThemeColor string `json:"theme_color"`
BackgroundColor string `json:"background_color"`
}
const (
// PrivilegeLevel1 low
PrivilegeLevel1 PrivilegeLevel = 1
// PrivilegeLevel2 medium
PrivilegeLevel2 PrivilegeLevel = 2
// PrivilegeLevel3 high
PrivilegeLevel3 PrivilegeLevel = 3
)
type PrivilegeLevel int
// GetPrivilegesConfigResp get privileges config response
type GetPrivilegesConfigResp struct {
Options []*PrivilegeOption `json:"options"`
SelectedLevel PrivilegeLevel `json:"selected_level"`
}
// PrivilegeOption privilege option
type PrivilegeOption struct {
Level PrivilegeLevel `json:"level"`
LevelDesc string `json:"level_desc"`
Privileges []*constant.Privilege `json:"privileges"`
}
// UpdatePrivilegesConfigReq update privileges config request
type UpdatePrivilegesConfigReq struct {
Level PrivilegeLevel `validate:"required,min=1,max=3" json:"level"`
}
var (
DefaultPrivilegeOptions []*PrivilegeOption
privilegeOptionsLevelMapping = map[string][]int{
constant.RankQuestionAddKey: {1, 1, 1},
constant.RankAnswerAddKey: {1, 1, 1},
constant.RankCommentAddKey: {1, 1, 1},
constant.RankReportAddKey: {1, 1, 1},
constant.RankCommentVoteUpKey: {1, 1, 1},
constant.RankLinkUrlLimitKey: {1, 10, 10},
constant.RankQuestionVoteUpKey: {1, 1, 15},
constant.RankAnswerVoteUpKey: {1, 1, 15},
constant.RankQuestionVoteDownKey: {125, 125, 125},
constant.RankAnswerVoteDownKey: {125, 125, 125},
constant.RankTagAddKey: {1, 750, 1500},
constant.RankTagEditKey: {1, 50, 100},
constant.RankQuestionEditKey: {1, 100, 200},
constant.RankAnswerEditKey: {1, 100, 200},
constant.RankQuestionEditWithoutReviewKey: {1, 1000, 2000},
constant.RankAnswerEditWithoutReviewKey: {1, 1000, 2000},
constant.RankQuestionAuditKey: {1, 1000, 2000},
constant.RankAnswerAuditKey: {1, 1000, 2000},
constant.RankTagAuditKey: {1, 2500, 5000},
constant.RankTagEditWithoutReviewKey: {1, 10000, 20000},
constant.RankTagSynonymKey: {1, 10000, 20000},
}
)
func init() {
DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{
Level: PrivilegeLevel1,
LevelDesc: reason.PrivilegeLevel1Desc,
}, &PrivilegeOption{
Level: PrivilegeLevel2,
LevelDesc: reason.PrivilegeLevel2Desc,
}, &PrivilegeOption{
Level: PrivilegeLevel3,
LevelDesc: reason.PrivilegeLevel3Desc,
})
for _, option := range DefaultPrivilegeOptions {
for _, privilege := range constant.RankAllPrivileges {
if len(privilegeOptionsLevelMapping[privilege.Key]) == 0 {
continue
}
option.Privileges = append(option.Privileges, &constant.Privilege{
Label: privilege.Label,
Value: privilegeOptionsLevelMapping[privilege.Key][option.Level-1],
Key: privilege.Key,
})
}
}
}
@@ -0,0 +1,81 @@
package schema
// UserExternalLoginResp user external login resp
type UserExternalLoginResp struct {
BindingKey string `json:"binding_key"`
AccessToken string `json:"access_token"`
// ErrMsg error message, if not empty, means login failed and this message should be displayed.
ErrMsg string `json:"-"`
ErrTitle string `json:"-"`
}
// ExternalLoginBindingUserSendEmailReq external login binding user request
type ExternalLoginBindingUserSendEmailReq struct {
BindingKey string `validate:"required,gt=1,lte=100" json:"binding_key"`
Email string `validate:"required,gt=1,lte=512,email" json:"email"`
// If must is true, whatever email if exists, try to bind user.
// If must is false, when email exist, will only be prompted with a warning.
Must bool `json:"must"`
}
// ExternalLoginBindingUserSendEmailResp external login binding user response
type ExternalLoginBindingUserSendEmailResp struct {
EmailExistAndMustBeConfirmed bool `json:"email_exist_and_must_be_confirmed"`
AccessToken string `json:"access_token"`
}
// ExternalLoginBindingUserReq external login binding user request
type ExternalLoginBindingUserReq struct {
Code string `validate:"required,gt=0,lte=500" json:"code"`
Content string `json:"-"`
}
// ExternalLoginBindingUserResp external login binding user response
type ExternalLoginBindingUserResp struct {
AccessToken string `json:"access_token"`
}
// ExternalLoginUserInfoCache external login user info
type ExternalLoginUserInfoCache struct {
// Third party identification
// e.g. facebook, twitter, instagram
Provider string
// required. The unique user ID provided by the third-party login
ExternalID string
// optional. This name is used preferentially during registration
DisplayName string
// optional. This username is used preferentially during registration
Username string
// optional. If email exist will bind the existing user
Email string
// optional. The avatar URL provided by the third-party login platform
Avatar string
// optional. The original user information provided by the third-party login platform
MetaInfo string
// optional. The bio provided by the third-party login platform
Bio string
}
// ExternalLoginUnbindingReq external login unbinding user
type ExternalLoginUnbindingReq struct {
ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"`
UserID string `json:"-"`
}
// UserCenterUserSettingsResp user center user info response
type UserCenterUserSettingsResp struct {
ProfileSettingAgent UserSettingAgent `json:"profile_setting_agent"`
AccountSettingAgent UserSettingAgent `json:"account_setting_agent"`
}
type UserCenterAdminFunctionAgentResp struct {
AllowCreateUser bool `json:"allow_create_user"`
AllowUpdateUserStatus bool `json:"allow_update_user_status"`
AllowUpdateUserPassword bool `json:"allow_update_user_password"`
AllowUpdateUserRole bool `json:"allow_update_user_role"`
}
type UserSettingAgent struct {
Enabled bool `json:"enabled"`
RedirectURL string `json:"redirect_url"`
}
+33 -22
View File
@@ -4,14 +4,12 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/gravatar"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
// UserVerifyEmailReq user verify email request
@@ -72,6 +70,8 @@ type GetUserResp struct {
RoleID int `json:"role_id"`
// user status
Status string `json:"status"`
// user have password
HavePassword bool `json:"have_password"`
}
func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
@@ -83,11 +83,13 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
if ok {
r.Status = statusShow
}
r.HavePassword = len(userInfo.Pass) > 0
}
type GetUserToSetShowResp struct {
*GetUserResp
Avatar *AvatarInfo `json:"avatar"`
Avatar *AvatarInfo `json:"avatar"`
HavePassword bool `json:"have_password"`
}
func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
@@ -108,6 +110,12 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
r.Avatar = avatarInfo
}
const (
AvatarTypeDefault = "default"
AvatarTypeGravatar = "gravatar"
AvatarTypeCustom = "custom"
)
func FormatAvatarInfo(avatarJson, email string) (res string) {
defer func() {
if constant.DefaultAvatar == "gravatar" && len(res) == 0 {
@@ -124,15 +132,22 @@ func FormatAvatarInfo(avatarJson, email string) (res string) {
return ""
}
switch avatarInfo.Type {
case "gravatar":
case AvatarTypeGravatar:
return avatarInfo.Gravatar
case "custom":
case AvatarTypeCustom:
return avatarInfo.Custom
default:
return ""
}
}
func CustomAvatar(url string) *AvatarInfo {
return &AvatarInfo{
Type: AvatarTypeCustom,
Custom: url,
}
}
// GetUserStatusResp get user status info
type GetUserStatusResp struct {
// user status
@@ -260,14 +275,14 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
return nil, nil
}
// UserModifyPassWordRequest
type UserModifyPassWordRequest struct {
UserID string `json:"-" ` // user_id
OldPass string `json:"old_pass" ` // old password
Pass string `json:"pass" ` // password
type UserModifyPasswordReq struct {
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
UserID string `json:"-"`
AccessToken string `json:"-"`
}
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
@@ -283,7 +298,7 @@ func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorFie
type UpdateInfoRequest struct {
// display_name
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
DisplayName string `validate:"omitempty,gt=0,lte=30" json:"display_name"`
// username
Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
// avatar
@@ -306,17 +321,12 @@ type AvatarInfo struct {
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
}
func (a *AvatarInfo) ToJsonString() string {
data, _ := json.Marshal(a)
return string(data)
}
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(req.Username) > 0 {
if checker.IsInvalidUsername(req.Username) {
errField := &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
}
errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
return nil, nil
}
@@ -399,6 +409,7 @@ type GetOtherUserInfoResp struct {
type UserChangeEmailSendCodeReq struct {
UserVerifyEmailSendReq
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"`
UserID string `json:"-"`
}
+6 -1
View File
@@ -3,6 +3,7 @@ package activity
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/answerdev/answer/internal/base/constant"
@@ -111,7 +112,11 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
item.Username = "N/A"
item.UserDisplayName = "N/A"
} else {
item.UserID = act.UserID
if act.TriggerUserID > 0 {
item.UserID = fmt.Sprintf("%d", act.TriggerUserID)
} else {
item.UserID = act.UserID
}
}
item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)
+3 -3
View File
@@ -476,7 +476,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
msg.ReceiverUserID = answerInfo.UserID
msg.TriggerUserID = answerInfo.UserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.YourAnswerWasDeleted
msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
notice_queue.AddNotification(msg)
return nil
@@ -566,7 +566,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
ObjectID: answerID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.UpdateAnswer
msg.NotificationAction = constant.NotificationUpdateAnswer
notice_queue.AddNotification(msg)
}
@@ -583,7 +583,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
ObjectID: answerID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AnswerTheQuestion
msg.NotificationAction = constant.NotificationAnswerTheQuestion
notice_queue.AddNotification(msg)
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
+18 -5
View File
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/token"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/log"
)
@@ -20,7 +21,7 @@ type AuthRepo interface {
SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)
AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
RemoveUserTokens(ctx context.Context, userID string)
RemoveUserTokens(ctx context.Context, userID string, remainToken string)
}
// AuthService kit service
@@ -42,7 +43,6 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
}
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
if cacheInfo != nil {
log.Debugf("user status updated: %+v", cacheInfo)
userCacheInfo.UserStatus = cacheInfo.UserStatus
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
userCacheInfo.RoleID = cacheInfo.RoleID
@@ -52,6 +52,14 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
return nil, err
}
}
// try to get user status from user center
uc, ok := plugin.GetUserCenter()
if ok && len(userCacheInfo.ExternalID) > 0 {
if userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable {
userCacheInfo.UserStatus = int(userStatus)
}
}
return userCacheInfo, nil
}
@@ -85,9 +93,14 @@ func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessTo
return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)
}
// RemoveUserTokens Log out all users under this user id
func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) {
as.authRepo.RemoveUserTokens(ctx, userID)
// RemoveUserAllTokens Log out all users under this user id
func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) {
as.authRepo.RemoveUserTokens(ctx, userID, "")
}
// RemoveTokensExceptCurrentUser remove all tokens except the current user
func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) {
as.authRepo.RemoveUserTokens(ctx, userID, accessToken)
}
//Admin
+4 -4
View File
@@ -471,7 +471,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.CommentQuestion
msg.NotificationAction = constant.NotificationCommentQuestion
notice_queue.AddNotification(msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
@@ -526,7 +526,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.CommentAnswer
msg.NotificationAction = constant.NotificationCommentAnswer
notice_queue.AddNotification(msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
@@ -578,7 +578,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.ReplyToYou
msg.NotificationAction = constant.NotificationReplyToYou
notice_queue.AddNotification(msg)
}
@@ -599,7 +599,7 @@ func (cs *CommentService) notificationMention(
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.MentionYou
msg.NotificationAction = constant.NotificationMentionYou
notice_queue.AddNotification(msg)
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
}
+14 -23
View File
@@ -162,39 +162,30 @@ func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGen
}
func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) {
ec, err := es.GetEmailConfig()
if err != nil {
return
}
siteinfo, err := es.GetSiteGeneral(ctx)
emailConfig, err := es.GetEmailConfig()
if err != nil {
return
}
siteInfo, err := es.GetSiteGeneral(ctx)
if err != nil {
return
}
templateData := RegisterTemplateData{
SiteName: siteinfo.Name, RegisterUrl: registerUrl,
}
tmpl, err := template.New("register_title").Parse(ec.RegisterTitle)
if err != nil {
return "", "", err
}
titleBuf := &bytes.Buffer{}
bodyBuf := &bytes.Buffer{}
err = tmpl.Execute(titleBuf, templateData)
if err != nil {
return "", "", err
SiteName: siteInfo.Name,
RegisterUrl: registerUrl,
}
tmpl, err = template.New("register_body").Parse(ec.RegisterBody)
title, err = es.parseTemplateData(emailConfig.RegisterTitle, templateData)
if err != nil {
return "", "", err
}
err = tmpl.Execute(bodyBuf, templateData)
if err != nil {
return "", "", err
return "", "", fmt.Errorf("email template parse error: %s", err)
}
return titleBuf.String(), bodyBuf.String(), nil
body, err = es.parseTemplateData(emailConfig.RegisterBody, templateData)
if err != nil {
return "", "", fmt.Errorf("email template parse error: %s", err)
}
return title, body, nil
}
func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) {
@@ -7,14 +7,15 @@ import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
@@ -127,35 +128,47 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
if err != nil {
return nil, err
}
resp, err = ns.formatNotificationPage(ctx, notifications)
if err != nil {
return nil, err
}
return pager.NewPageModel(total, resp), nil
}
func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (
resp []*schema.NotificationContent, err error) {
lang := handler.GetLangByCtx(ctx)
for _, notificationInfo := range notifications {
item := &schema.NotificationContent{}
err := json.Unmarshal([]byte(notificationInfo.Content), item)
if err != nil {
if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil {
log.Error("NotificationContent Unmarshal Error", err.Error())
continue
}
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
item.ID = notificationInfo.ID
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
if notificationInfo.IsRead == schema.NotificationRead {
item.IsRead = true
// If notification is downvote, the user info is not needed.
if item.NotificationAction == constant.NotificationDownVotedTheQuestion ||
item.NotificationAction == constant.NotificationDownVotedTheAnswer {
item.UserInfo = nil
}
answerID, ok := item.ObjectInfo.ObjectMap["answer"]
if ok {
item.ID = notificationInfo.ID
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
item.IsRead = notificationInfo.IsRead == schema.NotificationRead
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
if item.ObjectInfo.ObjectID == answerID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
questionID, ok := item.ObjectInfo.ObjectMap["question"]
if ok {
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok {
if item.ObjectInfo.ObjectID == questionID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
resp = append(resp, item)
}
return pager.NewPageModel(total, resp), nil
return resp, nil
}
@@ -15,6 +15,7 @@ import (
"github.com/answerdev/answer/internal/service/object_info"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/goccy/go-json"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
@@ -82,7 +83,9 @@ func (ns *NotificationCommon) HandleNotification() {
// ObjectInfo.ObjectID
// ObjectInfo.ObjectType
func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error {
if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() {
return nil
}
req := &schema.NotificationContent{
TriggerUserID: msg.TriggerUserID,
ReceiverUserID: msg.ReceiverUserID,
@@ -190,10 +193,10 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
if msg.NoNeedPushAllFollow {
return
}
if msg.NotificationAction != constant.UpdateQuestion &&
msg.NotificationAction != constant.AnswerTheQuestion &&
msg.NotificationAction != constant.UpdateAnswer &&
msg.NotificationAction != constant.AcceptAnswer {
if msg.NotificationAction != constant.NotificationUpdateQuestion &&
msg.NotificationAction != constant.NotificationAnswerTheQuestion &&
msg.NotificationAction != constant.NotificationUpdateAnswer &&
msg.NotificationAction != constant.NotificationAcceptAnswer {
return
}
condObjectID := msg.ObjectID
@@ -10,10 +10,10 @@ const (
QuestionReopen = "question.reopen"
QuestionVoteUp = "question.vote_up"
QuestionVoteDown = "question.vote_down"
QuestionPin = "question.pin" //Top the question
QuestionUnPin = "question.unpin" //untop the question
QuestionHide = "question.hide" //hide the question
QuestionShow = "question.show" //show the question
QuestionPin = "question.pin"
QuestionUnPin = "question.unpin"
QuestionHide = "question.hide"
QuestionShow = "question.show"
AnswerAdd = "answer.add"
AnswerEdit = "answer.edit"
AnswerEditWithoutReview = "answer.edit_without_review"
@@ -0,0 +1,80 @@
package plugin_common
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type PluginConfigRepo interface {
SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error)
GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error)
}
// PluginCommonService user service
type PluginCommonService struct {
configRepo config.ConfigRepo
pluginConfigRepo PluginConfigRepo
}
// NewPluginCommonService new report service
func NewPluginCommonService(
pluginConfigRepo PluginConfigRepo,
configRepo config.ConfigRepo) *PluginCommonService {
// init plugin status
pluginStatus, err := configRepo.GetString(constant.PluginStatus)
if err != nil {
log.Error(err)
} else {
if err := plugin.StatusManager.UnmarshalJSON([]byte(pluginStatus)); err != nil {
log.Error(err)
}
}
// init plugin config
pluginConfigs, err := pluginConfigRepo.GetPluginConfigAll(context.Background())
if err != nil {
log.Error(err)
} else {
for _, pluginConfig := range pluginConfigs {
err := plugin.CallConfig(func(fn plugin.Config) error {
if fn.Info().SlugName == pluginConfig.PluginSlugName {
return fn.ConfigReceiver([]byte(pluginConfig.Value))
}
return nil
})
if err != nil {
log.Errorf("parse plugin config failed: %s %v", pluginConfig.PluginSlugName, err)
}
}
}
return &PluginCommonService{
configRepo: configRepo,
pluginConfigRepo: pluginConfigRepo,
}
}
// UpdatePluginStatus update plugin status
func (ps *PluginCommonService) UpdatePluginStatus(ctx context.Context) (err error) {
content, err := plugin.StatusManager.MarshalJSON()
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err)
}
return ps.configRepo.SetConfig(constant.PluginStatus, string(content))
}
// UpdatePluginConfig update plugin config
func (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *schema.UpdatePluginConfigReq) (err error) {
configValue, _ := json.Marshal(req.ConfigFields)
return ps.pluginConfigRepo.SavePluginConfig(ctx, req.PluginSlugName, string(configValue))
}
+5
View File
@@ -16,6 +16,7 @@ import (
"github.com/answerdev/answer/internal/service/notification"
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/plugin_common"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/reason"
@@ -32,6 +33,7 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_admin"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/google/wire"
)
@@ -79,4 +81,7 @@ var ProviderSetService = wire.NewSet(
role.NewRoleService,
role.NewUserRoleRelService,
role.NewRolePowerRelService,
user_external_login.NewUserExternalLoginService,
user_external_login.NewUserCenterLoginService,
plugin_common.NewPluginCommonService,
)
+1 -1
View File
@@ -32,7 +32,7 @@ type QuestionRepo interface {
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
questionList []*entity.Question, total int64, err error)
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
+50 -49
View File
@@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
@@ -270,6 +271,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
question.Status = entity.QuestionStatusAvailable
question.RevisionID = "0"
question.CreatedAt = now
question.PostUpdateTime = now
question.Pin = entity.QuestionUnPin
question.Show = entity.QuestionShow
//question.UpdatedAt = nil
@@ -733,70 +735,74 @@ func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobject
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
}
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
userlist := make([]*schema.UserQuestionInfo, 0)
// PersonalQuestionPage get question list by user
func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) (
pageModel *pager.PageModel, err error) {
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return userlist, 0, err
return nil, err
}
if !Exist {
return userlist, 0, nil
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
search := &schema.QuestionPageReq{}
search.OrderCond = order
search.Page = page
search.PageSize = pageSize
search.OrderCond = req.OrderCond
search.Page = req.Page
search.PageSize = req.PageSize
search.UserIDBeSearched = userinfo.ID
search.LoginUserID = loginUserID
questionlist, count, err := qs.GetQuestionPage(ctx, search)
search.LoginUserID = req.LoginUserID
questionList, total, err := qs.GetQuestionPage(ctx, search)
if err != nil {
return userlist, 0, err
return nil, err
}
for _, item := range questionlist {
userQuestionInfoList := make([]*schema.UserQuestionInfo, 0)
for _, item := range questionList {
info := &schema.UserQuestionInfo{}
_ = copier.Copy(info, item)
status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]
if ok {
info.Status = status
}
userlist = append(userlist, info)
userQuestionInfoList = append(userQuestionInfoList, info)
}
return userlist, count, nil
return pager.NewPageModel(total, userQuestionInfoList), nil
}
func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserAnswerInfo, int64, error) {
answerlist := make([]*schema.AnswerInfo, 0)
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) (
pageModel *pager.PageModel, err error) {
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return userAnswerlist, 0, err
return nil, err
}
if !Exist {
return userAnswerlist, 0, nil
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
answersearch := &entity.AnswerSearch{}
answersearch.UserID = userinfo.ID
answersearch.PageSize = pageSize
answersearch.Page = page
if order == "newest" {
answersearch.PageSize = req.PageSize
answersearch.Page = req.Page
if req.OrderCond == "newest" {
answersearch.Order = entity.AnswerSearchOrderByTime
} else {
answersearch.Order = entity.AnswerSearchOrderByDefault
}
questionIDs := make([]string, 0)
answerList, count, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
answerList, total, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
if err != nil {
return userAnswerlist, count, err
return nil, err
}
answerlist := make([]*schema.AnswerInfo, 0)
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
for _, item := range answerList {
answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
answerlist = append(answerlist, answerinfo)
questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))
}
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
if err != nil {
return userAnswerlist, count, err
return nil, err
}
for _, item := range answerlist {
@@ -813,34 +819,29 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
}
}
return userAnswerlist, count, nil
return pager.NewPageModel(total, userAnswerlist), nil
}
func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, pageSize int, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
// PersonalCollectionPage get collection list by user
func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) (
pageModel *pager.PageModel, err error) {
list := make([]*schema.QuestionInfo, 0)
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, loginUserID)
if err != nil {
return list, 0, err
}
if !Exist {
return list, 0, nil
}
collectionSearch := &entity.CollectionSearch{}
collectionSearch.UserID = userinfo.ID
collectionSearch.Page = page
collectionSearch.PageSize = pageSize
collectionlist, count, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
collectionSearch.UserID = req.UserID
collectionSearch.Page = req.Page
collectionSearch.PageSize = req.PageSize
collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
if err != nil {
return list, 0, err
return nil, err
}
questionIDs := make([]string, 0)
for _, item := range collectionlist {
for _, item := range collectionList {
questionIDs = append(questionIDs, item.ObjectID)
}
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID)
if err != nil {
return list, count, err
return nil, err
}
for _, id := range questionIDs {
_, ok := questionMaps[uid.EnShortID(id)]
@@ -853,7 +854,7 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p
}
}
return list, count, nil
return pager.NewPageModel(total, list), nil
}
func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) {
@@ -1001,7 +1002,7 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques
}
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
req.UserIDBeSearched, req.TagID, req.OrderCond)
req.UserIDBeSearched, req.TagID, req.OrderCond, req.InDays)
if err != nil {
return nil, 0, err
}
@@ -1063,7 +1064,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
msg.ReceiverUserID = questionInfo.UserID
msg.TriggerUserID = questionInfo.UserID
msg.ObjectType = constant.QuestionObjectType
msg.NotificationAction = constant.YourQuestionWasDeleted
msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
notice_queue.AddNotification(msg)
return nil
}
+4
View File
@@ -16,6 +16,7 @@ import (
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
@@ -228,6 +229,9 @@ func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRan
// GetRankPersonalWithPage get personal comment list page
func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) (
pageModel *pager.PageModel, err error) {
if plugin.RankAgentEnabled() {
return pager.NewPageModel(0, []string{}), nil
}
if len(req.Username) > 0 {
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
@@ -66,7 +66,7 @@ func (rh *ReportHandle) HandleObject(ctx context.Context, reported *entity.Repor
switch req.FlaggedType {
case reasonDelete:
err = rh.commentRepo.RemoveComment(ctx, objectID)
rh.sendNotification(ctx, reportedUserID, objectID, constant.YourCommentWasDeleted)
rh.sendNotification(ctx, reportedUserID, objectID, constant.NotificationYourCommentWasDeleted)
}
}
return
+1 -1
View File
@@ -209,7 +209,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
ObjectID: answerinfo.ID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.UpdateAnswer
msg.NotificationAction = constant.NotificationUpdateAnswer
notice_queue.AddNotification(msg)
activity_queue.AddActivity(&schema.ActivityMsg{
+119 -44
View File
@@ -3,12 +3,15 @@ package siteinfo
import (
"context"
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
@@ -23,19 +26,23 @@ type SiteInfoService struct {
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
emailService *export.EmailService
tagCommonService *tagcommon.TagCommonService
configRepo config.ConfigRepo
}
func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
resp, err := siteInfoCommonService.GetSiteInterface(context.Background())
if err != nil {
log.Error(err)
} else {
constant.DefaultAvatar = resp.DefaultAvatar
tagCommonService *tagcommon.TagCommonService,
configRepo config.ConfigRepo,
) *SiteInfoService {
usersSiteInfo, _ := siteInfoCommonService.GetSiteUsers(context.Background())
if usersSiteInfo != nil {
constant.DefaultAvatar = usersSiteInfo.DefaultAvatar
}
generalSiteInfo, _ := siteInfoCommonService.GetSiteGeneral(context.Background())
if generalSiteInfo != nil {
constant.DefaultSiteURL = generalSiteInfo.SiteUrl
}
return &SiteInfoService{
@@ -43,6 +50,7 @@ func NewSiteInfoService(
siteInfoCommonService: siteInfoCommonService,
emailService: emailService,
tagCommonService: tagCommonService,
configRepo: configRepo,
}
}
@@ -61,6 +69,11 @@ func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.Sit
return s.siteInfoCommonService.GetSiteBranding(ctx)
}
// GetSiteUsers get site info about users
func (s *SiteInfoService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
return s.siteInfoCommonService.GetSiteUsers(ctx)
}
// GetSiteWrite get site info write
func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
@@ -106,45 +119,32 @@ func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteTh
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
req.FormatSiteUrl()
var (
siteType = "general"
content []byte
)
content, _ = json.Marshal(req)
data := entity.SiteInfo{
Type: siteType,
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeGeneral,
Content: string(content),
Status: 1,
}
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data)
if err == nil {
constant.DefaultSiteURL = req.SiteUrl
}
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
return
}
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
var (
siteType = "interface"
content []byte
)
// check language
if !translator.CheckLanguageIsValid(req.Language) {
err = errors.BadRequest(reason.LangNotFound)
return
}
content, _ = json.Marshal(req)
content, _ := json.Marshal(req)
data := entity.SiteInfo{
Type: siteType,
Type: constant.SiteTypeInterface,
Content: string(content),
}
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
if err == nil {
constant.DefaultAvatar = req.DefaultAvatar
}
return
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterface, &data)
}
// SaveSiteBranding save site branding information
@@ -218,6 +218,21 @@ func (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThe
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data)
}
// SaveSiteUsers save site users
func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUsersReq) (err error) {
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
Content: string(content),
Status: 1,
}
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)
if err == nil {
constant.DefaultAvatar = req.DefaultAvatar
}
return err
}
// GetSMTPConfig get smtp config
func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (
resp *schema.GetSMTPConfigResp, err error,
@@ -253,8 +268,11 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
return
}
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
resp = &schema.SiteSeoResp{}
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
resp = &schema.SiteSeoReq{}
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
return resp, err
}
loginConfig, err := s.GetSiteLogin(ctx)
if err != nil {
log.Error(err)
@@ -265,17 +283,6 @@ func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp,
resp.Robots = "User-agent: *\nDisallow: /"
return resp, nil
}
resp = &schema.SiteSeoResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeSeo)
if err != nil {
log.Error(err)
return resp, nil
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
@@ -302,3 +309,71 @@ func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (e
}
return
}
func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) {
privilege := &schema.UpdatePrivilegesConfigReq{}
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil {
return nil, err
}
resp = &schema.GetPrivilegesConfigResp{
Options: s.translatePrivilegeOptions(ctx),
SelectedLevel: schema.PrivilegeLevel3,
}
if privilege != nil && privilege.Level > 0 {
resp.SelectedLevel = privilege.Level
}
return resp, nil
}
func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context) (options []*schema.PrivilegeOption) {
la := handler.GetLangByCtx(ctx)
for _, option := range schema.DefaultPrivilegeOptions {
op := &schema.PrivilegeOption{
Level: option.Level,
LevelDesc: translator.Tr(la, option.LevelDesc),
}
for _, privilege := range option.Privileges {
op.Privileges = append(op.Privileges, &constant.Privilege{
Key: privilege.Key,
Label: translator.Tr(la, privilege.Label),
Value: privilege.Value,
})
}
options = append(options, op)
}
return
}
func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) {
var chooseOption *schema.PrivilegeOption
for _, option := range schema.DefaultPrivilegeOptions {
if option.Level == req.Level {
chooseOption = option
break
}
}
if chooseOption == nil {
return nil
}
// update site info that user choose which privilege level
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypePrivileges,
Content: string(content),
Status: 1,
}
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePrivileges, data)
if err != nil {
return err
}
// update privilege in config
for _, privilege := range chooseOption.Privileges {
err = s.configRepo.SetConfig(privilege.Key, fmt.Sprintf("%d", privilege.Value))
if err != nil {
return err
}
}
return
}
@@ -43,7 +43,7 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
return nil, err
}
return resp, nil
@@ -52,7 +52,7 @@ func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
return nil, err
}
return resp, nil
@@ -61,7 +61,16 @@ func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *sch
// GetSiteBranding get site info branding
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
resp = &schema.SiteBrandingResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
return nil, err
}
return resp, nil
}
// GetSiteUsers get site info about users
func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
resp = &schema.SiteUsersResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil {
return nil, err
}
return resp, nil
@@ -70,7 +79,7 @@ func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *sche
// GetSiteWrite get site info write
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
return nil, err
}
return resp, nil
@@ -79,7 +88,7 @@ func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.
// GetSiteLegal get site info write
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
return nil, err
}
return resp, nil
@@ -88,7 +97,7 @@ func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.
// GetSiteLogin get site login config
func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
resp = &schema.SiteLoginResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
return nil, err
}
return resp, nil
@@ -97,7 +106,7 @@ func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.
// GetSiteCustomCssHTML get site custom css html config
func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
resp = &schema.SiteCustomCssHTMLResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
return nil, err
}
return resp, nil
@@ -108,7 +117,7 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
resp = &schema.SiteThemeResp{
ThemeOptions: schema.GetThemeOptions,
}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
return nil, err
}
resp.TrTheme(ctx)
@@ -118,13 +127,13 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
// GetSiteSeo get site seo
func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
resp = &schema.SiteSeoReq{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
return nil, err
}
return resp, nil
}
func (s *SiteInfoCommonService) getSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
func (s *SiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)
if err != nil {
return err
+41
View File
@@ -18,10 +18,12 @@ import (
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
exifremove "github.com/scottleedavis/go-exif-remove"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
const (
@@ -72,6 +74,14 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig,
// UploadAvatarFile upload avatar file
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
return "", err
}
if len(url) > 0 {
return url, nil
}
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
_, file, err := ctx.Request.FormFile("file")
@@ -143,6 +153,14 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
return "", err
}
if len(url) > 0 {
return url, nil
}
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
@@ -161,6 +179,14 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
return "", err
}
if len(url) > 0 {
return url, nil
}
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
@@ -204,6 +230,21 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
return url, nil
}
func (us *UploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
url string, err error) {
_ = plugin.CallStorage(func(fn plugin.Storage) error {
resp := fn.UploadFile(ctx, source)
if resp.OriginalError != nil {
log.Errorf("upload file by plugin failed, err: %v", resp.OriginalError)
err = errors.BadRequest("").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err)
} else {
url = resp.FullURL
}
return nil
})
return url, err
}
func Dexif(filepath string, destpath string) error {
img, err := ioutil.ReadFile(filepath)
if err != nil {
+2 -2
View File
@@ -116,7 +116,7 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda
return err
}
us.authService.RemoveUserTokens(ctx, req.UserID)
us.authService.RemoveUserAllTokens(ctx, req.UserID)
return
}
@@ -179,7 +179,7 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.
return err
}
// logout this user
us.authService.RemoveUserTokens(ctx, req.UserID)
us.authService.RemoveUserAllTokens(ctx, req.UserID)
return
}
+43 -8
View File
@@ -2,16 +2,18 @@ package usercommon
import (
"context"
"encoding/hex"
"math/rand"
"strings"
"github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/random"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type UserRepo interface {
@@ -34,12 +36,20 @@ type UserRepo interface {
// UserCommon user service
type UserCommon struct {
userRepo UserRepo
userRepo UserRepo
userRoleService *role.UserRoleRelService
authService *auth.AuthService
}
func NewUserCommon(userRepo UserRepo) *UserCommon {
func NewUserCommon(
userRepo UserRepo,
userRoleService *role.UserRoleRelService,
authService *auth.AuthService,
) *UserCommon {
return &UserCommon{
userRepo: userRepo,
userRepo: userRepo,
userRoleService: userRoleService,
authService: authService,
}
}
@@ -135,9 +145,34 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
if !has {
break
}
bytes := make([]byte, 2)
_, _ = rand.Read(bytes)
suffix = hex.EncodeToString(bytes)
suffix = random.UsernameSuffix()
}
return username + suffix, nil
}
func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) (
accessToken string, userCacheInfo *entity.UserCacheInfo, err error) {
roleID, err := us.userRoleService.GetUserRole(ctx, userID)
if err != nil {
log.Error(err)
}
userCacheInfo = &entity.UserCacheInfo{
UserID: userID,
EmailStatus: emailStatus,
UserStatus: userStatus,
RoleID: roleID,
ExternalID: externalID,
}
accessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return "", nil, err
}
if userCacheInfo.RoleID == role.RoleAdminID {
if err = us.authService.SetAdminUserCacheInfo(ctx, accessToken, &entity.UserCacheInfo{UserID: userID}); err != nil {
return "", nil, err
}
}
return accessToken, userCacheInfo, nil
}
@@ -0,0 +1,277 @@
package user_external_login
import (
"context"
"encoding/json"
"time"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/random"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/log"
)
// UserCenterLoginService user external login service
type UserCenterLoginService struct {
userRepo usercommon.UserRepo
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
userActivity activity.UserActiveActivityRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewUserCenterLoginService new user external login service
func NewUserCenterLoginService(
userRepo usercommon.UserRepo,
userCommonService *usercommon.UserCommon,
userExternalLoginRepo UserExternalLoginRepo,
userActivity activity.UserActiveActivityRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
) *UserCenterLoginService {
return &UserCenterLoginService{
userRepo: userRepo,
userCommonService: userCommonService,
userExternalLoginRepo: userExternalLoginRepo,
userActivity: userActivity,
siteInfoCommonService: siteInfoCommonService,
}
}
func (us *UserCenterLoginService) ExternalLogin(
ctx context.Context, userCenter plugin.UserCenter, basicUserInfo *plugin.UserCenterBasicUserInfo) (
resp *schema.UserExternalLoginResp, err error) {
if len(basicUserInfo.Email) > 0 {
// check whether site allow register or not
siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx)
if err != nil {
return nil, err
}
if !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) {
log.Debugf("email domain not allowed: %s", basicUserInfo.Email)
return &schema.UserExternalLoginResp{
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError),
}, nil
}
}
oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
userCenter.Info().SlugName, basicUserInfo.ExternalID)
if err != nil {
return nil, err
}
if exist {
// if user is already a member, login directly
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)
if err != nil {
return nil, err
}
if exist {
// if user is deleted, do not allow login
if oldUserInfo.Status == entity.UserStatusDeleted {
return &schema.UserExternalLoginResp{
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied),
}, nil
}
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
log.Errorf("update user last login date failed: %v", err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
// cache external user info, waiting for user enter email address.
if userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 {
return &schema.UserExternalLoginResp{ErrMsg: "Requires authorized email to login"}, nil
}
oldUserInfo, err := us.registerNewUser(ctx, userCenter.Info().SlugName, basicUserInfo)
if err != nil {
return nil, err
}
us.activeUser(ctx, oldUserInfo)
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider string,
basicUserInfo *plugin.UserCenterBasicUserInfo) (userInfo *entity.User, err error) {
userInfo = &entity.User{}
userInfo.EMail = basicUserInfo.Email
userInfo.DisplayName = basicUserInfo.DisplayName
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, basicUserInfo.Username)
if err != nil {
log.Error(err)
userInfo.Username = random.Username()
}
if len(basicUserInfo.Avatar) > 0 {
avatarInfo := &schema.AvatarInfo{
Type: schema.AvatarTypeCustom,
Custom: basicUserInfo.Avatar,
}
avatar, _ := json.Marshal(avatarInfo)
userInfo.Avatar = string(avatar)
}
userInfo.MailStatus = entity.EmailStatusAvailable
userInfo.Status = entity.UserStatusAvailable
userInfo.LastLoginDate = time.Now()
userInfo.Bio = basicUserInfo.Bio
userInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio)
err = us.userRepo.AddUser(ctx, userInfo)
if err != nil {
return nil, err
}
metaInfo, _ := json.Marshal(basicUserInfo)
newExternalUserInfo := &entity.UserExternalLogin{
UserID: userInfo.ID,
Provider: provider,
ExternalID: basicUserInfo.ExternalID,
MetaInfo: string(metaInfo),
}
err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)
return userInfo, nil
}
func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) {
if err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {
log.Error(err)
}
}
func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) (
resp *schema.UserCenterUserSettingsResp, err error) {
resp = &schema.UserCenterUserSettingsResp{}
userCenter, ok := plugin.GetUserCenter()
if !ok {
return resp, nil
}
// get external login info
externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)
if err != nil {
return nil, err
}
var externalInfo *entity.UserExternalLogin
for _, t := range externalLoginList {
if t.Provider == userCenter.Info().SlugName {
externalInfo = t
}
}
if externalInfo == nil {
return resp, nil
}
settings, err := userCenter.UserSettings(externalInfo.ExternalID)
if err != nil {
log.Error(err)
return resp, nil
}
if len(settings.AccountSettingRedirectURL) > 0 {
resp.AccountSettingAgent = schema.UserSettingAgent{
Enabled: true,
RedirectURL: settings.AccountSettingRedirectURL,
}
}
if len(settings.ProfileSettingRedirectURL) > 0 {
resp.ProfileSettingAgent = schema.UserSettingAgent{
Enabled: true,
RedirectURL: settings.ProfileSettingRedirectURL,
}
}
return resp, nil
}
// UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions
// are turned off due to turning on the User Center plugin.
func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) (
resp *schema.UserCenterAdminFunctionAgentResp, err error) {
resp = &schema.UserCenterAdminFunctionAgentResp{
AllowCreateUser: true,
AllowUpdateUserStatus: true,
AllowUpdateUserPassword: true,
AllowUpdateUserRole: true,
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
return
}
desc := userCenter.Description()
// If user status agent is enabled, admin can not update user status in answer.
resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled
// If original user system is enabled, admin can update user password and role in answer.
resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem
resp.AllowUpdateUserRole = desc.EnabledOriginalUserSystem
resp.AllowCreateUser = desc.EnabledOriginalUserSystem
return resp, nil
}
func (us *UserCenterLoginService) UserCenterPersonalBranding(ctx context.Context, username string) (
resp *schema.UserCenterPersonalBranding, err error) {
resp = &schema.UserCenterPersonalBranding{
PersonalBranding: make([]*schema.PersonalBranding, 0),
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
return
}
userInfo, exist, err := us.userRepo.GetByUsername(ctx, username)
if err != nil {
return nil, err
}
if !exist {
return resp, nil
}
// get external login info
externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userInfo.ID)
if err != nil {
return nil, err
}
var externalInfo *entity.UserExternalLogin
for _, t := range externalLoginList {
if t.Provider == userCenter.Info().SlugName {
externalInfo = t
}
}
if externalInfo == nil {
return resp, nil
}
resp.Enabled = true
branding := userCenter.PersonalBranding(externalInfo.ExternalID)
for _, t := range branding {
resp.PersonalBranding = append(resp.PersonalBranding, &schema.PersonalBranding{
Icon: t.Icon,
Name: t.Name,
Label: t.Label,
Url: t.Url,
})
}
return resp, nil
}
@@ -0,0 +1,351 @@
package user_external_login
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/random"
"github.com/answerdev/answer/pkg/token"
"github.com/answerdev/answer/plugin"
"github.com/google/uuid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type UserExternalLoginRepo interface {
AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error)
UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error)
GetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error)
DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error)
SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error)
GetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error)
}
// UserExternalLoginService user external login service
type UserExternalLoginService struct {
userRepo usercommon.UserRepo
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
emailService *export.EmailService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
userActivity activity.UserActiveActivityRepo
}
// NewUserExternalLoginService new user external login service
func NewUserExternalLoginService(
userRepo usercommon.UserRepo,
userCommonService *usercommon.UserCommon,
userExternalLoginRepo UserExternalLoginRepo,
emailService *export.EmailService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
userActivity activity.UserActiveActivityRepo,
) *UserExternalLoginService {
return &UserExternalLoginService{
userRepo: userRepo,
userCommonService: userCommonService,
userExternalLoginRepo: userExternalLoginRepo,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
userActivity: userActivity,
}
}
// ExternalLogin if user is already a member logged in
func (us *UserExternalLoginService) ExternalLogin(
ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) (
resp *schema.UserExternalLoginResp, err error) {
oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
externalUserInfo.Provider, externalUserInfo.ExternalID)
if err != nil {
return nil, err
}
if exist {
// if user is already a member, login directly
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)
if err != nil {
return nil, err
}
if exist && oldUserInfo.Status != entity.UserStatusDeleted {
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
log.Errorf("update user last login date failed: %v", err)
}
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
if err != nil {
log.Error(err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
// cache external user info, waiting for user enter email address.
if len(externalUserInfo.Email) == 0 {
bindingKey := token.GenerateToken()
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo)
if err != nil {
return nil, err
}
return &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil
}
oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email)
if err != nil {
return nil, err
}
// if user is not a member, register a new user
if !exist {
oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo)
if err != nil {
return nil, err
}
}
// bind external user info to user
err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo)
if err != nil {
return nil, err
}
// If user login with external account and email is exist, active user directly.
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
if err != nil {
log.Error(err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
func (us *UserExternalLoginService) registerNewUser(ctx context.Context,
externalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) {
userInfo = &entity.User{}
userInfo.EMail = externalUserInfo.Email
userInfo.DisplayName = externalUserInfo.DisplayName
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, externalUserInfo.Username)
if err != nil {
log.Error(err)
userInfo.Username = random.Username()
}
if len(externalUserInfo.Avatar) > 0 {
avatarInfo := &schema.AvatarInfo{
Type: schema.AvatarTypeCustom,
Custom: externalUserInfo.Avatar,
}
avatar, _ := json.Marshal(avatarInfo)
userInfo.Avatar = string(avatar)
}
userInfo.MailStatus = entity.EmailStatusToBeVerified
userInfo.Status = entity.UserStatusAvailable
userInfo.LastLoginDate = time.Now()
userInfo.Bio = externalUserInfo.Bio
userInfo.BioHTML = externalUserInfo.Bio
err = us.userRepo.AddUser(ctx, userInfo)
if err != nil {
return nil, err
}
return userInfo, nil
}
func (us *UserExternalLoginService) bindOldUser(ctx context.Context,
externalUserInfo *schema.ExternalLoginUserInfoCache, oldUserInfo *entity.User) (err error) {
oldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
externalUserInfo.Provider,
externalUserInfo.ExternalID)
if err != nil {
return err
}
if exist {
oldExternalUserInfo.MetaInfo = externalUserInfo.MetaInfo
oldExternalUserInfo.UserID = oldUserInfo.ID
err = us.userExternalLoginRepo.UpdateInfo(ctx, oldExternalUserInfo)
} else {
newExternalUserInfo := &entity.UserExternalLogin{
UserID: oldUserInfo.ID,
Provider: externalUserInfo.Provider,
ExternalID: externalUserInfo.ExternalID,
MetaInfo: externalUserInfo.MetaInfo,
}
err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)
}
return err
}
func (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User,
externalUserInfo *schema.ExternalLoginUserInfoCache) (
mailStatus int, err error) {
log.Infof("user %s login with external account, try to active email, old status is %d",
oldUserInfo.ID, oldUserInfo.MailStatus)
// try to active user email
if oldUserInfo.MailStatus == entity.EmailStatusToBeVerified {
err = us.userRepo.UpdateEmailStatus(ctx, oldUserInfo.ID, entity.EmailStatusAvailable)
if err != nil {
return oldUserInfo.MailStatus, err
}
}
// try to update user avatar
if len(externalUserInfo.Avatar) > 0 && len(schema.FormatAvatarInfo(oldUserInfo.Avatar, oldUserInfo.EMail)) == 0 {
avatarInfo := &schema.AvatarInfo{
Type: schema.AvatarTypeCustom,
Custom: externalUserInfo.Avatar,
}
avatar, _ := json.Marshal(avatarInfo)
oldUserInfo.Avatar = string(avatar)
err = us.userRepo.UpdateInfo(ctx, oldUserInfo)
if err != nil {
log.Error(err)
}
}
if err = us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {
return oldUserInfo.MailStatus, err
}
return entity.EmailStatusAvailable, nil
}
// ExternalLoginBindingUserSendEmail Send an email for third-party account login for binding user
func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
ctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) (
resp *schema.ExternalLoginBindingUserSendEmailResp, err error) {
siteGeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx)
if err != nil {
return nil, err
}
resp = &schema.ExternalLoginBindingUserSendEmailResp{}
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey)
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
return nil, errors.BadRequest(reason.UserNotFound)
}
if len(externalLoginInfo.Email) > 0 {
log.Warnf("the binding email has been sent %s", req.BindingKey)
return &schema.ExternalLoginBindingUserSendEmailResp{}, nil
}
userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if exist && !req.Must {
resp.EmailExistAndMustBeConfirmed = true
return resp, nil
}
if !exist {
externalLoginInfo.Email = req.Email
userInfo, err = us.registerNewUser(ctx, externalLoginInfo)
if err != nil {
return nil, err
}
resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID)
if err != nil {
log.Error(err)
}
}
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo)
if err != nil {
return nil, err
}
// send bind confirmation email
data := &schema.EmailCodeContent{
SourceType: schema.BindingSourceType,
Email: req.Email,
UserID: userInfo.ID,
BindingKey: req.BindingKey,
}
code := uuid.NewString()
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", siteGeneral.SiteUrl, code)
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
if err != nil {
return nil, err
}
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
return resp, nil
}
// ExternalLoginBindingUser
// The user clicks on the email link of the bound account and requests the API to bind the user officially
func (us *UserExternalLoginService) ExternalLoginBindingUser(
ctx context.Context, bindingKey string, oldUserInfo *entity.User) (err error) {
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, bindingKey)
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
return errors.BadRequest(reason.UserNotFound)
}
return us.bindOldUser(ctx, externalLoginInfo, oldUserInfo)
}
// GetExternalLoginUserInfoList get external login user info list
func (us *UserExternalLoginService) GetExternalLoginUserInfoList(
ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) {
return us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)
}
// ExternalLoginUnbinding external login unbinding
func (us *UserExternalLoginService) ExternalLoginUnbinding(
ctx context.Context, req *schema.ExternalLoginUnbindingReq) (resp any, err error) {
// If user has only one external login and never set password, he can't unbind it.
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
if len(userInfo.Pass) == 0 {
loginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, req.UserID)
if err != nil {
return nil, err
}
if len(loginList) <= 1 {
return schema.ErrTypeToast, errors.BadRequest(reason.UserExternalLoginUnbindingForbidden)
}
}
return nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID)
}
// CheckUserStatusInUserCenter check user status in user center
func (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) (
valid bool, externalID string, err error) {
// If enable user center plugin, user status should be checked by user center
userCenter, ok := plugin.GetUserCenter()
if !ok {
return true, "", nil
}
userInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID)
if err != nil {
return false, "", err
}
var thisUcUserInfo *entity.UserExternalLogin
for _, t := range userInfoList {
if t.Provider == userCenter.Info().SlugName {
thisUcUserInfo = t
break
}
}
// If this user not login by user center, no need to check user status
if thisUcUserInfo == nil {
return true, "", nil
}
userStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID)
if userStatus == plugin.UserStatusDeleted {
return false, thisUcUserInfo.ExternalID, nil
}
return true, thisUcUserInfo.ExternalID, nil
}
+166 -85
View File
@@ -20,7 +20,9 @@ import (
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/plugin"
"github.com/google/uuid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -31,15 +33,16 @@ import (
// UserService user service
type UserService struct {
userCommonService *usercommon.UserCommon
userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo
activityRepo activity_common.ActivityRepo
serviceConfig *service_config.ServiceConfig
emailService *export.EmailService
authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userCommonService *usercommon.UserCommon
userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo
activityRepo activity_common.ActivityRepo
serviceConfig *service_config.ServiceConfig
emailService *export.EmailService
authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userExternalLoginService *user_external_login.UserExternalLoginService
}
func NewUserService(userRepo usercommon.UserRepo,
@@ -51,22 +54,25 @@ func NewUserService(userRepo usercommon.UserRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService,
userRoleService *role.UserRoleRelService,
userCommonService *usercommon.UserCommon,
userExternalLoginService *user_external_login.UserExternalLoginService,
) *UserService {
return &UserService{
userCommonService: userCommonService,
userRepo: userRepo,
userActivity: userActivity,
activityRepo: activityRepo,
emailService: emailService,
serviceConfig: serviceConfig,
authService: authService,
siteInfoService: siteInfoService,
userRoleService: userRoleService,
userCommonService: userCommonService,
userRepo: userRepo,
userActivity: userActivity,
activityRepo: activityRepo,
emailService: emailService,
serviceConfig: serviceConfig,
authService: authService,
siteInfoService: siteInfoService,
userRoleService: userRoleService,
userExternalLoginService: userExternalLoginService,
}
}
// GetUserInfoByUserID get user info by user id
func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (resp *schema.GetUserToSetShowResp, err error) {
func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (
resp *schema.GetUserToSetShowResp, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
@@ -74,6 +80,9 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
if userInfo.Status == entity.UserStatusDeleted {
return nil, errors.Unauthorized(reason.UnauthorizedError)
}
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
if err != nil {
log.Error(err)
@@ -82,6 +91,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
resp.GetFromUserEntity(userInfo)
resp.AccessToken = token
resp.RoleID = roleID
resp.HavePassword = len(userInfo.Pass) > 0
return resp, nil
}
@@ -112,10 +122,17 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
}
ok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
}
err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID)
if err != nil {
log.Error("UpdateLastLoginDate", err.Error())
log.Errorf("update last login data failed, err: %v", err)
}
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
@@ -130,6 +147,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
RoleID: roleID,
ExternalID: externalID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@@ -171,42 +189,43 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
return nil
}
// UseRePassword
func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) {
// UpdatePasswordWhenForgot update user password when user forgot password
func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) {
data := &schema.EmailCodeContent{}
err = data.FromJSONString(req.Content)
if err != nil {
return nil, errors.BadRequest(reason.EmailVerifyURLExpired)
return errors.BadRequest(reason.EmailVerifyURLExpired)
}
userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email)
if err != nil {
return nil, err
return err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
return errors.BadRequest(reason.UserNotFound)
}
enpass, err := us.encryptPassword(ctx, req.Pass)
if err != nil {
return nil, err
return err
}
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
if err != nil {
return nil, err
return err
}
resp = &schema.GetUserResp{}
return resp, nil
// When the user changes the password, all the current user's tokens are invalid.
us.authService.RemoveUserAllTokens(ctx, userInfo.ID)
return nil
}
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) {
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) {
userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return false, err
}
if !has {
return false, fmt.Errorf("user does not exist")
return false, errors.BadRequest(reason.UserNotFound)
}
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
if !isPass {
return false, nil
}
@@ -215,33 +234,56 @@ func (us *UserService) UserModifyPassWordVerification(ctx context.Context, reque
}
// UserModifyPassword user modify password
func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error {
enpass, err := us.encryptPassword(ctx, request.Pass)
func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error {
enpass, err := us.encryptPassword(ctx, req.Pass)
if err != nil {
return err
}
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return err
}
if !has {
return fmt.Errorf("user does not exist")
if !exist {
return errors.BadRequest(reason.UserNotFound)
}
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
if !isPass {
return fmt.Errorf("the old password verification failed")
return errors.BadRequest(reason.OldPasswordVerificationFailed)
}
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
if err != nil {
return err
}
us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken)
return nil
}
// UpdateInfo update user info
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (
errFields []*validator.FormErrorField, err error) {
if len(req.Username) > 0 {
siteUsers, err := us.siteInfoService.GetSiteUsers(ctx)
if err != nil {
return nil, err
}
if siteUsers.AllowUpdateUsername && len(req.Username) > 0 {
if checker.IsInvalidUsername(req.Username) {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
})
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
if checker.IsReservedUsername(req.Username) {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
})
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
if err != nil {
return nil, err
@@ -253,31 +295,57 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
})
return errFields, errors.BadRequest(reason.UsernameDuplicate)
}
if checker.IsReservedUsername(req.Username) {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
})
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
avatar, err := json.Marshal(req.Avatar)
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
return nil, err
}
userInfo := entity.User{}
userInfo.ID = req.UserID
userInfo.Avatar = string(avatar)
userInfo.DisplayName = req.DisplayName
userInfo.Bio = req.Bio
userInfo.BioHTML = req.BioHTML
userInfo.Location = req.Location
userInfo.Website = req.Website
userInfo.Username = req.Username
err = us.userRepo.UpdateInfo(ctx, &userInfo)
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req, siteUsers)
err = us.userRepo.UpdateInfo(ctx, cond)
return nil, err
}
func (us *UserService) formatUserInfoForUpdateInfo(
oldUserInfo *entity.User, req *schema.UpdateInfoRequest, siteUsersConf *schema.SiteUsersResp) *entity.User {
avatar, _ := json.Marshal(req.Avatar)
userInfo := &entity.User{}
userInfo.DisplayName = oldUserInfo.DisplayName
userInfo.Username = oldUserInfo.Username
userInfo.Avatar = oldUserInfo.Avatar
userInfo.Bio = oldUserInfo.Bio
userInfo.BioHTML = oldUserInfo.BioHTML
userInfo.Website = oldUserInfo.Website
userInfo.Location = oldUserInfo.Location
userInfo.ID = req.UserID
if len(req.DisplayName) > 0 && siteUsersConf.AllowUpdateDisplayName {
userInfo.DisplayName = req.DisplayName
}
if len(req.Username) > 0 && siteUsersConf.AllowUpdateUsername {
userInfo.Username = req.Username
}
if len(avatar) > 0 && siteUsersConf.AllowUpdateAvatar {
userInfo.Avatar = string(avatar)
}
if siteUsersConf.AllowUpdateBio {
userInfo.Bio = req.Bio
userInfo.BioHTML = req.BioHTML
}
if siteUsersConf.AllowUpdateWebsite {
userInfo.Website = req.Website
}
if siteUsersConf.AllowUpdateLocation {
userInfo.Location = req.Location
}
return userInfo
}
func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) {
_, has, err := us.userRepo.GetByEmail(ctx, email)
if err != nil {
@@ -435,50 +503,48 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
if !has {
return nil, errors.BadRequest(reason.UserNotFound)
}
userInfo.MailStatus = entity.EmailStatusAvailable
err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)
if err != nil {
return nil, err
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
userInfo.MailStatus = entity.EmailStatusAvailable
err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)
if err != nil {
return nil, err
}
}
if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil {
log.Error(err)
}
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
// In the case of three-party login, the associated users are bound
if len(data.BindingKey) > 0 {
err = us.userExternalLoginService.ExternalLoginBindingUser(ctx, data.BindingKey, userInfo)
if err != nil {
return nil, err
}
}
accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "")
if err != nil {
log.Error(err)
return nil, err
}
resp = &schema.GetUserResp{}
resp.GetFromUserEntity(userInfo)
userCacheInfo := &entity.UserCacheInfo{
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
RoleID: roleID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
resp.AccessToken = accessToken
// User verified email will update user email status. So user status cache should be updated.
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
return nil, err
}
resp.RoleID = userCacheInfo.RoleID
if resp.RoleID == role.RoleAdminID {
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
return nil, err
}
}
return resp, nil
}
// verifyPassword
// Compare whether the password is correct
func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool {
err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass))
func (us *UserService) verifyPassword(ctx context.Context, loginPass, userPass string) bool {
if len(loginPass) == 0 && len(userPass) == 0 {
return true
}
err := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass))
return err == nil
}
@@ -501,6 +567,15 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
return nil, errors.BadRequest(reason.UserNotFound)
}
// If user's email already verified, then must verify password first.
if userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
resp = append(resp, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),
})
return resp, errors.BadRequest(reason.OldPasswordVerificationFailed)
}
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
@@ -664,6 +739,9 @@ func (us *UserService) UserUnsubscribeEmailNotification(
func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,
userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {
if plugin.RankAgentEnabled() {
return make([]*entity.ActivityUserRankStat, 0), make([]string, 0), nil
}
rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)
if err != nil {
return nil, nil, err
@@ -683,6 +761,9 @@ func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, e
func (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int,
userIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) {
if plugin.RankAgentEnabled() {
return make([]*entity.ActivityUserVoteStat, 0), make([]string, 0), nil
}
voteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit)
if err != nil {
return nil, nil, err
+8 -8
View File
@@ -63,12 +63,12 @@ func NewVoteService(
}
// VoteUp vote up
func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{}
var objectUserID string
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil {
return
}
@@ -80,19 +80,19 @@ func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteRes
}
if dto.IsCancel {
return as.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
} else {
return as.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
}
}
// VoteDown vote down
func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{}
var objectUserID string
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil {
return
}
@@ -104,9 +104,9 @@ func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteR
}
if dto.IsCancel {
return as.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
} else {
return as.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
}
}
+17
View File
@@ -0,0 +1,17 @@
package checker
import "strings"
func EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool {
if len(allowEmailDomains) == 0 {
return true
}
for _, domain := range allowEmailDomains {
if strings.HasSuffix(email, domain) {
return true
}
}
return false
}
+7 -2
View File
@@ -38,6 +38,7 @@ func Markdown2HTML(source string) string {
filter.RequireNoFollowOnLinks(false)
filter.RequireParseableURLs(false)
filter.RequireNoFollowOnFullyQualifiedLinks(false)
filter.AllowElements("kbd")
html = filter.Sanitize(html)
return html
}
@@ -46,7 +47,7 @@ func Markdown2HTML(source string) string {
func Markdown2BasicHTML(source string) string {
content := Markdown2HTML(source)
filter := bluemonday.NewPolicy()
filter.AllowElements("p", "b", "br")
filter.AllowElements("p", "b", "br", "strong", "em")
filter.AllowAttrs("src").OnElements("img")
filter.AddSpaceWhenStrippingTag(true)
content = filter.Sanitize(content)
@@ -87,7 +88,11 @@ func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, n
l := n.Segments.Len()
for i := 0; i < l; i++ {
segment := n.Segments.At(i)
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
if string(source[segment.Start:segment.Stop]) == "<kbd>" || string(source[segment.Start:segment.Stop]) == "</kbd>" {
_, _ = w.Write(segment.Value(source))
} else {
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
}
}
return ast.WalkSkipChildren, nil
}
+18
View File
@@ -0,0 +1,18 @@
package random
import (
"encoding/hex"
"math/rand"
)
func UsernameSuffix() string {
bytes := make([]byte, 2)
_, _ = rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func Username() string {
bytes := make([]byte, 6)
_, _ = rand.Read(bytes)
return hex.EncodeToString(bytes)
}

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