From 62e61ac41fc3a08c750e3eb4e1a5f31779c9ecd5 Mon Sep 17 00:00:00 2001 From: sealos-release-robot Date: Fri, 12 Jul 2024 08:51:43 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=A4=96=20add=20release=20changelog=20?= =?UTF-8?q?using=20rebot.=20(#4887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-5.0.0.md | 130 +++++++++++++++++++++++++++++++++++ CHANGELOG/CHANGELOG.md | 1 + 2 files changed, 131 insertions(+) create mode 100644 CHANGELOG/CHANGELOG-5.0.0.md diff --git a/CHANGELOG/CHANGELOG-5.0.0.md b/CHANGELOG/CHANGELOG-5.0.0.md new file mode 100644 index 00000000000..81702f81dfa --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0.md @@ -0,0 +1,130 @@ +Welcome to the v5.0.0 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* e19143794992d526fcd0775f455b1157f0081a4e: feat(applaunchpad): support file browser for pv (#4674) (@0fatal) +* 18464891fca36fd623da4f17faff2a035343dfa6: feat(desktop):add inviting others into workspace by link (#4712) (@xudaotutou) +* b81ec5fe785a3a233490151a20ec7111e6621612: feat(docs): update private pricing on website (#4846) (@zjy365) +* a1442f9d29d12ee22a68fbfbc4f32aacfecf2110: feat: add a function to retrieve the user's backup size. (#4780) (@nowinkeyy) +* 6db3f2b4161e672f3643041085bc2998243f4270: feat: add cluster type license (#4752) (@lingdie) +* 835e22457b23e9be7700a11734d41c694231818a: feat: add providers workorder app (#4718) (@zjy365) +* 708e977d11b50680dda2b481b05cc17748a5ab51: feat: app cost svc (#4843) (@bxy4543) +* d4040594a3cd148bf832651d435cf68d501db689: feat: cloud host supports GPU (#4728) (@zjy365) +* 4b2d3c0e72851ff5573d0c85f32450aa526df7e3: feat: desktop support controlling the size of the evoked window (#4700) (@zjy365) +* a1982f0868bc4262625ca7b5dbe79fcb051f538d: feat: impl cronjob show envs (#4833) (@zijiren233) +* 469e42c20ffcfd032b40a00f865e0e688defb6f9: feat: implement desktop 5.0 UI design (#4783) (@zjy365) +* f3ab3fae684612df0c9015aa5c0cde9d51150c6e: feat: improve animation smoothness for expanding and collapsing sections (#4847) (@zijiren233) +* b109ffe2f10db323938629dce20cd4a4fe0c0488: feat: optimize resource monitoring logic, performance and fix nil pointer issue (#4827) (@zijiren233) +* 4cba736bcba50803d0083be9bac97b54779fb0f2: feat: support configurable favicon (#4864) (@zjy365) +* 34e74b454b851c9698fe35e1169de29bbb543491: feat: support workorder migrate (#4782) (@zjy365) +* 3859de964955dda9a350ddbc5cda7f9f48c2cc6a: feat: upgrade cost center config. (#4756) (@lingdie) +* 02e12d646d47fdfb3304cadbc412b302ba2c6b80: feat: yearly/monthly billing for cloudserver (#4781) (@zjy365) +* f0e17bc07ea8b512cd0895022e3176425b607b3c: feat:add cloudserver app (#4698) (@zjy365) +* e8950db28214fc9f690b91b3fd8a4fc9c9246d96: feat:docs add recharge event (#4784) (@zjy365) +* 4ad5ea1aef290be85260dc64660b961e19933b32: feat:license app adds system node information (#4845) (@zjy365) +### Bug fixes +* add7316542a417b8ab0b1452c76873be60b38b50: fix(costcenter): remove bank account regex (#4713) (@xudaotutou) +* 9c178471c7e72b5beb2a9736a20b45fb4fda0fe4: fix(costcenter): update amount by the query (#4737) (@xudaotutou) +* 950e6cb44eb03f6e097a501a80b160eefe9017fc: fix: Delete unnecessary logs (#4710) (@ghostloda) +* 74e80c5763bfea005969792b820f4b4366d78714: fix: add nodes after upgrading, the version installed on the newly added nodes is incorrect (#4857) (@yangxggo) +* 0ca8f84aadc152a0fa926fbb761a38dbb61e1433: fix: applaunchpad unhandledRejection promise (#4771) (@zjy365) +* 7c509b14bfefb9eaf54f2827190c226686437c1f: fix: db backup label encodeToHex (#4792) (@zjy365) +* c4af244fa180b58a9b3068b94ebf7d9d92c42dbf: fix: db resource quota i18n (#4865) (@zijiren233) +* 2505b97f4102667c1fd65d583b6cd452ac8516ce: fix: default app (#4859) (@zijiren233) +* e119d8faef5c4bd9aa936e1a53ea1e1f6121a62d: fix: desktop notifications forbidden error (#4789) (@zjy365) +* 9ef2a3ed311bb7696c2836d78984c2776350b675: fix: ensure license issuance fields are of number type (#4873) (@zjy365) +* 8382b4b594e7ae157a77f1903c19aae1bf0de5cf: fix: scaledown processor checker error (#4815) (@ghostloda) +* 9d0437a95d5d27a55535ed911edfbde2507bb302: fix: sealos 5 cannot upgrade k8s from v1.27.x to v1.28.y (#4842) (@yangxggo) +* 134a975b96e7ef7d0ec316b27884585b73586c1e: fix: skip renew certificate (#4880) (@ghostloda) +* b5a1ef63b7b16f180d648fa578cf790bdb410416: fix: upgrade from k8s v1.26.x to v1.27.y, kubelet restart failed (#4850) (@yangxggo) +* 18be6fe4d903f3982900f07c80bea40853c9a109: fix:add check containerd (#4748) (@ghostloda) +* 5968bae3f03db7bf520294c497ca20264b0253d2: fix:cloudserver bandwidth price display (#4734) (@zjy365) +* 7b5fdf221c342f7e65611a5cd706dca78fa53bbb: fix:db podAntiAffinity Preferred (#4701) (@zjy365) +* 733beccad1ff3c7da3b35f92467823db9516d166: fix:fix transfer (#4804) (@xudaotutou) +* 02a7510c6d28faed82904c61eeae3588cf2b023f: fix:fix transfer billing (#4837) (@xudaotutou) +* bca847623a2c821c184d42fcf165c6fc5b8059e6: fix:launchpad checkNetworkPorts (#4724) (@zjy365) +* c723346d6ef16d9cc32feec91c46e4043c14eb1f: fix:launchpad config yaml boolean (#4715) (@zjy365) +* 0707cb5074ca096db4ce2954dd8a521e4524f8d1: fix:template frontend leaves zombie git processes (#4799) (@zjy365) +### Documentation updates +* 29fb771de5298272254b6e04e593cc2e6f4e4e2d: doc(Frontend): Rename READMD.md to README.md (#4755) (@luoling8192) +* 1c6f9500aced8c487e1e4d9ce5e81f385cd109ba: doc: add a doc for deploying Object Storage (#4777) (@nowinkeyy) +* c6e037670683209260d5e0ebb966354c7f08cedf: doc: move objectstorage install doc (#4835) (@nowinkeyy) +### Other work +* b4187c53b61f384ca21d36e4b4b3d1de7688b35a: Add clear mongo log (#4757) (@wallyxjh) +* 0041eedb17805bea35ccd3d4cda725194dc4b778: Add default install desktop apps. (#4838) (@zzjin) +* f04556bed1fe47332d9ad8286f308c4e9bb5db63: Change object storage monitor service from prometheus to vm (#4727) (@nowinkeyy) +* aec5facb360ef39d66029f4677a6751a5335e0b8: Database exceptions monitor (#4830) (@wallyxjh) +* 39926854fa70117cd634873ae0313a97611ff62f: Delete clear mongo log (#4806) (@wallyxjh) +* 1ad3c7185c5a88f9ddf3d7fdad96e141ffd24c62: Feat/ add vms/email debt notification (#4763) (@bxy4543) +* 8cb7b734000b94dc59e0b9a9722c1fbc466562e5: Feat: Cloud Virtual Machine Billing (#4699) (@bxy4543) +* 694ddb97645f30166e57aec31d9ff9ce469ab7c4: Feat: impl cronjob envs (#4821) (@zijiren233) +* afb2584ddcebf4d4c1655209704d1647152866e1: Fix desktop config (#4733) (@lingdie) +* 4d02f7661760c2a5fc140697bd95867bf14ea35e: Fix desktop password salt (#4762) (@lingdie) +* 2138f4656ed70f2d9c4a14ccdc8c7ce1a9ab4c0d: Fix doc link 404. (#4743) (@zzjin) +* 49e6422e89b7a11a47631e62c8291a5df870b0d9: Fix kubepanel typo&link. (#4729) (@zzjin) +* 13bd347c7e33097b304a2f159f08e79a6d517d0b: Fix/costcente svc (#4736) (@bxy4543) +* 18402ae8df78325f7fef7797b71f5ec6a8477885: Fix: remove deprecated rlcp options (#4803) (@zijiren233) +* 182fa028da5c04892f89e077ed95f5d650f8ef27: Fix: switch user register (#4813) (@zijiren233) +* 3b044f328db58acdd0381c0aec7809027e2d2025: Launchpad monitor (#4689) (@wallyxjh) +* 701612bdd0c5bbfe64b605513f4f2de1e6df1f16: Replace 404 doc to official link. (#4800) (@zzjin) +* 16c8524f7b05201aea5d3edcbd10bc41be3827ab: Stop clusters (#4644) (@wallyxjh) +* 372632d16ee8f06110399b00c4a7a7e54e31fcce: Update README.md (#4745) (@fanux) +* a7c1d9df0e86ea64fb54860f661b4c52a6ffec16: Update app menu types. (#4735) (@zzjin) +* 17c2b43ac55b1170a236e043d3ed007e0bb8aa14: Update appcr (#4742) (@lingdie) +* 6758e182d7dfafbb9779dffb072ea3056e1d0c79: Update cockroach version (#4844) (@wallyxjh) +* 469e9c9796df59f332ec006db3bd0c4effc5e68c: Update doc fireboom homepage link. (#4778) (@zzjin) +* 2d62a13d0e882a328c4dc0a1a9dde1f597f5f3bd: Update i18n for applaunchpad (#4851) (@yangchuansheng) +* 78cefc7715b6c0952f079cc6bbc1bd32e96adbcb: Update install.sh (#4791) (@lingdie) +* 7d5f5a154251ef5e5a6d0fda321c6c41e5ca7880: Update install.sh (#4812) (@lingdie) +* 28698d2d81ec0f48265c8d163f5a52f7fb47f012: Update sealos mongo role (#4823) (@wallyxjh) +* 85474f252f28e0392944f16c446b827c85f403d3: Update terminal frontend ,support customize keepalived time. (#4810) (@zzjin) +* c0bb1e72dd2564edef48e3b38d7c84719c76d6c4: add a key for a bucket (#4714) (@nowinkeyy) +* d11c5fd0cd464c37c3fe95e6ae5adc6a5065b0f0: add email debt notification (#4820) (@bxy4543) +* 89ada630ae6aa71fd569bebd2863e3f22da40911: add quotaEnabled and defaultQuota env (#4822) (@nowinkeyy) +* 6f5c5905b32ec33b36fed1b67ffc16ac9a9b227c: add set-cert doc. (#4772) (@nowinkeyy) +* 1cec0811ca614c3c1d6eb20205bda1f1896a6883: change cert duration. (#4768) (@lingdie) +* 7362d807e9c1612f97f1b9151a6701090431e338: change mongo 4.0 to 4.4 (#4708) (@lingdie) +* 47ba1b87d3adec8bbd48b1d1f557a36d1d65172b: docs: Update sealos version in self-hosting docs (#4683) (@yangchuansheng) +* ffd72f2c0f2b0dec391bcb13db5ebc2ee755b00d: docs: add README for license-system-frontend (#4868) (@zjy365) +* 123980df36875ae9a5b6b4fe7b8db02d89fcd515: docs: add blog "How To Deploy and Configure Meilisearch using Docker" (#4854) (@yangchuansheng) +* 24697342c516803b99f3c506cc2676ab887eafc5: docs: update header for landing page (#4875) (@yangchuansheng) +* 63ecc33d3c2cf70d9cdcd042837e9efaf95bb133: docs: update landing page (#4858) (@yangchuansheng) +* 85318ad38d687fd09298075b00353238918a0d7d: docs:remove HomeUserBy component (#4879) (@zjy365) +* 467a675d00994ba8dd7222fe3400ad88a4fd532c: feat(frontend/cloudserver): Add a quantity option when creating the v… (#4852) (@HUAHUAI23) +* daff4ac6f63a3f04938ef813c2d387b848b5aef7: feat. change desktop to use config file. (#4709) (@lingdie) +* c56ce7aebd2dd7eab3fdcb45764b513fdbd6ebbb: fix billing record query with app type (#4731) (@bxy4543) +* 786985e3cced44b14d8c1faa1018d208db671883: fix control-plane component status (#4749) (@ghostloda) +* dd1cfc9221c260fa2e4f0f9d344775f9aaeeb7bc: fix cost center frontend manifests (#4761) (@lingdie) +* 1d0ad5c5213e4915f196f7a9348a33c1e9cb65d2: fix object storage monitor (#4691) (@bxy4543) +* f6b1e76230978671d5ff52be4eb6d1cc232ccedc: fix objectstorage frontend deploy.yaml format error (#4725) (@nowinkeyy) +* a4b7f8a6b89dea9ed23a49952523a5a869c480a1: fix pull cert-manager image in install.sh (#4775) (@lingdie) +* 26739d9a6cee1afa95bae74e63c21a4f1b57ec5c: fix set account create region id (#4739) (@bxy4543) +* 9a4f321c1d19d9d0e58dd1569455f1b41316c091: i18n: update i18n for App Launchpad (#4754) (@yangchuansheng) +* 4960ec3d0fe0450c66bd5d464735468eb2d75672: i18n: update i18n for DBProvider (#4787) (@yangchuansheng) +* 4c3fac74d54699dac47d1e029547b34fce601c00: monitor svc multi nodePort (#4726) (@bxy4543) +* 4b80930b9090afc7384ef6d6b875743a186c4d97: none (#4732) (@nowinkeyy) +* 651a27cea9affa9e527e3075318b8af065026134: opt: costcenter get quota default use current namespace (#4828) (@zijiren233) +* 9bb4c44653a379e2e36e21a5e3f9b700d13c974d: optimize query object storage metric (#4686) (@nowinkeyy) +* a05b86025cc091977f2b7ab53853d42b641cdb05: refactor(costcenter): refactor invoice (#4694) (@xudaotutou) +* 2176fbb8b4eb6d24b1f7582aa34703af1f0e4583: refactor: adjusted license billing based on cluster size (#4853) (@zjy365) +* ef4409df911bb1565242b4e9dbc2b705888e6235: refactor: improve the prompt when deleting an app (#4786) (@zjy365) +* 216d68e62195bc3066290c3a1e2fe3b1fad217a8: refactor: rename Signin directory to Login and fix file name case issues (#4870) (@zjy365) +* 02327d53e0b9a13d603db249bd50637217f41e39: release: v5.0.0 release, update sealos version in install.sh. (#4882) (@lingdie) +* 2e7eb9f07c225839dc4b02acadb03e3f6183f54d: revert: remove recharge event (#4788) (@zjy365) +* 2fe621674f37a307d8af515ac7d316b3f6650065: style: launchpad displays monitoring values (#4688) (@zjy365) +* 0e1693c32ac098a0b78d793ab334a98246f03d10: styles: Updated UI design for dbprovider frontend (#4747) (@zjy365) +* a58a5b159dcfe74381ce5e85abff9f95149a8eca: styles: launchpad AppStatusTag & workorder appendixs (#4740) (@zjy365) +* f164e9dd48e8a8f6b4ac229dac96e7036854c29a: temporary hiding suspends kb package import (#4753) (@bxy4543) +* 0992fc04dfb4279c198d6e743719a11334c9b5e5: update cvm interface (#4798) (@bxy4543) +* 7974ba511348f0e1a1acb472e694c460744a9cb4: 为kubepanel network 添加service (#4705) (@bearslyricattack) +* 4535a7fcaceeeb9de1b61018e013abf3674db0a0: 🤖 add release changelog using rebot. (#4681) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta5...v5.0.0 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG.md b/CHANGELOG/CHANGELOG.md index 2a288f8d08b..f4390057e1b 100644 --- a/CHANGELOG/CHANGELOG.md +++ b/CHANGELOG/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - [CHANGELOG-5.0.0-beta1.md](./CHANGELOG-5.0.0-beta1.md) - [CHANGELOG-5.0.0-alpha2.md](./CHANGELOG-5.0.0-alpha2.md) - [CHANGELOG-5.0.0-alpha1.md](./CHANGELOG-5.0.0-alpha1.md) +- [CHANGELOG-5.0.0.md](./CHANGELOG-5.0.0.md) - [CHANGELOG-4.4.0-beta3.md](./CHANGELOG-4.4.0-beta3.md) - [CHANGELOG-4.4.0-beta2.md](./CHANGELOG-4.4.0-beta2.md) - [CHANGELOG-4.4.0-beta1.md](./CHANGELOG-4.4.0-beta1.md) From 2eda19e407cca6cc00dd779451146556b40c430c Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Fri, 12 Jul 2024 11:40:29 +0800 Subject: [PATCH 2/6] feat: Enhance cron schedule, Markdown display (#4888) * feat: Enhance cron schedule, Markdown display, and ticket TAB persistence Signed-off-by: jingyang <3161362058@qq.com> * delete * add env --------- Signed-off-by: jingyang <3161362058@qq.com> --- frontend/pnpm-lock.yaml | 8 + .../applaunchpad/src/constants/theme.ts | 19 ++ .../src/pages/app/edit/components/Form.tsx | 1 - frontend/providers/workorder/.env.template | 10 +- .../deploy/manifests/deploy.yaml.tmpl | 9 + frontend/providers/workorder/package.json | 1 + .../workorder/public/locales/en/common.json | 3 +- .../workorder/public/locales/zh/common.json | 3 +- .../src/components/Markdown/index.module.scss | 2 +- .../workorder/src/pages/api/cronjob/init.ts | 56 ++++++ .../src/pages/api/workorder/check.ts | 183 ++++++++++++++++++ .../detail/components/AppMainInfo.tsx | 6 +- .../workorder/detail/components/Header.tsx | 13 +- .../src/pages/workorders/components/List.tsx | 8 +- .../workorder/src/pages/workorders/index.tsx | 13 +- .../workorder/src/services/db/workorder.ts | 12 ++ .../providers/workorder/src/styles/reset.scss | 83 -------- .../providers/workorder/src/types/index.ts | 2 + .../workorder/src/types/workorder.ts | 1 + 19 files changed, 339 insertions(+), 94 deletions(-) create mode 100644 frontend/providers/workorder/src/pages/api/cronjob/init.ts create mode 100644 frontend/providers/workorder/src/pages/api/workorder/check.ts diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ac1830d4dcb..1852b5c992c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -2248,6 +2248,9 @@ importers: axios: specifier: ^1.5.1 version: 1.6.2 + croner: + specifier: ^8.1.0 + version: 8.1.0 date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -11734,6 +11737,11 @@ packages: luxon: 3.4.4 dev: false + /croner@8.1.0: + resolution: {integrity: sha512-sz990XOUPR8dG/r5BRKMBd15MYDDUu8oeSaxFD5DqvNgHSZw8Psd1s689/IGET7ezxRMiNlCIyGeY1Gvxp/MLg==} + engines: {node: '>=18.0'} + dev: false + /cronstrue@2.44.0: resolution: {integrity: sha512-71aQD16uXrqjDUYHsFYY4/SSmEepzQZqTqWsU9x2kDMCYKyIp/5e0QW/cp2lBNO9PJB1xOpIbBJuQEa5yKx98A==} hasBin: true diff --git a/frontend/providers/applaunchpad/src/constants/theme.ts b/frontend/providers/applaunchpad/src/constants/theme.ts index 3b6677eccbc..482ab43103e 100644 --- a/frontend/providers/applaunchpad/src/constants/theme.ts +++ b/frontend/providers/applaunchpad/src/constants/theme.ts @@ -1,6 +1,22 @@ import { extendTheme } from '@chakra-ui/react'; import { theme as sealosTheme } from '@sealos/ui'; +import { switchAnatomy } from '@chakra-ui/anatomy'; +import { createMultiStyleConfigHelpers } from '@chakra-ui/react'; + +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers( + switchAnatomy.keys +); + +const baseStyle = definePartsStyle({ + container: {}, + track: { + bg: 'gray.200' + } +}); + +export const Switch = defineMultiStyleConfig({ baseStyle }); + export const theme = extendTheme(sealosTheme, { styles: { global: { @@ -12,5 +28,8 @@ export const theme = extendTheme(sealosTheme, { minWidth: '1024px' } } + }, + components: { + Switch: Switch } }); diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx index 64275e48f05..843871bc4b7 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx @@ -818,7 +818,6 @@ const Form = ({ { updateNetworks(i, { diff --git a/frontend/providers/workorder/.env.template b/frontend/providers/workorder/.env.template index ec888eb5b11..f7500dd584e 100644 --- a/frontend/providers/workorder/.env.template +++ b/frontend/providers/workorder/.env.template @@ -1,6 +1,6 @@ NEXT_PUBLIC_MOCK_USER= -SEALOS_DOMAIN="cloud.sealos.io" +SEALOS_DOMAIN="cloud.sealos.io" # mongodb MONGODB_URI= # minio @@ -12,4 +12,10 @@ MINIO_PORT= # admin ADMIN_FEISHU_URL= JWT_SECRET_DESKTOP_TO_APP= -JWT_SECRET_SELF= \ No newline at end of file +JWT_SECRET_SELF= +ADMIN_FEISHU_CALLBACK_URL= +ADMIN_API_TOKEN= +# fastgpt +FASTGPT_API_URL= +FASTGPT_API_KEY= +FASTGPT_API_LIMIT= \ No newline at end of file diff --git a/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl index 541b20fd3bc..0aafe3a03de 100644 --- a/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/workorder/deploy/manifests/deploy.yaml.tmpl @@ -64,6 +64,15 @@ spec: # do not modify this image, it is used for CI/CD image: ghcr.io/labring/sealos-workorder-frontend:latest imagePullPolicy: Always + readinessProbe: + httpGet: + path: /api/cronjob/init + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 volumeMounts: - name: workorder-frontend-volume mountPath: /config.yaml diff --git a/frontend/providers/workorder/package.json b/frontend/providers/workorder/package.json index 42fd27987c0..03f83a955be 100644 --- a/frontend/providers/workorder/package.json +++ b/frontend/providers/workorder/package.json @@ -23,6 +23,7 @@ "@types/multer": "^1.4.10", "ansi_up": "^5.2.1", "axios": "^1.5.1", + "croner": "^8.1.0", "date-fns": "^2.30.0", "dayjs": "^1.11.10", "echarts": "^5.4.3", diff --git a/frontend/providers/workorder/public/locales/en/common.json b/frontend/providers/workorder/public/locales/en/common.json index 023d727635d..52212d169b2 100644 --- a/frontend/providers/workorder/public/locales/en/common.json +++ b/frontend/providers/workorder/public/locales/en/common.json @@ -61,5 +61,6 @@ "api is error": "api is error", "region": "region", "userId": "userId", - "other": "other" + "other": "other", + "fastgpt": "FastGPT" } \ No newline at end of file diff --git a/frontend/providers/workorder/public/locales/zh/common.json b/frontend/providers/workorder/public/locales/zh/common.json index e3f58201863..8090d32602b 100644 --- a/frontend/providers/workorder/public/locales/zh/common.json +++ b/frontend/providers/workorder/public/locales/zh/common.json @@ -61,5 +61,6 @@ "api is error": "api is error", "region": "可用区", "userId": "用户ID", - "other": "其他" + "other": "其他", + "fastgpt": "FastGPT" } \ No newline at end of file diff --git a/frontend/providers/workorder/src/components/Markdown/index.module.scss b/frontend/providers/workorder/src/components/Markdown/index.module.scss index 684e63fde79..53ae87fc114 100644 --- a/frontend/providers/workorder/src/components/Markdown/index.module.scss +++ b/frontend/providers/workorder/src/components/Markdown/index.module.scss @@ -10,7 +10,6 @@ .animation { height: 20px; - &::after { display: inline-block; content: ''; @@ -21,6 +20,7 @@ animation: blink 0.6s infinite; } } + @keyframes blink { from, to { diff --git a/frontend/providers/workorder/src/pages/api/cronjob/init.ts b/frontend/providers/workorder/src/pages/api/cronjob/init.ts new file mode 100644 index 00000000000..4e69390a50d --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/cronjob/init.ts @@ -0,0 +1,56 @@ +import { jsonRes } from '@/services/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { Cron } from 'croner'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { action } = req.query; + const baseurl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`; + + if (!global.cronJobWorkOrders) { + global.cronJobWorkOrders = new Cron( + process.env.NODE_ENV === 'production' ? '0,30 10-17 * * 1-5' : '* * * * *', + async () => { + const result = await (await fetch(`${baseurl}/api/workorder/check`)).json(); + const now = new Date(); + console.log( + `Cron Job Run at: ${now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}` + ); + }, + { + timezone: 'Asia/Shanghai' + } + ); + } + + switch (action) { + case 'pause': + global.cronJobWorkOrders.pause(); + jsonRes(res, { + data: 'Cron job paused' + }); + break; + case 'resume': + global.cronJobWorkOrders.resume(); + jsonRes(res, { + data: 'Cron job resumed' + }); + break; + case 'stop': + global.cronJobWorkOrders.stop(); + jsonRes(res, { + data: 'Cron job stopped' + }); + break; + default: + jsonRes(res, { + data: 'Cron job is running' + }); + } + } catch (error) { + jsonRes(res, { + code: 500, + error: error + }); + } +} diff --git a/frontend/providers/workorder/src/pages/api/workorder/check.ts b/frontend/providers/workorder/src/pages/api/workorder/check.ts new file mode 100644 index 00000000000..3e1010cc6be --- /dev/null +++ b/frontend/providers/workorder/src/pages/api/workorder/check.ts @@ -0,0 +1,183 @@ +import { verifyDesktopToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { getUserById } from '@/services/db/user'; +import { fetchProcessingOrders, updateOrder } from '@/services/db/workorder'; +import { WorkOrderDB, WorkOrderStatus } from '@/types/workorder'; +import { NextApiRequest, NextApiResponse } from 'next'; + +const feishuUrl = process.env.ADMIN_FEISHU_URL; +const feishuCallBackUrl = process.env.ADMIN_FEISHU_CALLBACK_URL; +const adminToken = process.env.ADMIN_API_TOKEN; +const MINUTES_IN_A_WEEK = 7 * 24 * 60; + +const getFeishuForm = ({ + recentUnresponded, + overdueAutoCloseIn7Days +}: { + recentUnresponded: WorkOrderDB[]; + overdueAutoCloseIn7Days: WorkOrderDB[]; +}) => { + const content1 = recentUnresponded + .map((item) => `- [${item.orderId}](${feishuCallBackUrl}?orderId=${item.orderId})`) + .join('\n'); + + const content2 = overdueAutoCloseIn7Days + .map((item) => `- [${item.orderId}](${feishuCallBackUrl}?orderId=${item.orderId})`) + .join('\n'); + + const form = { + msg_type: 'interactive', + card: { + config: {}, + i18n_elements: { + zh_cn: [ + { + tag: 'markdown', + content: '**收到用户消息,超过30分钟的工单**', + text_align: 'left', + text_size: 'normal', + icon: { + tag: 'standard_icon', + token: 'vote_colorful', + color: 'grey' + } + }, + { + tag: 'markdown', + content: content1, + text_align: 'left', + text_size: 'normal' + }, + { + tag: 'hr' + }, + { + tag: 'markdown', + content: '**已自动关闭的工单**', + text_align: 'left', + text_size: 'normal', + icon: { + tag: 'standard_icon', + token: 'todo_colorful', + color: 'grey' + } + }, + { + tag: 'markdown', + content: content2, + text_align: 'left', + text_size: 'normal' + } + ] + }, + i18n_header: { + zh_cn: { + title: { + tag: 'plain_text', + content: '工单消息提醒' + }, + subtitle: { + tag: 'plain_text', + content: '' + }, + template: 'indigo', + ud_icon: { + tag: 'standard_icon', + token: 'myai_colorful' + } + } + } + } + }; + + return form; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (!adminToken) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const payload = await verifyDesktopToken(adminToken); + if (!payload?.userId) { + return jsonRes(res, { + code: 401, + message: "'token is invaild'" + }); + } + const user = await getUserById(payload?.userId); + if (!user?.isAdmin) { + return jsonRes(res, { + code: 403, + message: 'Access denied' + }); + } + + const orders = await fetchProcessingOrders(); + const recentUnresponded: WorkOrderDB[] = []; + const overdueAutoCloseIn7Days: WorkOrderDB[] = []; + const currentTime = new Date(); + orders.forEach((order) => { + if (!order.dialogs || order.dialogs.length === 0) return; + let lastDialog = order.dialogs[order.dialogs.length - 1]; + if (lastDialog.userId === 'robot') return; + + const lastDialogTime = new Date(lastDialog.time); + const timeDiff = Math.ceil((currentTime.getTime() - lastDialogTime.getTime()) / 1000 / 60); + if (!lastDialog.isAdmin && timeDiff > 30) { + recentUnresponded.push(order); + } + if (lastDialog.isAdmin && timeDiff > MINUTES_IN_A_WEEK) { + console.log(order); + overdueAutoCloseIn7Days.push(order); + } + }); + + if (recentUnresponded.length === 0 && overdueAutoCloseIn7Days.length === 0) { + return jsonRes(res, { + code: 204, + message: 'No content to send' + }); + } + + if (overdueAutoCloseIn7Days.length > 0) { + try { + for (const order of overdueAutoCloseIn7Days) { + await updateOrder({ + orderId: order.orderId, + userId: payload.userId, + updates: { + status: WorkOrderStatus.Completed + } + }); + } + } catch (error) {} + } + + const form = getFeishuForm({ overdueAutoCloseIn7Days, recentUnresponded }); + + if (!feishuUrl) { + return jsonRes(res, { + code: 500, + message: 'Missing Feishu API' + }); + } + const data = await fetch(feishuUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(form) + }); + const result = await data.json(); + jsonRes(res, { + data: result + }); + } catch (error) { + console.log(error); + jsonRes(res, { code: 500, error: error }); + } +} diff --git a/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx b/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx index ec604a0f5d4..5516700fa9a 100644 --- a/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx +++ b/frontend/providers/workorder/src/pages/workorder/detail/components/AppMainInfo.tsx @@ -97,6 +97,7 @@ const AppMainInfo = ({ isManuallyHandled: boolean; }) => { const textareaMinH = '50px'; + const [isLoading, setIsloading] = useState(false); const { session } = useSessionStore(); const { t } = useTranslation(); const [text, setText] = useState(''); @@ -282,6 +283,7 @@ const AppMainInfo = ({ const handleTransferToHuman = async () => { try { + setIsloading(true); await FeishuNotification({ type: app.type, description: app.description, @@ -298,6 +300,7 @@ const AppMainInfo = ({ status: 'error' }); } + setIsloading(false); refetchWorkOrder(); }; @@ -409,7 +412,7 @@ const AppMainInfo = ({ {isURL(item.content) ? ( img scrollToBottom()}> ) : ( - + )} @@ -418,6 +421,7 @@ const AppMainInfo = ({ !session?.user.isAdmin && !app?.manualHandling?.isManuallyHandled && ( + {lngVal} + ))} - - - ) : ( - <> + + ); } - -export default LangSelect; diff --git a/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx new file mode 100644 index 00000000000..93ab1f2532e --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx @@ -0,0 +1,147 @@ +import { useConfigStore } from '@/stores/config'; +import useSessionStore, { OauthAction } from '@/stores/session'; +import { OauthProvider } from '@/types/user'; +import { Text, Image, Center } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import router from 'next/router'; +import { useMemo } from 'react'; +import { ConfigItem } from './ConfigItem'; +import { BINDING_STATE_MODIFY_BEHAVIOR, BindingModifyButton } from './BindingModifyButton'; + +export function AuthModifyList({ + isOnlyOne, + GITHUBIsBinding, + WECHATIsBinding, + GOOGLEIsBinding, + avatarUrl +}: { + isOnlyOne: boolean; + avatarUrl: string; + GOOGLEIsBinding: boolean; + GITHUBIsBinding: boolean; + WECHATIsBinding: boolean; +}) { + const { authConfig: conf, layoutConfig } = useConfigStore(); + const { setProvider, generateState } = useSessionStore(); + const { t } = useTranslation(); + const authActionList: { + title: string; + actionCb: (actino: OauthAction) => void; + isBinding: boolean; + }[] = useMemo(() => { + if (!conf) return []; + const actionCbGen = + ({ + url, + provider, + clientId + }: { + url: string; + provider: OauthProvider; + clientId: string; + }) => + (action: T) => { + const state = generateState(action); + setProvider(provider); + if (conf.proxyAddress) { + const target = new URL(conf.proxyAddress); + const callback = new URL(conf.callbackURL); + target.searchParams.append( + 'oauthProxyState', + encodeURIComponent(callback.toString()) + '_' + state + ); + target.searchParams.append('oauthProxyClientID', clientId); + target.searchParams.append('oauthProxyProvider', provider); + router.replace(target.toString()); + } else { + const target = new URL(url); + target.searchParams.append('state', state); + router.replace(target); + } + }; + const result = []; + if (conf.idp.github.enabled) + result.push({ + title: t('Github'), + isBinding: GITHUBIsBinding, + actionCb(action: Exclude) { + const githubConf = conf.idp.github; + return actionCbGen({ + provider: 'GITHUB', + clientId: githubConf.clientID, + url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user` + })(action); + } + }); + + if (conf.idp.wechat.enabled) + result.push({ + title: t('Wechat'), + isBinding: WECHATIsBinding, + actionCb(action: Exclude) { + const wechatConf = conf.idp.wechat; + return actionCbGen({ + provider: 'WECHAT', + clientId: wechatConf.clientID, + url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&scope=snsapi_login&#wechat_redirect` + })(action); + } + }); + if (conf.idp.google.enabled) + result.push({ + title: t('Google'), + isBinding: GOOGLEIsBinding, + actionCb(action: Exclude) { + const googleConf = conf.idp.google; + const scope = encodeURIComponent( + `https://www.googleapis.com/auth/userinfo.profile openid` + ); + return actionCbGen({ + provider: 'GOOGLE', + clientId: googleConf.clientID, + url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleConf.clientID}&redirect_uri=${conf.callbackURL}&response_type=code&scope=${scope}&include_granted_scopes=true` + })(action); + } + }); + return result; + }, [conf, t, GITHUBIsBinding, WECHATIsBinding, GOOGLEIsBinding]); + return authActionList.map((auth) => { + return ( + {auth.title}} + RightElement={ + <> + {auth.isBinding ? ( +
+ user avator +
+ ) : ( + {t('Unbound')} + )} + {(!auth.isBinding || !isOnlyOne) && ( + { + e.preventDefault(); + auth.isBinding ? auth.actionCb('UNBIND') : auth.actionCb('BIND'); + }} + modifyBehavior={ + auth.isBinding + ? BINDING_STATE_MODIFY_BEHAVIOR.UNBINDING + : BINDING_STATE_MODIFY_BEHAVIOR.BINDING + } + /> + )} + + } + >
+ ); + }); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/BindingModifyButton.tsx b/frontend/desktop/src/components/account/AccountCenter/BindingModifyButton.tsx new file mode 100644 index 00000000000..775724a60ef --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/BindingModifyButton.tsx @@ -0,0 +1,24 @@ +import { ButtonProps, Button, forwardRef } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +export enum BINDING_STATE_MODIFY_BEHAVIOR { + BINDING, + UNBINDING, + CHANGE_BINDING +} +export const BindingModifyButton = forwardRef< + ButtonProps & { modifyBehavior: BINDING_STATE_MODIFY_BEHAVIOR }, + 'button' +>(function ChangeButton({ modifyBehavior, ...props }, ref) { + const { t } = useTranslation(); + return ( + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/ConfigItem.tsx b/frontend/desktop/src/components/account/AccountCenter/ConfigItem.tsx new file mode 100644 index 00000000000..9ee7ed093e0 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/ConfigItem.tsx @@ -0,0 +1,24 @@ +import { StackProps, HStack, Flex, Box, forwardRef } from '@chakra-ui/react'; +import { ReactNode } from 'react'; + +export const ConfigItem = forwardRef< + StackProps & { LeftElement: ReactNode; RightElement: ReactNode }, + 'div' +>(function ConfigItem({ LeftElement, RightElement, ...props }, ref) { + return ( + + + {LeftElement} + + + {RightElement} + + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx new file mode 100644 index 00000000000..4b2d652984b --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx @@ -0,0 +1,250 @@ +import { useCustomToast } from '@/hooks/useCustomToast'; +import useSessionStore from '@/stores/session'; +import { ValueOf } from '@/types'; +import { + ButtonProps, + useDisclosure, + Text, + Button, + Modal, + ModalOverlay, + ModalContent, + ModalCloseButton, + ModalHeader, + Spinner, + ModalBody, + VStack, + HStack, + FormControl, + Divider +} from '@chakra-ui/react'; +import { InfoCircleIcon, WarnTriangeIcon } from '@sealos/ui'; +import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import { SettingInput } from './SettingInput'; +import { SettingInputGroup } from './SettingInputGroup'; +import { useRouter } from 'next/router'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { deleteUserRequest } from '@/api/auth'; +enum PageStatus { + IDLE, + REMAIN_OTHER_REGION_RESOURCE, + REMAIN_WORKSPACE, + REMAIN_APP, + REMAIN_DATABASE, + REMAIN_OBJECT_STORAGE, + REMAIN_TEMPLATE, + INSUFFICIENT_BALANCE +} +export default function DeleteAccount({ ...props }: ButtonProps) { + const { onOpen, isOpen, onClose } = useDisclosure(); + const t = useTranslation().t; + const errorT = useTranslation('error').t; + const queryClient = useQueryClient(); + const { toast } = useCustomToast({ status: 'error' }); + const { delSession, setToken, session } = useSessionStore(); + const [pagestatus, setPagestatus] = useState(PageStatus.IDLE); + const [nickname, setnickname] = useState(''); + const verifyWords = t('deleteMyAccount'); + const router = useRouter(); + const [verifyValue, setVerifyValue] = useState(''); + const mutation = useMutation({ + mutationFn: deleteUserRequest, + onSuccess() { + delSession(); + queryClient.clear(); + setToken(''); + router.replace('/signin'); + setToken(''); + }, + onError(error: { message: ValueOf }) { + const message = error?.message; + if (message === RESOURCE_STATUS.REMAIN_APP) { + return setPagestatus(PageStatus.REMAIN_APP); + } else if (message === RESOURCE_STATUS.REMAIN_DATABASE) { + return setPagestatus(PageStatus.REMAIN_DATABASE); + } else if (message === RESOURCE_STATUS.REMAIN_OBJECT_STORAGE) { + return setPagestatus(PageStatus.REMAIN_OBJECT_STORAGE); + } else if (message === RESOURCE_STATUS.REMAIN_TEMPLATE) { + return setPagestatus(PageStatus.REMAIN_TEMPLATE); + } else if (message === RESOURCE_STATUS.REMAIN_OTHER_REGION_RESOURCE) { + return setPagestatus(PageStatus.REMAIN_OTHER_REGION_RESOURCE); + } else if (message === RESOURCE_STATUS.INSUFFICENT_BALANCE) { + return setPagestatus(PageStatus.INSUFFICIENT_BALANCE); + } else if (message === RESOURCE_STATUS.REMAIN_WORKSACE_OWNER) { + return setPagestatus(PageStatus.REMAIN_WORKSPACE); + } else { + setPagestatus(PageStatus.IDLE); + toast({ title: errorT(message) }); + } + } + }); + const deleteModalOnClose = () => { + setPagestatus(PageStatus.IDLE); + onClose(); + }; + return ( + <> + { + + } + + + + + + + {t('Delete Account Title')} + + {mutation.isLoading ? ( + + ) : ( + + {pagestatus === PageStatus.IDLE ? ( + + {t('DeleteAccountTitle')} + + + {t('IrreversibleActionTips')} + + + + + {t('Enter')} + {session?.user.name} + {t('Confirm')} + + + { + setnickname(e.target.value); + }} + /> + + + + + {t('Enter')} + {verifyWords} + {t('Confirm')} + + + { + setVerifyValue(e.target.value); + }} + /> + + + + + + + + ) : ( + + {pagestatus === PageStatus.INSUFFICIENT_BALANCE ? ( + {t('INSUFFICIENT_BALANCE_tips')} + ) : pagestatus === PageStatus.REMAIN_APP ? ( + {t('Remain App Tips')} + ) : pagestatus === PageStatus.REMAIN_TEMPLATE ? ( + {t('Remain Template Tips')} + ) : pagestatus === PageStatus.REMAIN_OBJECT_STORAGE ? ( + {t('Remain ObjectStorage Tips')} + ) : pagestatus === PageStatus.REMAIN_DATABASE ? ( + {t('Remain Database Tips')} + ) : pagestatus === PageStatus.REMAIN_WORKSPACE ? ( + {t('Remain Workspace Tips')} + ) : pagestatus === PageStatus.REMAIN_OTHER_REGION_RESOURCE ? ( + {t('Remain Other Region Resource Tips')} + ) : null} + + + {(pagestatus === PageStatus.REMAIN_APP || + pagestatus === PageStatus.REMAIN_DATABASE || + pagestatus === PageStatus.REMAIN_OBJECT_STORAGE || + pagestatus === PageStatus.REMAIN_TEMPLATE || + pagestatus === PageStatus.REMAIN_WORKSPACE || + pagestatus === PageStatus.REMAIN_OTHER_REGION_RESOURCE) && ( + {t('Delete Account Caution')} + )} + + + + + + + )} + + )} + + + + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/PasswordModify.tsx b/frontend/desktop/src/components/account/AccountCenter/PasswordModify.tsx new file mode 100644 index 00000000000..c5ee714f6e9 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/PasswordModify.tsx @@ -0,0 +1,125 @@ +import { + Button, + FlexProps, + FormControl, + FormLabel, + HStack, + Spinner, + VStack +} from '@chakra-ui/react'; +import { useMutation } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { ApiResp } from '@/types'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { strongPassword } from '@/utils/crypto'; +import { passwordModifyRequest } from '@/api/auth'; +import { SettingInput } from './SettingInput'; +import { SettingInputGroup } from './SettingInputGroup'; + +export default function PasswordModify( + props: FlexProps & { + onClose: () => void; + } +) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const { register, handleSubmit, reset, formState } = useForm<{ + oldPassword: string; + newPassword: string; + againPassword: string; + }>(); + const mutation = useMutation(passwordModifyRequest, { + onSuccess(data) { + if (data.code === 200) { + toast({ + status: 'success', + title: t('passwordChangeSuccess') + }); + reset(); + props.onClose?.(); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( + <> +
{ + mutation.mutate({ oldPassword, newPassword }); + }, + (errors) => { + if (errors.oldPassword) return toast({ title: t('currentPasswordRequired') }); + if (errors.newPassword) return toast({ title: t('password tips') }); + if (errors.againPassword) return toast({ title: t('passwordMismatch') }); + } + )} + > + + + + {t('currentPassword')} + + + + + + + + {t('newPassword')} + + + + + + + + {t('confirmNewPassword')} + + + + + + + +
+ + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInput.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInput.tsx new file mode 100644 index 00000000000..e7f6f6b6c89 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInput.tsx @@ -0,0 +1,20 @@ +import { InputProps, Flex, Input, forwardRef, InputGroup, InputGroupProps } from '@chakra-ui/react'; + +export const SettingInput = forwardRef(function SettingInput( + { children, ...props }, + ref +) { + return ( + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx new file mode 100644 index 00000000000..9b3d6563a2f --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx @@ -0,0 +1,22 @@ +import { InputProps, Flex, Input, forwardRef, InputGroup, InputGroupProps } from '@chakra-ui/react'; + +export const SettingInputGroup = forwardRef(function SettingInputGroup( + props, + ref +) { + return ( + + ); +}); diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInputRightElement.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInputRightElement.tsx new file mode 100644 index 00000000000..149e1ec2cb0 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInputRightElement.tsx @@ -0,0 +1,25 @@ +import { + InputProps, + Flex, + Input, + forwardRef, + InputGroup, + InputGroupProps, + InputRightElement, + InputRightElementProps +} from '@chakra-ui/react'; + +export const SettingInputRightElement = forwardRef( + function SettingInputRightElement({ ...props }, ref) { + return ( + + ); + } +); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx new file mode 100644 index 00000000000..5c5e20919f7 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsBind.tsx @@ -0,0 +1,204 @@ +import { + Button, + FormControl, + FormLabel, + HStack, + Spinner, + VStack, + Text, + Link +} from '@chakra-ui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { getSmsBindCodeRequest, verifySmsBindRequest } from '@/api/auth'; +import { SettingInput } from '../SettingInput'; +import { MouseEventHandler } from 'react'; + +import { SettingInputGroup } from '../SettingInputGroup'; +import { SettingInputRightElement } from '../SettingInputRightElement'; +import { useTimer } from '@/hooks/useTimer'; +import { ApiResp } from '@/types'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ProviderType } from 'prisma/global/generated/client'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { smsIdValid } from './utils'; +import { MERGE_USER_READY } from '@/types/response/utils'; + +const smsBindGen = (smsType: SmsType) => + function SmsBindCore({ onClose }: { onClose: () => void }) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + const { setMergeUserData, setMergeUserStatus } = useCallbackStore(); + const getCodeMutation = useMutation({ + mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { + return getSmsBindCodeRequest(smsType)({ id }); + }, + onSuccess(data) { + startTimer(); + toast({ + status: 'success', + title: t('Already Sent Code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('Get code failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('Already Sent Code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: smsType === 'phone' ? t('Invalid phone number') : t('Invalid Email') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ + id, + smsType + }); + }; + const queryClient = useQueryClient(); + const mutation = useMutation({ + async mutationFn({ smsType, ...data }: { id: string; code: string; smsType: SmsType }) { + return verifySmsBindRequest(smsType)(data); + }, + async onSuccess(data) { + const status = data.message || ''; + if (data.code === 200) { + toast({ + status: 'success', + title: t('Bind Success') + }); + reset(); + await queryClient.invalidateQueries(); + onClose?.(); + } else if (Object.values(MERGE_USER_READY).includes(status as MERGE_USER_READY)) { + if (status === MERGE_USER_READY.MERGE_USER_CONTINUE) { + const code = data.data?.code; + if (!code) return; + setMergeUserStatus(MergeUserStatus.CANMERGE); + setMergeUserData({ + code, + providerType: ProviderType.PHONE + }); + } else { + setMergeUserStatus(MergeUserStatus.CONFLICT); + } + onClose?.(); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message, status: 'error' }); + } + }); + + return mutation.isLoading ? ( + + ) : ( + <> +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + smsType + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'email') return toast({ title: t('Invalid Email') }); + else return toast({ title: t('Invalid phone number') }); + } + if (errors.verifyCode) return toast({ title: t('verify code tips') }); + } + )} + > + + + + {smsType === 'phone' ? t('Phone') : t('Email')} + + + + { + + {t('Get Code')} + + } + + + + + + + {t('verifyCode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ + ); + }; +export const PhoneBind = smsBindGen('phone'); +export const EmailBind = smsBindGen('email'); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsChange.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsChange.tsx new file mode 100644 index 00000000000..9b626bdfe7d --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsChange.tsx @@ -0,0 +1,389 @@ +import { + Button, + FlexProps, + FormControl, + FormLabel, + HStack, + Spinner, + VStack, + Text, + Link +} from '@chakra-ui/react'; +import { useMutation } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { ApiResp } from '@/types'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { SettingInput } from '../SettingInput'; +import { MouseEventHandler, useState } from 'react'; + +import { SettingInputGroup } from '../SettingInputGroup'; +import { SettingInputRightElement } from '../SettingInputRightElement'; +import { useTimer } from '@/hooks/useTimer'; +import { + getNewSmsCodeRequest, + getOldSmsCodeRequest, + verifyNewSmsRequest, + verifyOldSmsRequest +} from '@/api/auth'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ProviderType } from 'prisma/global/generated/client'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { smsIdValid } from './utils'; +import { MERGE_USER_READY } from '@/types/response/utils'; +enum PageState { + VERIFY_OLD, + VERIFY_NEW +} +function OldSms({ smsType, onSuccess }: { smsType: SmsType; onSuccess?: (uid: string) => void }) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + + const getCodeMutation = useMutation({ + mutationFn({ smsType, id }: { id: string; smsType: SmsType }) { + return getOldSmsCodeRequest(smsType)({ id }); + }, + onSuccess(data) { + startTimer(); + toast({ + status: 'success', + title: t('Already Sent Code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('Get code failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('Already Sent Code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: t('Get code failed') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ id, smsType }); + }; + const mutation = useMutation({ + mutationFn({ smsType, ...data }: { smsType: SmsType; id: string; code: string }) { + return verifyOldSmsRequest(smsType)(data); + }, + onSuccess(data) { + const status = data.message || ''; + if (data.code === 200) { + reset(); + onSuccess?.(data?.data?.uid || ''); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + smsType + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'phone') return toast({ title: t('Invalid phone number') }); + else return toast({ title: t('Invalid Email') }); + } + if (errors.verifyCode) return toast({ title: t('verify code tips') }); + } + )} + > + + + + + {smsType === 'phone' ? t('Old Phone') : t('Old Email')} + + + + + { + + {t('Get Code')} + + } + + + + + + + {t('verifyCode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ ); +} +function NewSms({ + smsType = 'phone', + uid, + onSuccess: onClose, + onError +}: { + uid: string; + smsType?: SmsType; + onSuccess?: () => void; + onError?: () => void; +}) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + const { setMergeUserData, setMergeUserStatus } = useCallbackStore(); + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + + const getCodeMutation = useMutation({ + mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { + return getNewSmsCodeRequest(smsType)({ id, uid }); + }, + onSuccess(data) { + startTimer(); + toast({ + status: 'success', + title: t('Already Sent Code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('Get code failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('Already Sent Code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: smsType === 'phone' ? t('Invalid phone number') : t('Invalid Email') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ + id, + smsType + }); + }; + const mutation = useMutation(verifyNewSmsRequest(smsType), { + onSuccess(data) { + const status = data.message || ''; + if (data.code === 200) { + toast({ + status: 'success', + title: smsType === 'phone' ? t('phoneChangeSuccess') : t('emailChangeSuccess') + }); + reset(); + onClose?.(); + } else if (Object.values(MERGE_USER_READY).includes(status as MERGE_USER_READY)) { + // onSuccess?.() + if (status === MERGE_USER_READY.MERGE_USER_CONTINUE) { + setMergeUserStatus(MergeUserStatus.CANMERGE); + setMergeUserData({ + code: data.data.code, + providerType: ProviderType.PHONE + }); + } else { + setMergeUserStatus(MergeUserStatus.CONFLICT); + } + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + uid + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'phone') return toast({ title: t('Invalid phone number') }); + else return toast({ title: t('Invalid Email') }); + } + if (errors.verifyCode) return toast({ title: t('verify code tips') }); + } + )} + > + + + + + {smsType === 'phone' ? t('New Phone') : t('New Email')} + + + + + { + + {t('Get Code')} + + } + + + + + + + {t('verifyCode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ ); +} +const smsChangeGen = (smsType: SmsType) => + function SmsChangeCore({ onClose }: { onClose: () => void }) { + const [pageState, setPageState] = useState(PageState.VERIFY_OLD); + const [codeUid, setCodeUid] = useState(''); + return pageState === PageState.VERIFY_OLD ? ( + { + setCodeUid(uid); + setPageState(PageState.VERIFY_NEW); + }} + /> + ) : ( + { + setPageState(PageState.VERIFY_OLD); + onClose(); + }} + /> + ); + }; + +export const PhoneChange = smsChangeGen('phone'); +export const EmailChange = smsChangeGen('email'); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsUnbind.tsx b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsUnbind.tsx new file mode 100644 index 00000000000..94bffad0228 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/SmsUnbind.tsx @@ -0,0 +1,187 @@ +import { + Button, + FormControl, + FormLabel, + HStack, + Spinner, + VStack, + Text, + Link +} from '@chakra-ui/react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { useTranslation } from 'next-i18next'; +import { useForm } from 'react-hook-form'; +import { getSmsUnbindCodeRequest, verifySmsUnbindRequest } from '@/api/auth'; +import { SettingInput } from '../SettingInput'; +import { MouseEventHandler } from 'react'; + +import { SettingInputGroup } from '../SettingInputGroup'; +import { SettingInputRightElement } from '../SettingInputRightElement'; +import { useTimer } from '@/hooks/useTimer'; +import { ApiResp } from '@/types'; +import { SmsType } from '@/services/backend/db/verifyCode'; +import { smsIdValid } from './utils'; + +const smsUnBindGen = (smsType: SmsType) => + function UnBindCore({ onClose }: { onClose: () => void }) { + const { t } = useTranslation(); + const { toast } = useCustomToast({ status: 'error' }); + + const { register, handleSubmit, trigger, getValues, reset, formState } = useForm<{ + id: string; + verifyCode: string; + }>(); + const { seconds, startTimer, isRunning } = useTimer({ + duration: 60, + step: 1 + }); + + const getCodeMutation = useMutation({ + mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { + return getSmsUnbindCodeRequest(smsType)({ id }); + }, + onSuccess(data) { + startTimer(); + toast({ + status: 'success', + title: t('Already Sent Code') + }); + }, + onError(err) { + getCodeMutation.reset(); + toast({ + status: 'error', + title: t('Get code failed') + }); + } + }); + const remainTime = 60 - seconds; + const getCode: MouseEventHandler = async (e) => { + e.preventDefault(); + if (isRunning) { + toast({ + status: 'error', + title: t('Already Sent Code') + }); + return; + } + if (!(await trigger('id'))) { + toast({ + status: 'error', + title: t('Get code failed') + }); + return; + } + const id = getValues('id'); + getCodeMutation.mutate({ + id, + smsType + }); + }; + const queryClient = useQueryClient(); + const mutation = useMutation({ + mutationFn({ smsType, ...data }: { id: string; smsType: SmsType; code: string }) { + return verifySmsUnbindRequest(smsType)(data); + }, + async onSuccess(data) { + if (data.code === 200) { + toast({ + status: 'success', + title: t('Unbind Success') + }); + reset(); + await queryClient.invalidateQueries(); + onClose?.(); + } + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + + return mutation.isLoading ? ( + + ) : ( + <> +
{ + mutation.mutate({ + id: data.id, + code: data.verifyCode, + smsType + }); + }, + (errors) => { + if (errors.id) { + if (smsType === 'email') return toast({ title: t('Invalid Email') }); + else return toast({ title: t('Invalid phone number') }); + } + if (errors.verifyCode) return toast({ title: t('verify code tips') }); + } + )} + > + + + + {smsType === 'phone' ? t('Phone') : t('Email')} + + + + { + + {t('Get Code')} + + } + + + + + + + {t('verifyCode')} + + v.length === 6 + })} + autoComplete="one-time-code" + flex={'1'} + type="text" + > + + {isRunning && {remainTime} s} + + + + + + +
+ + ); + }; +export const PhoneUnBind = smsUnBindGen('phone'); +export const EmailUnBind = smsUnBindGen('email'); diff --git a/frontend/desktop/src/components/account/AccountCenter/SmsModify/utils.ts b/frontend/desktop/src/components/account/AccountCenter/SmsModify/utils.ts new file mode 100644 index 00000000000..44433a9d3d0 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/SmsModify/utils.ts @@ -0,0 +1,20 @@ +import { SmsType } from '@/services/backend/db/verifyCode'; + +export const smsIdValid = (smsType: SmsType) => { + if (smsType === 'phone') + return { + pattern: { + value: /^1[3-9]\d{9}$/, + message: 'Invalid Phone Number' + }, + required: 'Phone number can not be blank' + }; + else + return { + pattern: { + value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + message: 'Invalid Email Address' + }, + required: 'Email Address can not be blank' + }; +}; diff --git a/frontend/desktop/src/components/account/AccountCenter/index.tsx b/frontend/desktop/src/components/account/AccountCenter/index.tsx new file mode 100644 index 00000000000..66183b34e9a --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/index.tsx @@ -0,0 +1,347 @@ +import { + Flex, + Text, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalOverlay, + useDisclosure, + IconButton, + IconButtonProps, + ModalHeader, + Spinner, + Image, + HStack, + VStack, + Center +} from '@chakra-ui/react'; +import { type FC, useMemo, useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import useSessionStore from '@/stores/session'; +import { useTranslation } from 'next-i18next'; +import { SettingIcon, LeftArrowIcon, GithubIcon, GoogleIcon, WechatIcon } from '@sealos/ui'; +import LangSelectList from '@/components/LangSelect'; +import { UserInfo } from '@/api/auth'; +import PasswordModify from '@/components/account/AccountCenter/PasswordModify'; +import { PhoneBind, EmailBind } from './SmsModify/SmsBind'; +import { PhoneUnBind, EmailUnBind } from './SmsModify/SmsUnbind'; +import { useConfigStore } from '@/stores/config'; +import { PhoneChange, EmailChange } from './SmsModify/SmsChange'; +import { BindingModifyButton, BINDING_STATE_MODIFY_BEHAVIOR } from './BindingModifyButton'; +import { ConfigItem } from './ConfigItem'; +import { AuthModifyList } from './AuthModifyList'; +import { useRouter } from 'next/router'; +import DeleteAccount from './DeleteAccountModal'; +import { ValueOf } from '@/types'; +enum _PageState { + INDEX = 0 + // WECHAT_BIND, + // WECHAT_UNBIND, + // GITHUB_UNBIND, + // GITHUB_BIND +} +enum PasswordState { + PASSWORD = 10 +} +enum PhoneState { + PHONE_BIND = 20, + PHONE_UNBIND, + PHONE_CHANGE_BIND +} +enum EmailState { + EMAIL_BIND = 30, + EMAIL_UNBIND, + EMAIL_CHANGE_BIND +} + +const PageState = Object.assign( + Object.assign({}, _PageState, EmailState, PhoneState), + PasswordState +); + +export default function Index(props: Omit) { + const { session } = useSessionStore((s) => s); + const { t } = useTranslation(); + const logo = '/images/default-user.svg'; + const { isOpen, onOpen, onClose } = useDisclosure(); + const [pageState, setPageState] = useState>(PageState.INDEX); + const resetPageState = () => { + setPageState(PageState.INDEX); + infoData.refetch(); + }; + const modalTitle = useMemo(() => { + if (pageState === PageState.INDEX) return t('Account Settings'); + else if (pageState === PageState.PASSWORD) return t('changePassword'); + else if (Object.values(PhoneState).includes(pageState as PhoneState)) + return t('changePhone'); // bind or unbind + else if (Object.values(EmailState).includes(pageState as EmailState)) return t('changeEmail'); + else return ''; + }, [t, pageState]); + const queryClient = useQueryClient(); + const infoData = useQuery({ + queryFn: UserInfo, + queryKey: [session?.token, 'UserInfo'], + select(d) { + return d.data?.info; + } + }); + + const providerState = useMemo(() => { + const providerList = ['PHONE', 'PASSWORD', 'GITHUB', 'WECHAT', 'GOOGLE', 'EMAIL'] as const; + const state = (infoData.data?.oauthProvider || []).reduce( + (pre, cur) => { + const { providerType, providerId } = cur; + // @ts-ignore + if (!providerList.includes(providerType)) return pre; + // @ts-ignore + pre[providerType].isBinding = true; + if (providerType === 'PHONE' || providerType === 'EMAIL') pre[providerType].id = providerId; + pre.total += 1; + return pre; + }, + { + total: 0, + PHONE: { + isBinding: false, + id: '' + }, + EMAIL: { + isBinding: false, + id: '' + }, + GITHUB: { + isBinding: false + }, + WECHAT: { + isBinding: false + }, + GOOGLE: { + isBinding: false + }, + PASSWORD: { + isBinding: false + }, + AVATAR_URL: '' + } + ); + state.AVATAR_URL = infoData.data?.avatarUri || ''; + return state; + }, [infoData.data?.oauthProvider]); + + return ( + <> + { + e.preventDefault(); + return onOpen(); + }} + {...props} + variant={'white-bg-icon'} + icon={} + /> + + + + + + + {pageState !== PageState.INDEX && ( + { + setPageState(PageState.INDEX); + }} + /> + )} + + {modalTitle} + + + + {infoData.isSuccess && infoData.data ? ( + + {pageState === PageState.INDEX ? ( + + {/* */} + + {t('Avatar')} + +
+ user avator +
+
+
+ + {t('Nickname')} + {infoData.data.nickname} + + + {'ID'} + {infoData.data.id} + + {providerState.PASSWORD.isBinding && ( + {t('Password')}} + RightElement={ + <> + ********* + { + setPageState(PageState.PASSWORD); + }} + /> + + } + /> + )} + {t('Phone')}} + RightElement={ + <> + + {providerState.PHONE.isBinding + ? providerState.PHONE.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') + : t('Unbound')} + + + { + providerState.PHONE.isBinding + ? setPageState(PageState.PHONE_CHANGE_BIND) + : setPageState(PageState.PHONE_BIND); + }} + /> + {providerState.PHONE.isBinding && providerState.total > 1 && ( + { + setPageState(PageState.PHONE_UNBIND); + }} + /> + )} + + + } + /> + {t('Email')}} + RightElement={ + <> + + {providerState.EMAIL.isBinding + ? providerState.EMAIL.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') + : t('Unbound')} + + + { + providerState.EMAIL.isBinding + ? setPageState(PageState.EMAIL_CHANGE_BIND) + : setPageState(PageState.EMAIL_BIND); + }} + /> + {providerState.EMAIL.isBinding && providerState.total > 1 && ( + { + setPageState(PageState.EMAIL_UNBIND); + }} + /> + )} + + + } + /> + + {t('Delete account')}} + RightElement={ + <> + {t('Delete Account Tips')} + + + } + /> +
+ ) : ( + + {pageState === PageState.PASSWORD ? ( + + ) : pageState === PageState.PHONE_BIND ? ( + + ) : pageState === PageState.PHONE_UNBIND ? ( + + ) : pageState === PageState.PHONE_CHANGE_BIND ? ( + + ) : pageState === PageState.EMAIL_BIND ? ( + + ) : pageState === PageState.EMAIL_UNBIND ? ( + + ) : pageState === PageState.EMAIL_CHANGE_BIND ? ( + + ) : null} + + )} +
+ ) : ( + + )} +
+
+ + ); +} diff --git a/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx new file mode 100644 index 00000000000..1d25913c988 --- /dev/null +++ b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx @@ -0,0 +1,145 @@ +import { useCustomToast } from '@/hooks/useCustomToast'; +import { + Text, + Button, + Modal, + ModalOverlay, + ModalContent, + ModalCloseButton, + ModalHeader, + Spinner, + ModalBody, + BoxProps, + VStack, + HStack +} from '@chakra-ui/react'; +import { WarnTriangeIcon } from '@sealos/ui'; +import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { mergeUserRequest } from '@/api/auth'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { useEffect, useState } from 'react'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; +import { ValueOf } from '@/types'; + +function NeedToMerge({ ...props }: BoxProps & {}) { + const { mergeUserStatus, mergeUserData, setMergeUserStatus, setMergeUserData } = + useCallbackStore(); + const [isOpen, setIsOpen] = useState(false); + + const onClose = () => { + setMergeUserStatus(MergeUserStatus.IDLE); + }; + + const { t } = useTranslation(); + const errorT = useTranslation('error').t; + const queryClient = useQueryClient(); + const { toast } = useCustomToast({ status: 'error' }); + const mutation = useMutation({ + mutationFn: mergeUserRequest, + onSuccess() { + queryClient.clear(); + }, + onError(err: { message: ValueOf }) { + toast({ + status: 'error', + title: errorT(err.message) + }); + }, + onSettled() { + setMergeUserData(); + setMergeUserStatus(MergeUserStatus.IDLE); + } + }); + useEffect(() => { + setIsOpen(!![MergeUserStatus.CONFLICT, MergeUserStatus.CANMERGE].includes(mergeUserStatus)); + }, [mergeUserStatus]); + return ( + + + + + + + {t('Merge Account Title')} + + {mutation.isLoading ? ( + + ) : ( + + + + {mergeUserStatus === MergeUserStatus.CONFLICT + ? t('MergeAccountTips1') + : t('DeleteAccountTips2')} + + {mergeUserStatus === MergeUserStatus.CONFLICT ? ( + + + + ) : ( + + + + + )} + + + )} + + + ); +} +export default NeedToMerge; diff --git a/frontend/desktop/src/components/account/PasswordModify.tsx b/frontend/desktop/src/components/account/PasswordModify.tsx deleted file mode 100644 index f9efe451de5..00000000000 --- a/frontend/desktop/src/components/account/PasswordModify.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { - Button, - Flex, - FlexProps, - Image, - Input, - InputGroup, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay, - Spinner, - Stack, - useDisclosure -} from '@chakra-ui/react'; -import { useMutation } from '@tanstack/react-query'; -import { useCustomToast } from '@/hooks/useCustomToast'; -import { ApiResp } from '@/types'; -import { useTranslation } from 'react-i18next'; -import { useForm } from 'react-hook-form'; -import { strongPassword } from '@/utils/crypto'; -import { passwordModifyRequest } from '@/api/auth'; -export default function PasswordModify(props: FlexProps) { - const { onOpen, isOpen, onClose } = useDisclosure(); - const { t } = useTranslation(); - const { toast } = useCustomToast({ status: 'error' }); - const mutation = useMutation(passwordModifyRequest, { - onSuccess(data) { - if (data.code === 200) { - toast({ - status: 'success', - title: t('passwordChangeSuccess') - }); - onClose(); - } - }, - onError(error) { - toast({ title: (error as ApiResp).message }); - } - }); - const { register, handleSubmit, reset } = useForm<{ - oldPassword: string; - newPassword: string; - againPassword: string; - }>(); - return ( - <> - { - reset(); - onOpen(); - }} - > - {t('change')} - - - - - - - {t('changePassword')} - - {mutation.isLoading ? ( - - ) : ( - <> - -
{ - mutation.mutate({ oldPassword, newPassword }); - }, - (errors) => { - if (errors.oldPassword) return toast({ title: t('currentPasswordRequired') }); - if (errors.newPassword) return toast({ title: t('password tips') }); - if (errors.againPassword) return toast({ title: t('passwordMismatch') }); - } - )} - > - - - - - - - - - - - - -
-
- - )} -
-
- - ); -} diff --git a/frontend/desktop/src/components/account/index.tsx b/frontend/desktop/src/components/account/index.tsx index 85d7e11b75c..b3381c4ea2b 100644 --- a/frontend/desktop/src/components/account/index.tsx +++ b/frontend/desktop/src/components/account/index.tsx @@ -4,14 +4,7 @@ import { useConfigStore } from '@/stores/config'; import useSessionStore from '@/stores/session'; import download from '@/utils/downloadFIle'; import { Box, Center, Flex, IconButton, Image, Text, useDisclosure } from '@chakra-ui/react'; -import { - CopyIcon, - DocsIcon, - DownloadIcon, - LogoutIcon, - NotificationIcon, - SettingIcon -} from '@sealos/ui'; +import { CopyIcon, DocsIcon, DownloadIcon, LogoutIcon, NotificationIcon } from '@sealos/ui'; import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; @@ -20,10 +13,10 @@ import LangSelectSimple from '../LangSelect/simple'; import { blurBackgroundStyles } from '../desktop_content'; import RegionToggle from '../region/RegionToggle'; import WorkspaceToggle from '../team/WorkspaceToggle'; -import PasswordModify from './PasswordModify'; import GithubComponent from './github'; import { ArrowIcon } from '../icons'; import useAppStore from '@/stores/app'; +import AccountCenter from './AccountCenter'; const baseItemStyle = { w: '52px', @@ -39,8 +32,6 @@ const baseItemStyle = { export default function Account() { const { layoutConfig } = useConfigStore(); const [showId, setShowId] = useState(true); - const passwordEnabled = useConfigStore().authConfig?.idp?.password?.enabled; - const router = useRouter(); const { copyData } = useCopyData(); const { t } = useTranslation(); @@ -159,13 +150,7 @@ export default function Account() { px={'16px'} > {t('Account Settings')} - kubeconfig && copyData(kubeconfig)} - icon={} - aria-label={'setting'} - /> + )} {layoutConfig?.common.workorderEnabled && ( @@ -225,20 +210,6 @@ export default function Account() { /> - {/* {passwordEnabled && ( - - {t('changePassword')} - - - )} */} import('../AppDock'), { ssr: false }); const FloatButton = dynamic(() => import('@/components/floating_button'), { ssr: false }); @@ -240,6 +241,8 @@ export default function Desktop(props: any) { ); })} + {/* modal */} + ); } diff --git a/frontend/desktop/src/components/signin/auth/AuthList.tsx b/frontend/desktop/src/components/signin/auth/AuthList.tsx index 49b721dfabc..6f9b7edb746 100644 --- a/frontend/desktop/src/components/signin/auth/AuthList.tsx +++ b/frontend/desktop/src/components/signin/auth/AuthList.tsx @@ -32,8 +32,10 @@ const AuthList = () => { setProvider(provider); const target = new URL(conf.proxyAddress); const callback = new URL(conf.callbackURL); - callback.searchParams.append('state', state); - target.searchParams.append('oauthProxyState', callback.toString()); + target.searchParams.append( + 'oauthProxyState', + encodeURIComponent(callback.toString()) + '_' + state + ); target.searchParams.append('oauthProxyClientID', id); target.searchParams.append('oauthProxyProvider', provider); router.replace(target.toString()); @@ -47,13 +49,13 @@ const AuthList = () => { const githubConf = conf?.idp.github; if (conf?.proxyAddress) oauthProxyLogin({ - provider: 'github', + provider: 'GITHUB', state, id: githubConf?.clientID as string }); else oauthLogin({ - provider: 'github', + provider: 'GITHUB', url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user&state=${state}` }); }, @@ -67,13 +69,13 @@ const AuthList = () => { const state = generateState(); if (conf.proxyAddress) oauthProxyLogin({ - provider: 'wechat', + provider: 'WECHAT', state, id: conf.idp.wechat?.clientID }); else oauthLogin({ - provider: 'wechat', + provider: 'WECHAT', url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&state=${state}&scope=snsapi_login&#wechat_redirect` }); }, @@ -91,17 +93,13 @@ const AuthList = () => { if (conf.proxyAddress) oauthProxyLogin({ state, - provider: 'google', - id: googleConf?.clientID as string + provider: 'GOOGLE', + id: googleConf.clientID }); else oauthLogin({ - provider: 'google', - url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${ - googleConf?.clientID as string - }&redirect_uri=${ - conf?.callbackURL - }&response_type=code&state=${state}&scope=${scope}&include_granted_scopes=true` + provider: 'GOOGLE', + url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleConf.clientID}&redirect_uri=${conf.callbackURL}&response_type=code&state=${state}&scope=${scope}&include_granted_scopes=true` }); }, need: conf.idp.google.enabled as boolean @@ -118,20 +116,20 @@ const AuthList = () => { const oauth2Conf = conf?.idp.oauth2; if (conf.proxyAddress) oauthProxyLogin({ - provider: 'oauth2', + provider: 'OAUTH2', state, id: oauth2Conf.clientID as string }); else oauthLogin({ - provider: 'oauth2', + provider: 'OAUTH2', url: `${oauth2Conf?.authURL}?client_id=${oauth2Conf.clientID}&redirect_uri=${oauth2Conf.callbackURL}&response_type=code&state=${state}` }); }, need: conf.idp.oauth2?.enabled as boolean } ]; - }, [conf, generateState, logo, router, setProvider]); + }, [conf, logo, router]); return ( diff --git a/frontend/desktop/src/components/signin/auth/useSms.tsx b/frontend/desktop/src/components/signin/auth/useSms.tsx index a1ab7d6e2f7..6f9401695f7 100644 --- a/frontend/desktop/src/components/signin/auth/useSms.tsx +++ b/frontend/desktop/src/components/signin/auth/useSms.tsx @@ -53,7 +53,7 @@ export default function useSms({ const result1 = await request.post>( '/api/auth/phone/verify', { - phoneNumbers: data.phoneNumber, + id: data.phoneNumber, code: data.verifyCode, inviterId: getInviterId() } @@ -116,7 +116,7 @@ export default function useSms({ try { const cfToken = getCfToken?.(); const res = await request.post>('/api/auth/phone/sms', { - phoneNumbers: getValues('phoneNumber'), + id: getValues('phoneNumber'), cfToken }); if (res.code !== 200 || res.message !== 'successfully') { diff --git a/frontend/desktop/src/components/team/CreateTeam.tsx b/frontend/desktop/src/components/team/CreateTeam.tsx index b9eed46db76..7898fc3db92 100644 --- a/frontend/desktop/src/components/team/CreateTeam.tsx +++ b/frontend/desktop/src/components/team/CreateTeam.tsx @@ -20,7 +20,7 @@ import useSessionStore from '@/stores/session'; import { createRequest } from '@/api/namespace'; import { useCustomToast } from '@/hooks/useCustomToast'; import { ApiResp } from '@/types'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { AddIcon, GroupAddIcon } from '@sealos/ui'; export default function CreateTeam({ textButton = false }: { textButton?: boolean }) { const { onOpen, isOpen, onClose } = useDisclosure(); diff --git a/frontend/desktop/src/components/team/DissolveTeam.tsx b/frontend/desktop/src/components/team/DissolveTeam.tsx index 1461d09d2f5..84d05a883e8 100644 --- a/frontend/desktop/src/components/team/DissolveTeam.tsx +++ b/frontend/desktop/src/components/team/DissolveTeam.tsx @@ -19,7 +19,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { deleteTeamRequest } from '@/api/namespace'; import useSessionStore from '@/stores/session'; import { ApiResp } from '@/types'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { DeleteIcon } from '@sealos/ui'; export default function DissolveTeam({ nsid, diff --git a/frontend/desktop/src/components/team/InviteMember.tsx b/frontend/desktop/src/components/team/InviteMember.tsx index 059adf638dd..0e39fa40cce 100644 --- a/frontend/desktop/src/components/team/InviteMember.tsx +++ b/frontend/desktop/src/components/team/InviteMember.tsx @@ -26,7 +26,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { getInviteCodeRequest, inviteMemberRequest } from '@/api/namespace'; import { vaildManage } from '@/utils/tools'; import { ApiResp } from '@/types'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { GroupAddIcon } from '@sealos/ui'; import { useCopyData } from '@/hooks/useCopyData'; diff --git a/frontend/desktop/src/components/team/ModifyRole.tsx b/frontend/desktop/src/components/team/ModifyRole.tsx index f217a52c8b1..6f40affe524 100644 --- a/frontend/desktop/src/components/team/ModifyRole.tsx +++ b/frontend/desktop/src/components/team/ModifyRole.tsx @@ -21,7 +21,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { modifyRoleRequest } from '@/api/namespace'; import { useCustomToast } from '@/hooks/useCustomToast'; import { ApiResp } from '@/types'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { ExpanMoreIcon } from '@sealos/ui'; export default function ModifyRole({ ns_uid, diff --git a/frontend/desktop/src/components/team/NsListItem.tsx b/frontend/desktop/src/components/team/NsListItem.tsx index 4c8408259bd..7989b41fb00 100644 --- a/frontend/desktop/src/components/team/NsListItem.tsx +++ b/frontend/desktop/src/components/team/NsListItem.tsx @@ -1,6 +1,6 @@ import { Box, Flex, FlexProps, HStack, Text, VStack } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; const NsListItem = ({ isSelected, diff --git a/frontend/desktop/src/components/team/ReciveMessage.tsx b/frontend/desktop/src/components/team/ReciveMessage.tsx index 06b0f1826cf..9c0a1f4b804 100644 --- a/frontend/desktop/src/components/team/ReciveMessage.tsx +++ b/frontend/desktop/src/components/team/ReciveMessage.tsx @@ -16,7 +16,7 @@ import { import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ROLE_LIST, teamMessageDto } from '@/types/team'; import { reciveAction, verifyInviteRequest } from '@/api/namespace'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; export default function ReciveMessage({ message, CloseTipHandler, diff --git a/frontend/desktop/src/components/team/RemoveMember.tsx b/frontend/desktop/src/components/team/RemoveMember.tsx index 1fcaeae1dea..0d82cda3da1 100644 --- a/frontend/desktop/src/components/team/RemoveMember.tsx +++ b/frontend/desktop/src/components/team/RemoveMember.tsx @@ -19,7 +19,7 @@ import { CancelIcon, DeleteIcon } from '@sealos/ui'; import { removeMemberRequest } from '@/api/namespace'; import { useCustomToast } from '@/hooks/useCustomToast'; import { ApiResp } from '@/types'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; export default function RemoveMember({ ns_uid, diff --git a/frontend/desktop/src/components/team/TeamCenter.tsx b/frontend/desktop/src/components/team/TeamCenter.tsx index 875a5968bad..87ff4975cbd 100644 --- a/frontend/desktop/src/components/team/TeamCenter.tsx +++ b/frontend/desktop/src/components/team/TeamCenter.tsx @@ -31,9 +31,8 @@ import { InvitedStatus, NSType, UserRole, teamMessageDto } from '@/types/team'; import { TeamUserDto } from '@/types/user'; import ReciveMessage from './ReciveMessage'; import { nsListRequest, reciveMessageRequest, teamDetailsRequest } from '@/api/namespace'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { CopyIcon, ListIcon, SettingIcon, StorageIcon } from '@sealos/ui'; -import { GetUserDefaultNameSpace } from '@/services/backend/kubernetes/user'; import NsListItem from '@/components/team/NsListItem'; export default function TeamCenter(props: StackProps) { diff --git a/frontend/desktop/src/components/team/WorkspaceToggle.tsx b/frontend/desktop/src/components/team/WorkspaceToggle.tsx index 13c579a7f87..be64906f934 100644 --- a/frontend/desktop/src/components/team/WorkspaceToggle.tsx +++ b/frontend/desktop/src/components/team/WorkspaceToggle.tsx @@ -27,7 +27,6 @@ export default function WorkspaceToggle() { async onSuccess(data) { if (data.code === 200 && !!data.data && session) { const payload = jwtDecode(data.data.token); - console.log(payload, session); await sessionConfig({ ...data.data, kubeconfig: switchKubeconfigNamespace(session.kubeconfig, payload.workspaceId) diff --git a/frontend/desktop/src/components/team/userTable.tsx b/frontend/desktop/src/components/team/userTable.tsx index 2c5165252bb..09d12ef0781 100644 --- a/frontend/desktop/src/components/team/userTable.tsx +++ b/frontend/desktop/src/components/team/userTable.tsx @@ -18,7 +18,7 @@ import { vaildManage } from '@/utils/tools'; import RemoveMember from './RemoveMember'; import Abdication from './Abdication'; import ModifyRole from './ModifyRole'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from 'next-i18next'; import { useConfigStore } from '@/stores/config'; export default function UserTable({ diff --git a/frontend/desktop/src/hooks/useTimer.ts b/frontend/desktop/src/hooks/useTimer.ts new file mode 100644 index 00000000000..32a2a47d29f --- /dev/null +++ b/frontend/desktop/src/hooks/useTimer.ts @@ -0,0 +1,32 @@ +import { useState, useEffect } from 'react'; + +export const useTimer = ({ duration, step }: { duration: number; step: number }) => { + const [seconds, setSeconds] = useState(0); + const [start, setStart] = useState(seconds > 0); + useEffect(() => { + if (!start) return; + const interval = setInterval(() => { + // autoClear + if (seconds >= duration || !start) { + setSeconds(0); + setStart(false); + clearInterval(interval); + } + setSeconds((prevSeconds) => prevSeconds + step); + }, step * 1000); + + return () => clearInterval(interval); + }, [duration, seconds, start, step]); + + const resetTimer = () => { + setSeconds(0); + setStart(false); + }; + const startTimer = () => { + setStart(true); + }; + const stopTimer = () => { + setStart(true); + }; + return { seconds, resetTimer, startTimer, isRunning: start, stopTimer }; +}; diff --git a/frontend/desktop/src/pages/api/account/updateGuide.ts b/frontend/desktop/src/pages/api/account/updateGuide.ts index c3ef0606d69..8716e1afd02 100644 --- a/frontend/desktop/src/pages/api/account/updateGuide.ts +++ b/frontend/desktop/src/pages/api/account/updateGuide.ts @@ -30,7 +30,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) jsonRes(res, { data: reuslt?.body }); } catch (error) { - console.log(error, 'get user account err'); jsonRes(res, { code: 500, data: error }); } } diff --git a/frontend/desktop/src/pages/api/auth/delete/checkResource.ts b/frontend/desktop/src/pages/api/auth/delete/checkResource.ts new file mode 100644 index 00000000000..84be09e030c --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/delete/checkResource.ts @@ -0,0 +1,16 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken, filterAuthenticationToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { resourceGuard } from '@/services/backend/middleware/checkResource'; +import { jsonRes } from '@/services/backend/response'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAuthenticationToken(req, res, async ({ userUid }) => { + await resourceGuard(userUid)(res, () => { + jsonRes(res, { + code: 200, + message: RESOURCE_STATUS.RESULT_SUCCESS + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/delete/index.ts b/frontend/desktop/src/pages/api/auth/delete/index.ts new file mode 100644 index 00000000000..fc2d3053d0d --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/delete/index.ts @@ -0,0 +1,20 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { + otherRegionResourceGuard, + resourceGuard +} from '@/services/backend/middleware/checkResource'; +import { accountBalanceGuard } from '@/services/backend/middleware/amount'; +import { deleteUserSvc } from '@/services/backend/svc/deleteUser'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAccessToken(req, res, async ({ userUid, userId, userCrName }) => { + await accountBalanceGuard(userUid)(res, async () => { + await resourceGuard(userUid)(res, async () => { + await otherRegionResourceGuard(userUid, userId)(res, async () => { + await deleteUserSvc(userUid)(res); + }); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/bind/sms.ts b/frontend/desktop/src/pages/api/auth/email/bind/sms.ts new file mode 100644 index 00000000000..fac8154e629 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/bind/sms.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { enableEmailSms } from '@/services/enable'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { filterCf, filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableEmailSms()) { + throw new Error('SMS is not enabled'); + } + await filterCf(req, res, async () => { + await filterAccessToken(req, res, () => + filterEmailParams(req, res, ({ email }) => + sendEmailCodeGuard(email)(res, () => sendEmailCodeSvc(email)(res)) + ) + ); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts new file mode 100644 index 00000000000..9c1f568d09c --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts @@ -0,0 +1,23 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { verifyEmailCodeGuard, filterEmailVerifyParams } from '@/services/backend/middleware/sms'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { bindEmailSvc, bindPhoneSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindEmailGuard, bindPhoneGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterEmailVerifyParams(req, res, async ({ email, code }) => { + await verifyEmailCodeGuard(email, code)(res, () => + bindEmailGuard(email, userUid)(res, () => bindEmailSvc(email, userUid)(res)) + ); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts new file mode 100644 index 00000000000..faf1647b9e6 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts @@ -0,0 +1,24 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + verifyCodeUidGuard, + filterEmailParams, + sendEmailCodeGuard, + filterCodeUid, + sendNewEmailCodeGuard +} from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, ({ userUid }) => + filterEmailParams(req, res, ({ email }) => + filterCodeUid(req, res, ({ uid }) => + sendNewEmailCodeGuard(uid, email)(res, () => sendEmailCodeSvc(email)(res)) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts new file mode 100644 index 00000000000..08958e261ee --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts @@ -0,0 +1,20 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/services/backend/response'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { sendEmailCodeGuard, filterEmailParams } from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, ({ userUid }) => + filterEmailParams(req, res, ({ email }) => + unbindEmailGuard(email, userUid)(res, () => + sendEmailCodeGuard(email)(res, () => sendEmailCodeSvc(email)(res)) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts new file mode 100644 index 00000000000..b9e6c7d7356 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts @@ -0,0 +1,33 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + verifyCodeUidGuard, + verifyEmailCodeGuard, + filterEmailVerifyParams, + filterCodeUid +} from '@/services/backend/middleware/sms'; +import { changeEmailBindingSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindEmailGuard, unbindEmailGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, ({ userUid }) => + filterEmailVerifyParams(req, res, ({ email, code }) => + filterCodeUid(req, res, ({ uid }) => + verifyCodeUidGuard(uid)(res, ({ smsInfo: oldEmailInfo }) => + verifyEmailCodeGuard(email, code)(res, ({ smsInfo: newEmailInfo }) => + unbindEmailGuard(oldEmailInfo.id, userUid)(res, () => + bindEmailGuard(newEmailInfo.id, userUid)(res, () => + changeEmailBindingSvc(oldEmailInfo.id, newEmailInfo.id, userUid)(res) + ) + ) + ) + ) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts new file mode 100644 index 00000000000..cb3adc68db8 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts @@ -0,0 +1,31 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/services/backend/response'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterEmailVerifyParams(req, res, async ({ email, code }) => { + await unbindEmailGuard(email, userUid)(res, async () => { + await verifyEmailCodeGuard(email, code)(res, async ({ smsInfo }) => { + return jsonRes(res, { + code: 200, + message: 'Successfully', + data: { + uid: smsInfo.uid + } + }); + }); + }); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts new file mode 100644 index 00000000000..7c04a5f4bcd --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts @@ -0,0 +1,22 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { sendEmailCodeGuard, filterEmailParams, filterCf } from '@/services/backend/middleware/sms'; +import { enableSms } from '@/services/enable'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, ({ userUid }) => + filterCf(req, res, async () => + filterEmailParams(req, res, ({ email }) => + unbindEmailGuard(email, userUid)(res, () => + sendEmailCodeGuard(email)(res, () => sendEmailCodeSvc(email)(res)) + ) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts new file mode 100644 index 00000000000..aef6f20822a --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts @@ -0,0 +1,19 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { verifyEmailCodeGuard, filterEmailVerifyParams } from '@/services/backend/middleware/sms'; +import { unbindEmailSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, ({ userUid }) => + filterEmailVerifyParams(req, res, async ({ email, code }) => + verifyEmailCodeGuard(email, code)(res, async ({ smsInfo }) => + unbindEmailSvc(smsInfo.id, userUid)(res) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/info.ts b/frontend/desktop/src/pages/api/auth/info.ts index d4d5f46534e..85ebadca940 100644 --- a/frontend/desktop/src/pages/api/auth/info.ts +++ b/frontend/desktop/src/pages/api/auth/info.ts @@ -1,6 +1,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/services/backend/response'; import { globalPrisma, prisma } from '@/services/backend/db/init'; +import { ProviderType } from 'prisma/global/generated/client'; import { verifyAccessToken } from '@/services/backend/auth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -20,6 +21,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) globalPrisma.user.findUnique({ where: { uid: regionUser.userUid + }, + include: { + oauthProvider: { + select: { + providerType: true, + providerId: true + } + } } }) ]); @@ -28,14 +37,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) code: 404, message: 'Not found' }); - else - return jsonRes(res, { - code: 200, - message: 'Successfully', - data: { - info: globalData - } - }); + const info = { + ...globalData, + oauthProvider: globalData.oauthProvider.map((o) => ({ + providerType: o.providerType, + providerId: ( + [ProviderType.PHONE, ProviderType.PASSWORD, ProviderType.EMAIL] as ProviderType[] + ).includes(o.providerType) + ? o.providerId + : '' + })) + }; + return jsonRes(res, { + code: 200, + message: 'Successfully', + data: { + info + } + }); } catch (err) { console.log(err); return jsonRes(res, { diff --git a/frontend/desktop/src/pages/api/auth/mergeUser.ts b/frontend/desktop/src/pages/api/auth/mergeUser.ts new file mode 100644 index 00000000000..9366ce71879 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/mergeUser.ts @@ -0,0 +1,22 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { accountBalanceGuard } from '@/services/backend/middleware/amount'; +import { filterMergeUser, mergeUserGuard } from '@/services/backend/middleware/mergeUser'; +import { mergeUserSvc } from '@/services/backend/svc/mergeUser'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAccessToken(req, res, async ({ userUid, userId, userCrName }) => { + await filterMergeUser(req, res, async ({ providerId, providerType }) => { + await mergeUserGuard( + userUid, + providerType, + providerId + )(res, async ({ mergeUserUid }) => { + await accountBalanceGuard(mergeUserUid)(res, async () => { + await mergeUserSvc(userUid, mergeUserUid)(res); + }); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts b/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts index 5d1d50c226e..568a8639663 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts @@ -1,5 +1,5 @@ import { jsonRes } from '@/services/backend/response'; -import { modifyTeamRole } from '@/services/backend/team'; +import { modifyWorkspaceRole } from '@/services/backend/team'; import { UserRole } from '@/types/team'; import { NextApiRequest, NextApiResponse } from 'next'; import { prisma } from '@/services/backend/db/init'; @@ -43,7 +43,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!target || target.status !== JoinStatus.IN_WORKSPACE) return jsonRes(res, { code: 404, message: 'The targetUser is not in namespace' }); // modify K8S - await modifyTeamRole({ + await modifyWorkspaceRole({ action: 'Change', pre_k8s_username: payload.userCrUid, k8s_username: target.userCr.crName, diff --git a/frontend/desktop/src/pages/api/auth/namespace/create.ts b/frontend/desktop/src/pages/api/auth/namespace/create.ts index 65ae25c3c1d..be19019f99f 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/create.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/create.ts @@ -1,7 +1,7 @@ import { getTeamKubeconfig } from '@/services/backend/kubernetes/admin'; import { GetUserDefaultNameSpace } from '@/services/backend/kubernetes/user'; import { jsonRes } from '@/services/backend/response'; -import { bindingRole, modifyTeamRole } from '@/services/backend/team'; +import { bindingRole, modifyWorkspaceRole } from '@/services/backend/team'; import { getTeamLimit } from '@/services/enable'; import { NSType, NamespaceDto, UserRole } from '@/types/team'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -19,7 +19,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!teamName) return jsonRes(res, { code: 400, message: 'teamName is required' }); const currentNamespaces = await prisma.userWorkspace.findMany({ where: { - userCrUid: payload.userCrUid + userCrUid: payload.userCrUid, + status: 'IN_WORKSPACE' }, include: { workspace: { @@ -65,7 +66,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) direct: true }); if (!utnResult) throw new Error(`fail to binding namesapce: ${workspace.id}`); - await modifyTeamRole({ + await modifyWorkspaceRole({ role: UserRole.Owner, action: 'Create', workspaceId, diff --git a/frontend/desktop/src/pages/api/auth/namespace/delete.ts b/frontend/desktop/src/pages/api/auth/namespace/delete.ts index 9e24b7514e7..4547d14fe42 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/delete.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/delete.ts @@ -4,7 +4,7 @@ import { applyDeleteRequest } from '@/services/backend/team'; import { NextApiRequest, NextApiResponse } from 'next'; import { prisma } from '@/services/backend/db/init'; import { validate } from 'uuid'; -import { JoinStatus, Role } from 'prisma/region/generated/client'; +import { Role } from 'prisma/region/generated/client'; import { verifyAccessToken } from '@/services/backend/auth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -40,13 +40,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!res1) throw new Error('fail to update user '); const res2 = await applyDeleteRequest(creator); if (!res2) throw new Error('fail to delete namespace '); - const results = await prisma.userWorkspace.updateMany({ + const results = await prisma.userWorkspace.deleteMany({ where: { workspaceUid: ns_uid, isPrivate: false - }, - data: { - status: JoinStatus.NOT_IN_WORKSPACE } }); diff --git a/frontend/desktop/src/pages/api/auth/namespace/details.ts b/frontend/desktop/src/pages/api/auth/namespace/details.ts index 8750bece674..ca3ecaec173 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/details.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/details.ts @@ -47,19 +47,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) teamName: workspace.displayName, nstype: NSType.Team }; - const users = queryResult.map((x) => { - const user = userResult.find((user) => user.uid === x.userCr.userUid)!; - return { - uid: x.userCr.userUid, - crUid: x.userCrUid, - k8s_username: x.userCr.crName, - avatarUrl: user.avatarUri, - nickname: user.nickname, - createdTime: x.userCr.createdAt.toString(), - joinTime: x.joinAt || undefined, - role: roleToUserRole(x.role), - status: joinStatusToNStatus(x.status) - }; + const users = queryResult.flatMap((x) => { + const user = userResult.find((user) => user.uid === x.userCr.userUid); + // when the user is deleting + if (!user) return []; + return [ + { + uid: x.userCr.userUid, + crUid: x.userCrUid, + k8s_username: x.userCr.crName, + avatarUrl: user.avatarUri, + nickname: user.nickname, + createdTime: x.userCr.createdAt.toString(), + joinTime: x.joinAt || undefined, + role: roleToUserRole(x.role), + status: joinStatusToNStatus(x.status) + } + ]; }); jsonRes(res, { code: 200, diff --git a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts index 00aa8bca42a..4b800d6babf 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts @@ -1,6 +1,6 @@ import { verifyAccessToken } from '@/services/backend/auth'; import { jsonRes } from '@/services/backend/response'; -import { modifyBinding, modifyTeamRole } from '@/services/backend/team'; +import { modifyBinding, modifyWorkspaceRole } from '@/services/backend/team'; import { UserRole } from '@/types/team'; import { isUserRole, roleToUserRole, vaildManage } from '@/utils/tools'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -52,7 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (roleToUserRole(targetUser.role) === tRole) return jsonRes(res, { code: 200, message: 'Successfully' }); - await modifyTeamRole({ + await modifyWorkspaceRole({ k8s_username: targetUser.userCr.crName, role: tRole, action: 'Modify', @@ -71,6 +71,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } catch (e) { console.log(e); - jsonRes(res, { code: 500, message: 'fail to remove team member' }); + jsonRes(res, { code: 500, message: 'fail to modify member role' }); } } diff --git a/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts b/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts index bfac7a05c42..d2145af7fdb 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts @@ -1,5 +1,5 @@ import { jsonRes } from '@/services/backend/response'; -import { modifyTeamRole, unbindingRole } from '@/services/backend/team'; +import { modifyWorkspaceRole, unbindingRole } from '@/services/backend/team'; import { NextApiRequest, NextApiResponse } from 'next'; import { roleToUserRole, vaildManage } from '@/utils/tools'; import { validate } from 'uuid'; @@ -58,7 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } else if (JoinStatus.IN_WORKSPACE === tItem.status) { // modify role - await modifyTeamRole({ + await modifyWorkspaceRole({ k8s_username: tItem.userCr.crName, role: roleToUserRole(tItem.role), action: 'Deprive', diff --git a/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts b/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts index 62604309b71..efab03ddc78 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts @@ -1,6 +1,6 @@ import { reciveAction } from '@/api/namespace'; import { jsonRes } from '@/services/backend/response'; -import { acceptInvite, modifyTeamRole, unbindingRole } from '@/services/backend/team'; +import { acceptInvite, modifyWorkspaceRole, unbindingRole } from '@/services/backend/team'; import { NextApiRequest, NextApiResponse } from 'next'; import { prisma } from '@/services/backend/db/init'; @@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); if (!queryStatus) return jsonRes(res, { code: 404, message: "You're not invited" }); if (action === reciveAction.Accepte) { - await modifyTeamRole({ + await modifyWorkspaceRole({ k8s_username: queryStatus.userCr.crName, role: roleToUserRole(queryStatus.role), workspaceId: queryStatus.workspace.id, diff --git a/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts b/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts index d74bb5ba453..4088660a128 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts @@ -1,6 +1,6 @@ import { reciveAction } from '@/api/namespace'; import { jsonRes } from '@/services/backend/response'; -import { modifyTeamRole } from '@/services/backend/team'; +import { modifyWorkspaceRole } from '@/services/backend/team'; import { NextApiRequest, NextApiResponse } from 'next'; import { prisma } from '@/services/backend/db/init'; import { UserRoleToRole } from '@/utils/tools'; @@ -45,7 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!inviterStatus) return jsonRes(res, { code: 404, message: 'the inviter or the namespace is not found' }); - await modifyTeamRole({ + await modifyWorkspaceRole({ k8s_username: payload.userCrName, role: linkResults.role, workspaceId: inviterStatus.workspace.id, diff --git a/frontend/desktop/src/pages/api/auth/oauth/github/bind.ts b/frontend/desktop/src/pages/api/auth/oauth/github/bind.ts new file mode 100644 index 00000000000..f5c8873abc2 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/oauth/github/bind.ts @@ -0,0 +1,30 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableGithub } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + OauthCodeFilter, + bindGithubGuard, + githubOAuthEnvFilter, + githubOAuthGuard +} from '@/services/backend/middleware/oauth'; +import { bindGithubSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableGithub()) { + throw new Error('github clinet is not defined'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await OauthCodeFilter(req, res, async ({ code }) => { + await githubOAuthEnvFilter()(async ({ clientID, clientSecret }) => { + await githubOAuthGuard( + clientID, + clientSecret, + code + )(res, ({ id }) => + bindGithubGuard(id, userUid)(res, () => bindGithubSvc(id, userUid)(res)) + ); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/github/index.ts b/frontend/desktop/src/pages/api/auth/oauth/github/index.ts index 1ad2b7ad256..2433eed91cc 100644 --- a/frontend/desktop/src/pages/api/auth/oauth/github/index.ts +++ b/frontend/desktop/src/pages/api/auth/oauth/github/index.ts @@ -1,75 +1,32 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import { TgithubToken, TgithubUser } from '@/types/user'; import { enableGithub } from '@/services/enable'; -import { getGlobalToken } from '@/services/backend/globalAuth'; import { persistImage } from '@/services/backend/persistImage'; -import { isNumber } from 'lodash'; import { ProviderType } from 'prisma/global/generated/client'; +import { + OauthCodeFilter, + githubOAuthEnvFilter, + githubOAuthGuard +} from '@/services/backend/middleware/oauth'; +import { getGlobalTokenByGithubSvc } from '@/services/backend/svc/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const clientId = global.AppConfig?.desktop.auth.idp.github?.clientID!; - const clientSecret = global.AppConfig?.desktop.auth.idp.github?.clientSecret!; - try { - if (!enableGithub()) { - throw new Error('github clinet is not defined'); - } - const { code, inviterId } = req.body; - const url = ` https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`; - const __data = (await ( - await fetch(url, { method: 'POST', headers: { Accept: 'application/json' } }) - ).json()) as TgithubToken; - const access_token = __data.access_token; - if (!access_token) { - return jsonRes(res, { - message: 'Failed to authenticate with GitHub', - code: 500, - data: 'access_token is null' - }); - } - const userUrl = `https://api.github.com/user`; - const response = await fetch(userUrl, { - headers: { - Authorization: `Bearer ${access_token}` - } - }); - if (!response.ok) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' - }); - const result = (await response.json()) as TgithubUser; - - const name = result.login; - const id = result.id; - if (!isNumber(id)) throw Error(); - const persistUrl = await persistImage( - result.avatar_url, - 'avatar/' + ProviderType.GITHUB + '/' + result.id - ); - const avatar_url = persistUrl || ''; - const data = await getGlobalToken({ - provider: ProviderType.GITHUB, - id: id + '', - avatar_url, - name, - inviterId - }); - if (!data) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableGithub()) { + throw new Error('github clinet is not defined'); + } + await OauthCodeFilter(req, res, async ({ code, inviterId }) => { + await githubOAuthEnvFilter()(async ({ clientID, clientSecret }) => { + await githubOAuthGuard( + clientID, + clientSecret, + code + )(res, async ({ id, name, avatar_url }) => { + const persistUrl = await persistImage( + avatar_url, + 'avatar/' + ProviderType.GITHUB + '/' + id + ); + await getGlobalTokenByGithubSvc(persistUrl || '', id, name, inviterId)(res); }); - return jsonRes(res, { - data, - code: 200, - message: 'Successfully' }); - } catch (err) { - console.log(err); - return jsonRes(res, { - message: 'Failed to authenticate with GitHub', - code: 500 - }); - } -} + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/github/unbind.ts b/frontend/desktop/src/pages/api/auth/oauth/github/unbind.ts new file mode 100644 index 00000000000..4754f47ad49 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/oauth/github/unbind.ts @@ -0,0 +1,30 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableGithub } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + OauthCodeFilter, + githubOAuthEnvFilter, + githubOAuthGuard, + unbindGithubGuard +} from '@/services/backend/middleware/oauth'; +import { unbindGithubSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableGithub()) { + throw new Error('github clinet is not defined'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await OauthCodeFilter(req, res, async ({ code }) => { + await githubOAuthEnvFilter()(async ({ clientID, clientSecret }) => { + await githubOAuthGuard( + clientID, + clientSecret, + code + )(res, ({ id }) => + unbindGithubGuard(id, userUid)(res, () => unbindGithubSvc(id, userUid)(res)) + ); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/google/bind.ts b/frontend/desktop/src/pages/api/auth/oauth/google/bind.ts new file mode 100644 index 00000000000..31e7db02b56 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/oauth/google/bind.ts @@ -0,0 +1,31 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableGithub, enableGoogle } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + OauthCodeFilter, + bindGoogleGuard, + googleOAuthEnvFilter, + googleOAuthGuard +} from '@/services/backend/middleware/oauth'; +import { bindGoogleSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableGoogle()) { + throw new Error('google svc is not defined'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await OauthCodeFilter(req, res, async ({ code }) => { + await googleOAuthEnvFilter()(async ({ clientID, clientSecret, callbackURL }) => { + await googleOAuthGuard( + clientID, + clientSecret, + code, + callbackURL + )(res, ({ name, id, avatar_url }) => + bindGoogleGuard(id, userUid)(res, () => bindGoogleSvc(id, userUid)(res)) + ); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/google/index.ts b/frontend/desktop/src/pages/api/auth/oauth/google/index.ts index 88d9a7accc4..517b0546ebf 100644 --- a/frontend/desktop/src/pages/api/auth/oauth/google/index.ts +++ b/frontend/desktop/src/pages/api/auth/oauth/google/index.ts @@ -1,80 +1,32 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import * as jwt from 'jsonwebtoken'; import { enableGoogle } from '@/services/enable'; -import { getBase64FromRemote } from '@/utils/tools'; -import { getGlobalToken } from '@/services/backend/globalAuth'; import { persistImage } from '@/services/backend/persistImage'; import { ProviderType } from 'prisma/global/generated/client'; +import { + googleOAuthEnvFilter, + googleOAuthGuard, + OauthCodeFilter +} from '@/services/backend/middleware/oauth'; +import { getGlobalTokenByGoogleSvc } from '@/services/backend/svc/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const clientId = global.AppConfig?.desktop.auth.idp.google?.clientID!; - const clientSecret = global.AppConfig?.desktop.auth.idp.google?.clientSecret!; - const callbackUrl = global.AppConfig?.desktop.auth.callbackURL || ''; - try { - if (!enableGoogle()) { - throw new Error('google clinet is not defined'); - } - const { code, inviterId } = req.body; - if (!code) - return jsonRes(res, { - code: 400, - message: 'The code is required' - }); - const url = `https://oauth2.googleapis.com/token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}&redirect_uri=${callbackUrl}&grant_type=authorization_code`; - const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json' } }); - if (!response.ok) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' - }); - const __data = (await response.json()) as { - access_token: string; - scope: string; - token_type: string; - id_token: string; - }; - const userInfo = jwt.decode(__data.id_token) as { - iss: string; - azp: string; - aud: string; - sub: string; - at_hash: string; - name: string; - picture: string; - given_name: string; - family_name: string; - locale: string; - iat: number; - exp: number; - }; - const name = userInfo.name; - const id = userInfo.sub; - const avatar_url = - (await persistImage(userInfo.picture, 'avatar/' + ProviderType.WECHAT + '/' + id)) || ''; - if (!id) throw new Error('fail to get google openid'); - const data = await getGlobalToken({ - provider: ProviderType.GOOGLE, - id: name, - avatar_url, - name, - inviterId - }); - if (!data) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' - }); - return jsonRes(res, { - data, - code: 200, - message: 'Successfully' - }); - } catch (err) { - console.log(err); - return jsonRes(res, { - message: 'Failed to authenticate with Google', - code: 500 - }); - } -} +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableGoogle()) throw new Error('google clinet is not defined'); + await OauthCodeFilter( + req, + res, + async ({ code, inviterId }) => + await googleOAuthEnvFilter()(async ({ clientID, clientSecret, callbackURL }) => { + await googleOAuthGuard( + clientID, + clientSecret, + code, + callbackURL + )(res, async ({ id, name, avatar_url }) => { + const presistAvatarUrl = + (await persistImage(avatar_url, 'avatar/' + ProviderType.GOOGLE + '/' + id)) || ''; + await getGlobalTokenByGoogleSvc(presistAvatarUrl, id, name, inviterId)(res); + }); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/google/unbind.ts b/frontend/desktop/src/pages/api/auth/oauth/google/unbind.ts new file mode 100644 index 00000000000..478107dd627 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/oauth/google/unbind.ts @@ -0,0 +1,31 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableGithub, enableGoogle } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + OauthCodeFilter, + googleOAuthEnvFilter, + googleOAuthGuard, + unbindGoogleGuard +} from '@/services/backend/middleware/oauth'; +import { unbindGoogleSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableGoogle()) { + throw new Error('google svc is not defined'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await OauthCodeFilter(req, res, async ({ code }) => { + await googleOAuthEnvFilter()(async ({ clientID, clientSecret, callbackURL }) => { + await googleOAuthGuard( + clientID, + clientSecret, + code, + callbackURL + )(res, ({ name, id, avatar_url }) => + unbindGoogleGuard(id, userUid)(res, () => unbindGoogleSvc(id, userUid)(res)) + ); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts b/frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts index 4a20fba3b0e..6628bd75398 100644 --- a/frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts +++ b/frontend/desktop/src/pages/api/auth/oauth/oauth2/index.ts @@ -1,4 +1,5 @@ import { getGlobalToken } from '@/services/backend/globalAuth'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { jsonRes } from '@/services/backend/response'; import { enableOAuth2 } from '@/services/enable'; import { OAuth2Type, OAuth2UserInfoType } from '@/types/user'; @@ -8,83 +9,74 @@ import { ProviderType } from 'prisma/global/generated/client'; const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12); //OAuth2 Support client_secret_post method to obtain token -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { const clientId = global.AppConfig?.desktop.auth.idp.oauth2?.clientID!; const clientSecret = global.AppConfig?.desktop.auth.idp.oauth2?.clientSecret!; const tokenUrl = global.AppConfig?.desktop.auth.idp.oauth2?.tokenURL; const userInfoUrl = global.AppConfig?.desktop.auth.idp.oauth2?.userInfoURL; const redirectUrl = global.AppConfig?.desktop.auth.callbackURL; - try { - if (!enableOAuth2() || !redirectUrl) { - throw new Error('District related env'); - } - - const { code, inviterId } = req.body; - const url = `${tokenUrl}`; - const oauth2Data = (await ( - await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - code, - client_id: clientId, - client_secret: clientSecret, - grant_type: 'authorization_code', - redirect_uri: redirectUrl - }) + if (!enableOAuth2() || !redirectUrl) { + throw new Error('District related env'); + } + const { code, inviterId } = req.body; + const url = `${tokenUrl}`; + const oauth2Data = (await ( + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + code, + client_id: clientId, + client_secret: clientSecret, + grant_type: 'authorization_code', + redirect_uri: redirectUrl }) - ).json()) as OAuth2Type; - const access_token = oauth2Data.access_token; + }) + ).json()) as OAuth2Type; + const access_token = oauth2Data.access_token; - if (!access_token) { - return jsonRes(res, { - message: 'Failed to authenticate', - code: 500, - data: 'access_token is null' - }); - } - - const userUrl = `${userInfoUrl}?access_token=${access_token}`; - const response = await fetch(userUrl, { - headers: { - Authorization: `Bearer ${access_token}` - } + if (!access_token) { + return jsonRes(res, { + message: 'Failed to authenticate', + code: 500, + data: 'access_token is null' }); - if (!response.ok) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' - }); - const result = (await response.json()) as OAuth2UserInfoType; - - const id = result.sub; - const name = result?.nickname || result?.name || nanoid(8); - const avatar_url = result?.picture || ''; + } - const data = await getGlobalToken({ - provider: ProviderType.OAUTH2, - id: id + '', - avatar_url, - name, - inviterId - }); - if (!data) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' - }); + const userUrl = `${userInfoUrl}?access_token=${access_token}`; + const response = await fetch(userUrl, { + headers: { + Authorization: `Bearer ${access_token}` + } + }); + if (!response.ok) return jsonRes(res, { - data, - code: 200, - message: 'Successfully' + code: 401, + message: 'Unauthorized' }); - } catch (err) { - console.log(err); + const result = (await response.json()) as OAuth2UserInfoType; + + const id = result.sub; + const name = result?.nickname || result?.name || nanoid(8); + const avatar_url = result?.picture || ''; + + const data = await getGlobalToken({ + provider: ProviderType.OAUTH2, + providerId: id + '', + avatar_url, + name, + inviterId + }); + if (!data) return jsonRes(res, { - message: 'Failed to authenticate with GitHub', - code: 500 + code: 401, + message: 'Unauthorized' }); - } -} + return jsonRes(res, { + data, + code: 200, + message: 'Successfully' + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/wechat/bind.ts b/frontend/desktop/src/pages/api/auth/oauth/wechat/bind.ts new file mode 100644 index 00000000000..1dfcbd8c7d3 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/oauth/wechat/bind.ts @@ -0,0 +1,30 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableWechat } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + bindWechatGuard, + OauthCodeFilter, + wechatOAuthEnvFilter, + wechatOAuthGuard +} from '@/services/backend/middleware/oauth'; +import { bindWechatSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableWechat()) { + throw new Error('github clinet is not defined'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await OauthCodeFilter(req, res, async ({ code }) => { + await wechatOAuthEnvFilter()(async ({ clientID, clientSecret }) => { + await wechatOAuthGuard( + clientID, + clientSecret, + code + )(res, ({ name, id, avatar_url }) => + bindWechatGuard(id, userUid)(res, () => bindWechatSvc(id, userUid)(res)) + ); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/wechat/index.ts b/frontend/desktop/src/pages/api/auth/oauth/wechat/index.ts index fee46d8f077..c0a5aca8124 100644 --- a/frontend/desktop/src/pages/api/auth/oauth/wechat/index.ts +++ b/frontend/desktop/src/pages/api/auth/oauth/wechat/index.ts @@ -1,64 +1,31 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/services/backend/response'; -import { TWechatToken, TWechatUser } from '@/types/user'; import { enableWechat } from '@/services/enable'; -import { getGlobalToken } from '@/services/backend/globalAuth'; import { persistImage } from '@/services/backend/persistImage'; import { ProviderType } from 'prisma/global/generated/client'; +import { + OauthCodeFilter, + wechatOAuthEnvFilter, + wechatOAuthGuard +} from '@/services/backend/middleware/oauth'; +import { getGlobalTokenByWechatSvc } from '@/services/backend/svc/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const APP_ID = global.AppConfig?.desktop.auth.idp.wechat?.clientID!; - const APP_SECRET = global.AppConfig?.desktop.auth.idp.wechat?.clientSecret!; - try { - if (!enableWechat()) { - throw new Error('wechat clinet is not defined'); - } - const { code, inviterId } = req.body; - const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${APP_ID}&secret=${APP_SECRET}&code=${code}&grant_type=authorization_code`; - const { access_token, openid } = (await ( - await fetch(url, { headers: { Accept: 'application/json' } }) - ).json()) as TWechatToken; - - if (!access_token || !openid) { - return jsonRes(res, { - message: 'Failed to authenticate with wechat', - code: 400, - data: 'access_token is null' - }); - } - - const userUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`; - const response = await fetch(userUrl); - if (!response.ok) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' - }); - const { nickname: name, unionid: id, headimgurl } = (await response.json()) as TWechatUser; - const avatar_url = - (await persistImage(headimgurl, 'avatar/' + ProviderType.WECHAT + '/' + id)) || ''; - const data = await getGlobalToken({ - provider: ProviderType.WECHAT, - id, - avatar_url, - name, - inviterId - }); - if (!data) - return jsonRes(res, { - code: 401, - message: 'Unauthorized' +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableWechat()) { + throw new Error('wechat clinet is not defined'); + } + await OauthCodeFilter(req, res, async ({ code, inviterId }) => { + await wechatOAuthEnvFilter()(async ({ clientID, clientSecret }) => { + await wechatOAuthGuard( + clientID, + clientSecret, + code + )(res, async ({ id, name, avatar_url }) => { + const persistUrl = + (await persistImage(avatar_url, 'avatar/' + ProviderType.WECHAT + '/' + id)) || ''; + await getGlobalTokenByWechatSvc(persistUrl, id, name, inviterId)(res); }); - return jsonRes(res, { - data, - code: 200, - message: 'Successfully' }); - } catch (err) { - console.log(err); - jsonRes(res, { - message: 'Failed to authenticate with wechat', - code: 500 - }); - } -} + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/oauth/wechat/unbind.ts b/frontend/desktop/src/pages/api/auth/oauth/wechat/unbind.ts new file mode 100644 index 00000000000..d1dc96b8559 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/oauth/wechat/unbind.ts @@ -0,0 +1,30 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableWechat } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + OauthCodeFilter, + unbindWechatGuard, + wechatOAuthEnvFilter, + wechatOAuthGuard +} from '@/services/backend/middleware/oauth'; +import { unbindWechatSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableWechat()) { + throw new Error('github clinet is not defined'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await OauthCodeFilter(req, res, async ({ code }) => { + await wechatOAuthEnvFilter()(async ({ clientID, clientSecret }) => { + await wechatOAuthGuard( + clientID, + clientSecret, + code + )(res, ({ id }) => + unbindWechatGuard(id, userUid)(res, () => unbindWechatSvc(id, userUid)(res)) + ); + }); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/password/exist.ts b/frontend/desktop/src/pages/api/auth/password/exist.ts index 476913e5780..7a6218bba65 100644 --- a/frontend/desktop/src/pages/api/auth/password/exist.ts +++ b/frontend/desktop/src/pages/api/auth/password/exist.ts @@ -20,8 +20,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); } - // const userRes = await findUser({realUserUid: payload.realUserUid}) - // const passwordProvider = userRes?.oauthProvider.find(val=>val.providerType === ProviderType.PASSWORD) const isExist = await globalPrisma.oauthProvider.findUnique({ where: { providerId_providerType: { diff --git a/frontend/desktop/src/pages/api/auth/password/index.ts b/frontend/desktop/src/pages/api/auth/password/index.ts index 9eac9ea76cf..1301cc57332 100644 --- a/frontend/desktop/src/pages/api/auth/password/index.ts +++ b/frontend/desktop/src/pages/api/auth/password/index.ts @@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } const data = await getGlobalToken({ provider: ProviderType.PASSWORD, - id: name, + providerId: name, avatar_url: '', password, name, diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts new file mode 100644 index 00000000000..04b4dfe2f33 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts @@ -0,0 +1,20 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { sendPhoneCodeGuard, filterPhoneParams, filterCf } from '@/services/backend/middleware/sms'; +import { enableSms } from '@/services/enable'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterCf(req, res, async () => { + console.log('filiterAccess'); + await filterAccessToken(req, res, () => + filterPhoneParams(req, res, ({ phoneNumbers: phone }) => + sendPhoneCodeGuard(phone)(res, () => sendPhoneCodeSvc(phone)(res)) + ) + ); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts b/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts new file mode 100644 index 00000000000..56c208cc893 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts @@ -0,0 +1,24 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/services/backend/response'; +import { enableSms } from '@/services/enable'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { bindPhoneSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindPhoneGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterPhoneVerifyParams(req, res, async ({ phoneNumbers, code }) => { + await verifyPhoneCodeGuard(phoneNumbers, code)(res, () => + bindPhoneGuard(phoneNumbers, userUid)(res, () => bindPhoneSvc(phoneNumbers, userUid)(res)) + ); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts new file mode 100644 index 00000000000..5b8e7b4525f --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts @@ -0,0 +1,22 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + filterPhoneParams, + filterCodeUid, + sendNewPhoneCodeGuard +} from '@/services/backend/middleware/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, ({ userUid }) => + filterPhoneParams(req, res, ({ phoneNumbers }) => + filterCodeUid(req, res, ({ uid }) => + sendNewPhoneCodeGuard(uid, phoneNumbers)(res, () => sendPhoneCodeSvc(phoneNumbers)(res)) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts new file mode 100644 index 00000000000..30bd0065e35 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts @@ -0,0 +1,22 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { sendPhoneCodeGuard, filterPhoneParams } from '@/services/backend/middleware/sms'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterPhoneParams(req, res, async ({ phoneNumbers }) => { + await unbindPhoneGuard(phoneNumbers, userUid)(res, () => + sendPhoneCodeGuard(phoneNumbers)(res, async () => sendPhoneCodeSvc(phoneNumbers)(res)) + ); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts new file mode 100644 index 00000000000..822acaa4428 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts @@ -0,0 +1,44 @@ +import next, { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { + verifyCodeUidGuard, + filterCodeUid, + filterPhoneVerifyParams, + verifyPhoneCodeGuard +} from '@/services/backend/middleware/sms'; +import { changePhoneBindingSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindPhoneGuard, unbindPhoneGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterPhoneVerifyParams( + req, + res, + async ({ phoneNumbers, code }) => + await filterCodeUid( + req, + res, + async ({ uid }) => + await verifyCodeUidGuard(uid)(res, async ({ smsInfo: oldPhoneInfo }) => { + await verifyPhoneCodeGuard(phoneNumbers, code)( + res, + async ({ smsInfo: newPhoneInfo }) => + unbindPhoneGuard(oldPhoneInfo.id, userUid)(res, () => + bindPhoneGuard(newPhoneInfo.id, userUid)(res, () => + changePhoneBindingSvc(oldPhoneInfo.id, newPhoneInfo.id, userUid)(res) + ) + ) + ); + }) + ) + ) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts new file mode 100644 index 00000000000..0cde43b58b1 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts @@ -0,0 +1,31 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/services/backend/response'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterPhoneVerifyParams(req, res, async ({ phoneNumbers, code }) => { + await unbindPhoneGuard(phoneNumbers, userUid)(res, async () => { + await verifyPhoneCodeGuard(phoneNumbers, code)(res, async ({ smsInfo: phoneInfo }) => { + return jsonRes(res, { + code: 200, + message: 'Successfully', + data: { + uid: phoneInfo.uid + } + }); + }); + }); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/sms.ts b/frontend/desktop/src/pages/api/auth/phone/sms.ts index bdd427930fa..c1456aebfad 100644 --- a/frontend/desktop/src/pages/api/auth/phone/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/sms.ts @@ -1,112 +1,16 @@ import { NextApiRequest, NextApiResponse } from 'next'; -// import twilio from 'twilio'; -//@ts-ignore -import Dysmsapi, * as dysmsapi from '@alicloud/dysmsapi20170525'; -//@ts-ignore -import * as OpenApi from '@alicloud/openapi-client'; -//@ts-ignore -import * as Util from '@alicloud/tea-util'; -import { jsonRes } from '@/services/backend/response'; -import { addOrUpdateCode, checkSendable } from '@/services/backend/db/verifyCode'; import { enableSms } from '@/services/enable'; -import { retrySerially } from '@/utils/tools'; +import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const accessKeyId = global.AppConfig?.desktop.auth.idp.sms?.ali?.accessKeyID!; - const accessKeySecret = global.AppConfig?.desktop.auth.idp.sms?.ali?.accessKeySecret!; - const templateCode = global.AppConfig?.desktop.auth.idp.sms?.ali?.templateCode!; - const signName = global.AppConfig?.desktop.auth.idp.sms?.ali?.signName!; - const cfSiteKey = global.AppConfig?.common.cfSiteKey!; - const cfVerifyEndpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; - try { - if (!enableSms()) { - throw new Error('SMS is not enabled'); - } - const { phoneNumbers, cfToken } = req.body as { phoneNumbers?: string; cfToken?: string }; - - if (cfSiteKey) { - if (!cfToken) - return jsonRes(res, { - message: 'cfToken is invalid', - code: 400 - }); - const verifyRes = await fetch(cfVerifyEndpoint, { - method: 'POST', - body: `secret=${encodeURIComponent(cfSiteKey)}&response=${encodeURIComponent(cfToken)}`, - headers: { - 'content-type': 'application/x-www-form-urlencoded' - } - }); - const data = await verifyRes.json(); - if (!data.success) - return jsonRes(res, { - message: 'cfToken is invalid', - code: 400 - }); - } - if (!phoneNumbers) - return jsonRes(res, { - message: 'phoneNumbers is invalid', - code: 400 - }); - if (!(await checkSendable({ phone: phoneNumbers }))) { - return jsonRes(res, { - message: 'code already sent', - code: 400 - }); - } - // randomly generate six bit check code - const code = Math.floor(Math.random() * 900000 + 100000).toString(); - const sendSmsRequest = new dysmsapi.SendSmsRequest({ - phoneNumbers, - signName, - templateCode, - templateParam: `{"code":${code}}` - }); - const config = new OpenApi.Config({ - accessKeyId, - accessKeySecret - }); - - const client = new Dysmsapi(config); - const runtime = new Util.RuntimeOptions({}); - const result = await retrySerially(async () => { - try { - const _result = await client.sendSmsWithOptions(sendSmsRequest, runtime); - - if (!_result) { - throw new Error('sms result is null'); - } - if (_result.statusCode !== 200) { - throw new Error(`sms result status code is ${_result.statusCode} - ${_result.body} - ${phoneNumbers}, - ${new Date()} - `); - } - if (_result.body.code !== 'OK') { - throw new Error(` - ${_result.body.message} - ${phoneNumbers}, - ${new Date()}`); - } - return _result; - } catch (error) { - return Promise.reject(error); - } - }, 3); - - // update cache - await addOrUpdateCode({ phone: phoneNumbers, code }); - return jsonRes(res, { - message: 'successfully', - code: 200 - }); - } catch (error) { - console.log(error); - jsonRes(res, { - message: 'Failed to send code', - code: 500 - }); +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); } -} + await filterCf(req, res, async () => { + await filterPhoneParams(req, res, ({ phoneNumbers: phone }) => + sendPhoneCodeGuard(phone)(res, () => sendPhoneCodeSvc(phone)(res)) + ); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts new file mode 100644 index 00000000000..b5aa6c8c757 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts @@ -0,0 +1,29 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { sendPhoneCodeGuard, filterPhoneParams, filterCf } from '@/services/backend/middleware/sms'; +import { enableSms } from '@/services/enable'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken(req, res, async ({ userUid }) => { + await filterCf(req, res, async () => { + await filterPhoneParams( + req, + res, + async ({ phoneNumbers }) => + await unbindPhoneGuard(phoneNumbers, userUid)( + res, + async () => + await sendPhoneCodeGuard(phoneNumbers)(res, async () => { + await sendPhoneCodeSvc(phoneNumbers)(res); + }) + ) + ); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts new file mode 100644 index 00000000000..10edab96ce8 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts @@ -0,0 +1,22 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { enableSms } from '@/services/enable'; +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { unbindPhoneSvc } from '@/services/backend/svc/bindProvider'; +import { ErrorHandler } from '@/services/backend/middleware/error'; + +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); + } + await filterAccessToken( + req, + res, + async ({ userUid }) => + await filterPhoneVerifyParams(req, res, async ({ phoneNumbers, code }) => { + await verifyPhoneCodeGuard(phoneNumbers, code)(res, async ({ smsInfo: phoneInfo }) => { + await unbindPhoneSvc(phoneInfo.id, userUid)(res); + }); + }) + ); +}); diff --git a/frontend/desktop/src/pages/api/auth/phone/verify.ts b/frontend/desktop/src/pages/api/auth/phone/verify.ts index ced07287003..3f867367e89 100644 --- a/frontend/desktop/src/pages/api/auth/phone/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/verify.ts @@ -1,39 +1,16 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import { checkCode } from '@/services/backend/db/verifyCode'; import { enableSms } from '@/services/enable'; -import { getGlobalToken } from '@/services/backend/globalAuth'; -import { ProviderType } from 'prisma/global/generated/client'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { getGlobalTokenByPhoneSvc } from '@/services/backend/svc/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (!enableSms()) { - throw new Error('sms client is not defined'); - } - const { phoneNumbers, code, inviterId } = req.body; - if (!(await checkCode({ phone: phoneNumbers, code }))) { - return jsonRes(res, { - message: 'SMS code is wrong', - code: 400 - }); - } - const data = await getGlobalToken({ - provider: ProviderType.PHONE, - id: phoneNumbers, - name: phoneNumbers, - avatar_url: '', - inviterId - }); - return jsonRes(res, { - data, - code: 200, - message: 'Successfully' - }); - } catch (error) { - console.log(error); - jsonRes(res, { - message: 'Failed to authenticate with phone', - code: 400 - }); +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableSms()) { + throw new Error('SMS is not enabled'); } -} + await filterPhoneVerifyParams(req, res, async ({ phoneNumbers, code, inviterId }) => { + await verifyPhoneCodeGuard(phoneNumbers, code)(res, async ({ smsInfo: phoneInfo }) => { + await getGlobalTokenByPhoneSvc(phoneInfo.id, inviterId)(res); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/publicWechat/getWechatResult.ts b/frontend/desktop/src/pages/api/auth/publicWechat/getWechatResult.ts index d4722b0a22e..fecf0f3a535 100644 --- a/frontend/desktop/src/pages/api/auth/publicWechat/getWechatResult.ts +++ b/frontend/desktop/src/pages/api/auth/publicWechat/getWechatResult.ts @@ -40,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const _data = await getGlobalToken({ provider: ProviderType.WECHAT, - id: userInfo.openid, + providerId: userInfo.openid, avatar_url, name: userInfo?.nickname }); diff --git a/frontend/desktop/src/pages/api/auth/regionToken.ts b/frontend/desktop/src/pages/api/auth/regionToken.ts index af5901664e4..45b8278b23d 100644 --- a/frontend/desktop/src/pages/api/auth/regionToken.ts +++ b/frontend/desktop/src/pages/api/auth/regionToken.ts @@ -1,27 +1,15 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/services/backend/response'; import { getRegionToken } from '@/services/backend/regionAuth'; -import { verifyAuthenticationToken } from '@/services/backend/auth'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const realUser = await verifyAuthenticationToken(req.headers); - - if (!realUser) - return jsonRes(res, { - code: 401, - message: 'invalid token' - }); - const regionData = await getRegionToken(realUser); +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { filterAuthenticationToken } from '@/services/backend/middleware/access'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAuthenticationToken(req, res, async ({ userId, userUid }) => { + const regionData = await getRegionToken({ userId, userUid }); return jsonRes(res, { code: 200, message: 'Successfully', data: regionData }); - } catch (err) { - console.log(err); - return jsonRes(res, { - message: 'Failed to authenticate with globalToken', - code: 500 - }); - } -} + }); +}, 'Failed to authenticate with globalToken'); diff --git a/frontend/desktop/src/pages/api/dev/migrate.ts b/frontend/desktop/src/pages/api/dev/migrate.ts index 8a0d06dfb74..28589664a46 100644 --- a/frontend/desktop/src/pages/api/dev/migrate.ts +++ b/frontend/desktop/src/pages/api/dev/migrate.ts @@ -200,7 +200,6 @@ async function pullUserData() { } // make sure provider sucessfully!!! - console.log('provider', providerInsertData); if (providerInsertData.length > 0) await globalPrisma.oauthProvider .createMany({ diff --git a/frontend/desktop/src/pages/api/platform/getAppConfig.ts b/frontend/desktop/src/pages/api/platform/getAppConfig.ts index b659d81e8f9..1c74133ba50 100644 --- a/frontend/desktop/src/pages/api/platform/getAppConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAppConfig.ts @@ -12,6 +12,12 @@ import { getCloudConfig } from '@/pages/api/platform/getCloudConfig'; import { getAuthClientConfig } from '@/pages/api/platform/getAuthConfig'; import { getLayoutConfig } from '@/pages/api/platform/getLayoutConfig'; import { getCommonClientConfig } from './getCommonConfig'; +import { Cron } from 'croner'; +import { + commitTransactionjob, + finishTransactionJob, + runTransactionjob +} from '@/services/backend/cronjob'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const config = await getAppConfig(); @@ -44,6 +50,33 @@ export async function getAppConfig(): Promise { const commonConf = await getCommonClientConfig(); const layoutConf = await getLayoutConfig(); const conf = genResConfig(cloudConf, authConf, commonConf, layoutConf); + if (!global.commitCroner) { + // console.log('init commit croner'); + global.commitCroner = new Cron('* * * * * *', commitTransactionjob, { + name: 'commitTransactionJob', + catch: (err) => { + // console.log(err); + } + }); + } + if (!global.runCroner) { + // console.log('init run croner'); + global.runCroner = new Cron('* * * * * *', runTransactionjob, { + name: 'runTransactionJob', + catch: (err) => { + // console.log(err); + } + }); + } + if (!global.finishCroner) { + // console.log('init finish croner'); + global.finishCroner = new Cron('* * * * * *', finishTransactionJob, { + name: 'finishTransactionJob', + catch: (err) => { + // console.log(err); + } + }); + } return conf; } catch (error) { console.log('-getAppConfig-', error); diff --git a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts index f41b7a4b572..be538cfa7c8 100644 --- a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts @@ -60,7 +60,7 @@ function genResAuthClientConfig(conf: AuthConfigType) { export async function getAuthClientConfig(): Promise { try { - if (!global.AppConfig) { + if (process.env.NODE_ENV === 'development' || !global.AppConfig) { const filename = process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; global.AppConfig = yaml.load(readFileSync(filename, 'utf-8')) as AppConfigType; diff --git a/frontend/desktop/src/pages/callback.tsx b/frontend/desktop/src/pages/callback.tsx index 827f204794c..06a24d0f6fa 100644 --- a/frontend/desktop/src/pages/callback.tsx +++ b/frontend/desktop/src/pages/callback.tsx @@ -1,89 +1,114 @@ import type { NextPage } from 'next'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; -import request from '@/services/request'; +import { useCallback, useEffect } from 'react'; import useSessionStore from '@/stores/session'; -import { ApiResp } from '@/types'; +import { ApiResp, AppClientConfigType } from '@/types'; import { Flex, Spinner } from '@chakra-ui/react'; import { uploadConvertData } from '@/api/platform'; -import { jwtDecode } from 'jwt-decode'; import { isString } from 'lodash'; -import { getRegionToken, UserInfo } from '@/api/auth'; -import { AccessTokenPayload } from '@/types/token'; +import { bindRequest, getRegionToken, signInRequest, unBindRequest } from '@/api/auth'; import { getInviterId, sessionConfig } from '@/utils/sessionConfig'; - -const Callback: NextPage = () => { +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ProviderType } from 'prisma/global/generated/client'; +import axios from 'axios'; +import request from '@/services/request'; +import { BIND_STATUS } from '@/types/response/bind'; +import { MERGE_USER_READY } from '@/types/response/utils'; +export default function Callback() { const router = useRouter(); - const setSession = useSessionStore((s) => s.setSession); const setProvider = useSessionStore((s) => s.setProvider); const setToken = useSessionStore((s) => s.setToken); const provider = useSessionStore((s) => s.provider); const compareState = useSessionStore((s) => s.compareState); + const { setMergeUserData, setMergeUserStatus } = useCallbackStore(); useEffect(() => { if (!router.isReady) return; let isProxy: boolean = false; (async () => { try { - if (!provider || !['github', 'wechat', 'google', 'oauth2'].includes(provider)) + if (!provider || !['GITHUB', 'WECHAT', 'GOOGLE', 'OAUTH2'].includes(provider)) throw new Error('provider error'); const { code, state } = router.query; if (!isString(code) || !isString(state)) throw new Error('failed to get code and state'); - console.log(encodeURIComponent(state), code, state); - if (!compareState(state)) throw new Error('invalid state'); - // proxy oauth2.0 - const _url = state; - await new Promise((resolve, reject) => { - resolve(new URL(_url)); - }) - .then(async (url) => { - const result = (await ( - await fetch(`/api/auth/canProxy?domain=${url.host}`) - ).json()) as ApiResp<{ containDomain: boolean }>; - isProxy = true; - if (result.data?.containDomain) { - url.searchParams.append('code', code); - console.log(url); - await router.replace(url.toString()); - } + const compareResult = compareState(state); + if (!compareResult.isSuccess) throw new Error('invalid state'); + if (compareResult.action === 'PROXY') { + // proxy oauth2.0, PROXY_URL_[ACTION]_STATE + const [_url, ...ret] = compareResult.statePayload; + await new Promise((resolve, reject) => { + resolve(new URL(decodeURIComponent(_url))); }) - .catch(() => { - Promise.resolve(); - }); - if (isProxy) { - // prevent once token - setProvider(); - isProxy = false; - return; - } - - const data = await request.post< - any, - ApiResp<{ - token: string; - realUser: { - realUserUid: string; - }; - }> - >('/api/auth/oauth/' + provider, { code, inviterId: getInviterId() }); - setProvider(); - if (data.code === 200 && data.data?.token) { - const token = data.data?.token; - setToken(token); - const regionTokenRes = await getRegionToken(); - if (regionTokenRes?.data) { - await sessionConfig(regionTokenRes.data); - uploadConvertData([3]).then( - (res) => { - console.log(res); - }, - (err) => { - console.log(err); + .then(async (url) => { + const result = (await request(`/api/auth/canProxy?domain=${url.host}`)) as ApiResp<{ + containDomain: boolean; + }>; + isProxy = true; + if (result.data?.containDomain) { + url.searchParams.append('code', code); + url.searchParams.append('state', ret.join('_')); + await router.replace(url.toString()); } - ); - await router.replace('/'); + }) + .catch(() => { + Promise.resolve(); + }); + if (isProxy) { + // prevent once token + setProvider(); + isProxy = false; + return; } } else { - throw new Error(); + const { statePayload, action } = compareResult; + // return + if (action === 'LOGIN') { + const data = await signInRequest(provider)({ code, inviterId: getInviterId()! }); + setProvider(); + if (data.code === 200 && data.data?.token) { + const token = data.data?.token; + setToken(token); + const regionTokenRes = await getRegionToken(); + if (regionTokenRes?.data) { + await sessionConfig(regionTokenRes.data); + uploadConvertData([3]).then( + (res) => { + console.log(res); + }, + (err) => { + console.log(err); + } + ); + await router.replace('/'); + } + } else { + throw new Error(); + } + } else if (action === 'BIND') { + const response = await bindRequest(provider)({ code }); + if (response.message === BIND_STATUS.RESULT_SUCCESS) { + setProvider(); + await router.replace('/'); + } else if (response.message === MERGE_USER_READY.MERGE_USER_CONTINUE) { + const code = response.data?.code; + if (!code) return; + setMergeUserData({ + providerType: provider as ProviderType, + code + }); + setMergeUserStatus(MergeUserStatus.CANMERGE); + setProvider(); + await router.replace('/'); + } else if (response.message === MERGE_USER_READY.MERGE_USER_PROVIDER_CONFLICT) { + setMergeUserData(); + setMergeUserStatus(MergeUserStatus.CONFLICT); + setProvider(); + await router.replace('/'); + } + } else if (action === 'UNBIND') { + await unBindRequest(provider)({ code }); + setProvider(); + await router.replace('/'); + } } } catch (error) { console.error(error); @@ -96,5 +121,4 @@ const Callback: NextPage = () => { ); -}; -export default Callback; +} diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index decb872afd3..3f78594ff12 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -13,7 +13,7 @@ import Script from 'next/script'; import { createContext, useEffect, useState } from 'react'; import useCallbackStore from '@/stores/callback'; import FloatButton from '@/components/floating_button'; -import 'react-contexify/dist/ReactContexify.css'; +// import 'react-contexify/dist/ReactContexify.css'; const destination = '/signin'; interface IMoreAppsContext { @@ -126,7 +126,12 @@ export async function getServerSideProps({ req, res, locales }: any) { return { props: { - ...(await serverSideTranslations(local, ['common', 'cloudProviders'], null, locales || [])), + ...(await serverSideTranslations( + local, + ['common', 'cloudProviders', 'error'], + null, + locales || [] + )), sealos_cloud_domain } }; diff --git a/frontend/desktop/src/pages/proxyOAuth.tsx b/frontend/desktop/src/pages/proxyOAuth.tsx index 80e6b746ffe..7fa6fd6901b 100644 --- a/frontend/desktop/src/pages/proxyOAuth.tsx +++ b/frontend/desktop/src/pages/proxyOAuth.tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/router'; import { useEffect } from 'react'; import request from '@/services/request'; import useSessionStore from '@/stores/session'; -import { ApiResp, AuthConfigType } from '@/types'; +import { ApiResp, AppClientConfigType, AuthConfigType } from '@/types'; import { Flex, Spinner } from '@chakra-ui/react'; import { isString } from 'lodash'; import { useQuery } from '@tanstack/react-query'; @@ -10,65 +10,65 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { compareFirstLanguages } from '@/utils/tools'; import { OauthProvider } from '@/types/user'; import { useConfigStore } from '@/stores/config'; +import { getAppConfig } from './api/platform/getAppConfig'; -export default function Callback() { +export default function Callback({ appConfig }: { appConfig: AppClientConfigType }) { const router = useRouter(); const setProvider = useSessionStore((s) => s.setProvider); const generateState = useSessionStore((s) => s.generateState); - const { data: res } = useQuery(['getPlatformEnv'], () => - request>('/api/platform/getLoginConfig') - ); - const conf = res?.data; - const callback_url = conf?.callbackURL; - const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => { - setProvider(provider); - window.location.href = url; - }; + const callback_url = appConfig.desktop.auth.callbackURL; useEffect(() => { - // if (router.isReady) return - console.log('hellow proxy'); + if (!router.isReady) return; + const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => { + setProvider(provider); + window.location.href = url; + }; const oauthProxyState = router.query.oauthProxyState; const oauthProvider = router.query.oauthProxyProvider; + let isProxying = false; // nextjs auto decode let oauthClientId = router.query.oauthProxyClientID; if (!isString(oauthProxyState) || !isString(oauthProvider) || !isString(oauthClientId)) return; - console.log(oauthProvider, oauthProxyState, oauthClientId, callback_url); (async () => { try { - const _url = new URL(oauthProxyState); + if (isProxying) return; + isProxying = true; + const [originUrl, ...res] = oauthProxyState.split('_'); + const _url = new URL(decodeURIComponent(originUrl)); const result = (await ( await fetch(`/api/auth/canProxy?domain=${encodeURIComponent(_url.host)}`, {}) ).json()) as ApiResp<{ containDomain: boolean }>; - console.log(result); if (!result.data?.containDomain) return; - const state = generateState(oauthProxyState); + const state = encodeURIComponent(generateState('PROXY', oauthProxyState)); - if (oauthProvider === 'github') { + if (oauthProvider === 'GITHUB') { await oauthLogin({ - provider: 'github', + provider: 'GITHUB', url: `https://github.com/login/oauth/authorize?client_id=${oauthClientId}&redirect_uri=${callback_url}&scope=user:email%20read:user&state=${state}` }); - } else if (oauthProvider === 'wechat') { + } else if (oauthProvider === 'WECHAT') { await oauthLogin({ - provider: 'wechat', + provider: 'WECHAT', url: `https://open.weixin.qq.com/connect/qrconnect?appid=${oauthClientId}&redirect_uri=${callback_url}&response_type=code&state=${state}&scope=snsapi_login&#wechat_redirect` }); - } else if (oauthProvider === 'google') { + } else if (oauthProvider === 'GOOGLE') { const scope = encodeURIComponent( `https://www.googleapis.com/auth/userinfo.profile openid` ); oauthLogin({ - provider: 'google', + provider: 'GOOGLE', url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${oauthClientId}&redirect_uri=${callback_url}&response_type=code&state=${state}&scope=${scope}&include_granted_scopes=true` }); } } catch (err) { console.error(err); + } finally { + isProxying = false; } })(); return () => { - // abortController.abort() + isProxying = true; }; }, [router, callback_url]); return ( @@ -83,10 +83,12 @@ export async function getServerSideProps({ req, res, locales }: any) { req?.cookies?.NEXT_LOCALE || compareFirstLanguages(req?.headers?.['accept-language'] || 'zh'); res.setHeader('Set-Cookie', `NEXT_LOCALE=${local}; Max-Age=2592000; Secure; SameSite=None`); const sealos_cloud_domain = useConfigStore.getState().cloudConfig?.domain; + const appConfig = await getAppConfig(); return { props: { ...(await serverSideTranslations(local, undefined, null, locales || [])), - sealos_cloud_domain + sealos_cloud_domain, + appConfig } }; } diff --git a/frontend/desktop/src/services/backend/auth.ts b/frontend/desktop/src/services/backend/auth.ts index 145630ecf42..0d8f114ff8d 100644 --- a/frontend/desktop/src/services/backend/auth.ts +++ b/frontend/desktop/src/services/backend/auth.ts @@ -1,7 +1,7 @@ import { IncomingHttpHeaders } from 'http'; import { sign, verify } from 'jsonwebtoken'; import { JWTPayload } from '@/types'; -import { AuthenticationTokenPayload, AccessTokenPayload } from '@/types/token'; +import { AuthenticationTokenPayload, AccessTokenPayload, CronJobTokenPayload } from '@/types/token'; const regionUID = () => global.AppConfig?.cloud.regionUID || '123456789'; const grobalJwtSecret = () => global.AppConfig?.desktop.auth.jwt.global || '123456789'; @@ -17,7 +17,6 @@ const verifyToken = async (header: IncomingHttpHeaders) => { const payload = await verifyJWT(token); return payload; } catch (err) { - console.error(err); return null; } }; @@ -41,7 +40,6 @@ export const verifyAuthenticationToken = async (header: IncomingHttpHeaders) => const payload = await verifyJWT(token, grobalJwtSecret()); return payload; } catch (err) { - console.error(err); return null; } }; @@ -50,10 +48,9 @@ export const verifyJWT = (token?: string, secret? if (!token) return resolve(null); verify(token, secret || regionalJwtSecret(), (err, payload) => { if (err) { - console.log(err); + // console.log(err); resolve(null); } else if (!payload) { - console.log('payload is null'); resolve(null); } else { resolve(payload as T); @@ -66,3 +63,6 @@ export const generateAppToken = (props: AccessTokenPayload) => sign(props, internalJwtSecret(), { expiresIn: '7d' }); export const generateAuthenticationToken = (props: AuthenticationTokenPayload) => sign(props, grobalJwtSecret(), { expiresIn: '60000' }); + +export const generateCronJobToken = (props: CronJobTokenPayload) => + sign(props, internalJwtSecret(), { expiresIn: '60000' }); diff --git a/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts b/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts new file mode 100644 index 00000000000..880cd60f831 --- /dev/null +++ b/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts @@ -0,0 +1,113 @@ +import { setUserDelete } from '../kubernetes/admin'; +import { globalPrisma, prisma } from '../db/init'; +import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; +import { CronJobStatus } from '@/services/backend/cronjob/index'; +import { DeleteUserEvent } from '@/types/db/event'; +import { getRegionUid } from '@/services/enable'; + +export class DeleteUserCrJob implements CronJobStatus { + private userUid = ''; + transactionType = TransactionType.DELETE_USER; + UNIT_TIMEOUT = 3000; + COMMIT_TIMEOUT = 30000; + constructor(private transactionUid: string, private infoUid: string) {} + async init() { + const infoUid = this.infoUid; + const info = await globalPrisma.deleteUserTransactionInfo.findUnique({ + where: { + uid: infoUid + } + }); + if (!info) throw new Error('the transaction info not found'); + + this.userUid = info.userUid; + } + async unit() { + await this.init(); + const userUid = this.userUid; + const userCr = await prisma.userCr.findUnique({ + where: { userUid } + }); + if (!userCr) { + await globalPrisma.eventLog.create({ + data: { + eventName: DeleteUserEvent['_DELETE_USERCR'], + mainId: userUid, + data: JSON.stringify({ + userUid, + regionUid: getRegionUid(), + message: `Because the userCR is not found, deleting user success` + }) + } + }); + return; + // throw new Error('the userCR not found'); + } + const deleteResult = await setUserDelete(userCr.crName); + + if (!deleteResult) { + throw new Error(`delete User not Success`); + } + await globalPrisma.eventLog.create({ + data: { + eventName: DeleteUserEvent['_DELETE_USERCR'], + mainId: userUid, + data: JSON.stringify({ + userUid, + regionUid: getRegionUid(), + message: `delete user success` + }) + } + }); + await prisma.userWorkspace.deleteMany({ + where: { + userCrUid: userCr.uid + } + }); + } + canCommit() { + return true; + } + async commit() { + await this.init(); + const userUid = this.userUid; + if (!userUid) throw Error('uid not found'); + await globalPrisma.$transaction([ + globalPrisma.commitTransactionSet.create({ + data: { + precommitTransactionUid: this.transactionUid + } + }), + globalPrisma.deleteUserLog.create({ + data: { + userUid + } + }), + globalPrisma.eventLog.create({ + data: { + eventName: DeleteUserEvent['_COMMIT'], + mainId: userUid, + data: JSON.stringify({ + userUid, + message: `delete user success` + }) + } + }), + globalPrisma.precommitTransaction.findUniqueOrThrow({ + where: { + uid: this.transactionUid, + status: TransactionStatus.FINISH + } + }), + globalPrisma.precommitTransaction.update({ + where: { + uid: this.transactionUid, + status: TransactionStatus.FINISH + }, + data: { + status: TransactionStatus.COMMITED + } + }) + ]); + } +} diff --git a/frontend/desktop/src/services/backend/cronjob/index.ts b/frontend/desktop/src/services/backend/cronjob/index.ts new file mode 100644 index 00000000000..b8e99a2cceb --- /dev/null +++ b/frontend/desktop/src/services/backend/cronjob/index.ts @@ -0,0 +1,239 @@ +import { globalPrisma } from '../db/init'; +import { + TransactionStatus, + TransactionType, + Prisma as GlobalPrisma +} from 'prisma/global/generated/client'; +import { getRegionUid } from '@/services/enable'; +import dayjs from 'dayjs'; +import { DeleteUserCrJob } from '@/services/backend/cronjob/deleteUserCr'; +import { Prisma } from '@prisma/client/extension'; +import { MergeUserCrJob } from '@/services/backend/cronjob/mergeUserCr'; + +export type CronJobStatus = { + unit: (infoUid: string, transactionUid: string) => Promise; + canCommit: () => boolean; + transactionType: TransactionType; + COMMIT_TIMEOUT: number; + commit: () => Promise; +}; +const TIMEOUT = 5000; +const getJob = ( + transaction: Prisma.Result< + typeof globalPrisma.precommitTransaction, + GlobalPrisma.PrecommitTransactionDefaultArgs, + 'findFirst' + > +) => { + if (!transaction) return null; + if (transaction.transactionType === TransactionType.DELETE_USER) { + return new DeleteUserCrJob(transaction.uid, transaction.infoUid); + } else if (transaction.transactionType === TransactionType.MERGE_USER) { + return new MergeUserCrJob(transaction.uid, transaction.infoUid); + } else { + return null; + } +}; +// ready=>running +// handle one Task per tick +export const runTransactionjob = async () => { + // console.log('run transactionjob', new Date()); + const regionUid = getRegionUid(); + let isTimeoutTransactionDetail = false; + // find task + let transactionDetail = await globalPrisma.transactionDetail.findFirst({ + where: { + regionUid, + status: TransactionStatus.READY + }, + include: { + precommitTransaction: true + }, + orderBy: { + updatedAt: 'asc' + } + }); + if (!transactionDetail) { + transactionDetail = await globalPrisma.transactionDetail.findFirst({ + where: { + regionUid, + // death lock + status: TransactionStatus.RUNNING, + updatedAt: { + lte: dayjs().subtract(TIMEOUT, 'ms').toDate() + } + }, + orderBy: { + updatedAt: 'asc' + }, + include: { + precommitTransaction: true + } + }); + isTimeoutTransactionDetail = true; + } + // not found task + if (!transactionDetail) { + return; + } + const transaction = transactionDetail.precommitTransaction; + + const job = getJob(transaction); + if (!job) return; + + // startingRunning + if ( + transaction.status === TransactionStatus.RUNNING || + transaction.status === TransactionStatus.READY + ) { + if (isTimeoutTransactionDetail) { + await globalPrisma.$transaction([ + // make sure it is not running + globalPrisma.transactionDetail.findUniqueOrThrow({ + where: { + uid: transactionDetail.uid, + updatedAt: { + lte: dayjs().subtract(TIMEOUT, 'ms').toDate() + } + } + }), + globalPrisma.transactionDetail.update({ + where: { + uid: transactionDetail.uid, + status: TransactionStatus.RUNNING + }, + data: { + status: TransactionStatus.RUNNING + } + }) + ]); + } else { + await globalPrisma.$transaction([ + globalPrisma.transactionDetail.findUniqueOrThrow({ + where: { + uid: transactionDetail.uid, + status: TransactionStatus.READY + } + }), + globalPrisma.transactionDetail.update({ + where: { + uid: transactionDetail.uid, + status: TransactionStatus.READY + }, + data: { + status: TransactionStatus.RUNNING + } + }) + ]); + } + } else { + return; + } + // try { + await job.unit(); + await globalPrisma.transactionDetail.update({ + where: { + uid: transactionDetail.uid, + status: TransactionStatus.RUNNING + }, + data: { + status: TransactionStatus.FINISH + } + }); +}; + +// running => finish or error +export const finishTransactionJob = async () => { + // console.log('finish transactionjob', new Date()); + const regionList = await globalPrisma.region.findMany({}); + const transactionList = await globalPrisma.precommitTransaction.findMany({ + where: { + status: { + in: [TransactionStatus.RUNNING, TransactionStatus.READY] + } + }, + include: { + transactionDetail: { + select: { + status: true, + regionUid: true + } + } + } + }); + const needFinishTransactionList = transactionList + .filter((tx) => { + const finishList = tx.transactionDetail.filter((d) => d.status === TransactionStatus.FINISH); + return regionList.every(({ uid }) => finishList.findIndex((f) => f.regionUid === uid) >= 0); + }) + .map((tx) => tx.uid); + if (!needFinishTransactionList) return; + + await globalPrisma.precommitTransaction.updateMany({ + where: { + status: { + in: [TransactionStatus.RUNNING, TransactionStatus.READY] + }, + uid: { + in: needFinishTransactionList + } + }, + data: { + status: TransactionStatus.FINISH + } + }); +}; + +// finish => commited +export const commitTransactionjob = async () => { + // console.log('commit transactionjob', new Date()); + const unCommitedTransaction = await globalPrisma.precommitTransaction.findFirst({ + where: { + status: TransactionStatus.FINISH, + commitTransactionSet: null + }, + orderBy: { + updatedAt: 'asc' + } + }); + if (!unCommitedTransaction) return; + const job = getJob(unCommitedTransaction); + if (!job) return; + // if timeout, mark error + const currentTime = new Date().getTime(); + // the record will be updated when status is updated + if (currentTime - unCommitedTransaction.updatedAt.getTime() > job.COMMIT_TIMEOUT) { + await globalPrisma.$transaction([ + globalPrisma.commitTransactionSet.create({ + data: { + precommitTransactionUid: unCommitedTransaction.uid + } + }), + globalPrisma.precommitTransaction.findUniqueOrThrow({ + where: { + uid: unCommitedTransaction.uid, + status: TransactionStatus.FINISH + } + }), + globalPrisma.precommitTransaction.update({ + where: { + uid: unCommitedTransaction.uid, + status: TransactionStatus.FINISH + }, + data: { + status: TransactionStatus.ERROR + } + }), + globalPrisma.errorPreCommitTransaction.create({ + data: { + transactionUid: unCommitedTransaction.uid, + reason: 'commit timeout' + } + }) + ]); + return; + } + const canCommit = job.canCommit(); + if (!canCommit) return; + else await job.commit(); +}; diff --git a/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts b/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts new file mode 100644 index 00000000000..82832f8643b --- /dev/null +++ b/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts @@ -0,0 +1,254 @@ +import { globalPrisma, prisma } from '../db/init'; +import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { getBillingUrl, getCvmUrl, getRegionUid, getWorkorderUrl } from '@/services/enable'; +import { CronJobStatus } from '@/services/backend/cronjob/index'; +import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; +import { mergeUserModifyBinding, mergeUserWorkspaceRole } from '@/services/backend/team'; +import axios from 'axios'; +import { generateCronJobToken } from '../auth'; +import { MergeUserEvent } from '@/types/db/event'; + +/** + * | | user is exist | user is not exist | + * |mergeUser is exist | merge | update mereUser to user | + * |mergeUser is not exist| return ok | return ok | + */ +export class MergeUserCrJob implements CronJobStatus { + private mergeUserUid = ''; + private userUid: string = ''; + UNIT_TIMEOUT = 3000; + COMMIT_TIMEOUT = 60000; + transactionType = TransactionType.MERGE_USER; + constructor(private transactionUid: string, private infoUid: string) {} + async init() { + const info = await globalPrisma.mergeUserTransactionInfo.findUnique({ + where: { + uid: this.infoUid + } + }); + if (!info) throw new Error('the transaction info not found'); + const { mergeUserUid, userUid } = info; + this.mergeUserUid = mergeUserUid; + this.userUid = userUid; + } + async unit() { + await this.init(); + const mergeUserUid = this.mergeUserUid; + const userUid = this.userUid; + const mergeUserCr = await prisma.userCr.findUnique({ + where: { userUid: mergeUserUid } + }); + + if (!mergeUserCr) { + // the mergeUser is not exist in the current region + await globalPrisma.eventLog.create({ + data: { + eventName: MergeUserEvent['_MERGE_WORKSPACE'], + mainId: userUid, + data: JSON.stringify({ + mergeUserUid, + userUid, + regionUid: getRegionUid(), + message: `Because the mergeUserCR is not found, merge workspace success` + }) + } + }); + return; + // throw new Error('the mergeUserCR is not found'); + } + const userCr = await prisma.userCr.findUnique({ + where: { userUid } + }); + if (!userCr) { + // the user is not exist in the current region + // throw new Error('the userCR is not found'); + await prisma.userCr.update({ + where: { + userUid: mergeUserUid + }, + data: { + userUid + } + }); + await globalPrisma.eventLog.create({ + data: { + eventName: MergeUserEvent['_MERGE_WORKSPACE'], + mainId: userUid, + data: JSON.stringify({ + mergeUserUid, + userUid, + regionUid: getRegionUid(), + message: `Because the userCR is not found, merge workspace success` + }) + } + }); + } else { + const [userWorkspaceList, mergeUserWorkspaceList] = await prisma.$transaction([ + prisma.userWorkspace.findMany({ + where: { + userCrUid: userCr.uid, + status: JoinStatus.IN_WORKSPACE + } + }), + prisma.userWorkspace.findMany({ + where: { + userCrUid: mergeUserCr.uid, + status: JoinStatus.IN_WORKSPACE + }, + include: { + workspace: { + select: { + id: true + } + } + } + }) + ]); + // modify role + await Promise.all( + mergeUserWorkspaceList.map(async ({ role: mergeUserRole, workspaceUid, workspace }) => { + try { + const userWorkspace = userWorkspaceList.find((r) => r.workspaceUid === workspaceUid); + await globalPrisma.eventLog.create({ + data: { + eventName: MergeUserEvent['_MERGE_WORKSPACE'], + mainId: userUid, + data: JSON.stringify({ + mergeUserCrName: mergeUserCr.crName, + userCrName: userCr.crName, + workspaceId: workspace.id, + userUid, + mergeUserUid, + mergeUserRole, + regionUid: getRegionUid(), + userRole: userWorkspace?.role, + message: `merge workspace` + }) + } + }); + // modify k8s resource, the handle is idempotent + await mergeUserWorkspaceRole({ + mergeUserRole, + userRole: userWorkspace?.role, + workspaceId: workspace.id, + mergeUserCrName: mergeUserCr.crName, + userCrName: userCr.crName + }); + // modify db resource + await mergeUserModifyBinding({ + mergeUserCrUid: mergeUserCr.uid, + mergeUserRole, + userCrUid: userCr.uid, + workspaceUid, + userRole: userWorkspace?.role + }); + } catch (err: any) { + console.error(err); + } + }) + ); + } + } + canCommit() { + const baseBillingUrl = getBillingUrl(); + const baseWorkorderUrl = getWorkorderUrl(); + const baseCvmUrl = getCvmUrl(); + return !!baseBillingUrl && !!baseCvmUrl && !!baseWorkorderUrl; + } + async commit() { + await this.init(); + const mergeUserUid = this.mergeUserUid; + const userUid = this.userUid; + if (!mergeUserUid || !userUid) throw Error('uid not found'); + const baseBillingUrl = getBillingUrl(); + const baseWorkorderUrl = getWorkorderUrl(); + const baseCvmUrl = getCvmUrl(); + const billingUrl = baseBillingUrl + '/account/v1alpha1/transfer'; + const workorderUrl = baseWorkorderUrl + '/api/v1/migrate'; + const cvmUrl = baseCvmUrl + '/action/sealos-account-merge'; + // transfer + const [user, mergeUser] = await globalPrisma.$transaction([ + globalPrisma.user.findUniqueOrThrow({ where: { uid: userUid } }), + globalPrisma.user.findUniqueOrThrow({ where: { uid: mergeUserUid } }) + ]); + let finalUserCr = await prisma.userCr.findUnique({ + where: { + userUid: mergeUser.uid + } + }); + if (!finalUserCr) { + finalUserCr = await prisma.userCr.findUnique({ + where: { + userUid + } + }); + if (!finalUserCr) { + throw Error('the userCr is not exist'); + } + } + const kubeConfig = await getUserKubeconfigNotPatch(finalUserCr.crName); + if (!kubeConfig) throw Error('the kubeconfig for ' + finalUserCr.crName + ' is not found'); + const [transferResult, workorderResult, cvmResult] = await Promise.all([ + axios.post(billingUrl, { + kubeConfig, + owner: finalUserCr.crName, + userid: mergeUser.id, + toUser: user.id, + transferAll: true + }), + axios.post(workorderUrl, { + token: generateCronJobToken({ + userUid: user.id, + mergeUserUid: mergeUser.id + }) + }), + axios.post(cvmUrl, { + token: generateCronJobToken({ + userUid: user.uid, + mergeUserUid: mergeUser.uid + }) + }) + ]); + const transferMergeSuccess = transferResult && transferResult.status === 200; + const workorderMergeSuccess = + workorderResult && workorderResult.status === 200 && workorderResult.data.code === 200; + const cvmMergeSuccess = cvmResult && cvmResult.status === 200 && cvmResult.data.data === 'ok'; + if (!transferMergeSuccess || !workorderMergeSuccess || !cvmMergeSuccess) { + throw new Error('commit Error'); + } + await globalPrisma.$transaction([ + globalPrisma.commitTransactionSet.create({ + data: { + precommitTransactionUid: this.transactionUid + } + }), + globalPrisma.deleteUserLog.create({ + data: { + userUid: mergeUserUid + } + }), + globalPrisma.eventLog.create({ + data: { + eventName: MergeUserEvent['_COMMIT'], + mainId: this.userUid, + data: JSON.stringify({ + userUid, + mergeUserUid, + regionUid: getRegionUid(), + message: `from ${mergeUser.id} to ${user.id}, + merge workorder, cloud vm and balance success` + }) + } + }), + globalPrisma.precommitTransaction.update({ + where: { + uid: this.transactionUid + }, + data: { + status: TransactionStatus.COMMITED + } + }) + ]); + } +} diff --git a/frontend/desktop/src/services/backend/db/emailVerifyCode.ts b/frontend/desktop/src/services/backend/db/emailVerifyCode.ts new file mode 100644 index 00000000000..45f242ef8df --- /dev/null +++ b/frontend/desktop/src/services/backend/db/emailVerifyCode.ts @@ -0,0 +1,75 @@ +import { v4 } from 'uuid'; +import { connectToDatabase } from './mongodb'; + +export type TEmailVerification_Codes = { + email: string; + code: string; + uid: string; + createdAt: Date; +}; + +async function connectToCollection() { + const client = await connectToDatabase(); + const collection = client.db().collection('email_verification_codes'); + await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 * 5 }); + await collection.createIndex({ uid: 1 }, { unique: true }); + return collection; +} + +// addOrUpdateCode +export async function addOrUpdateCode({ email, code }: { email: string; code: string }) { + const codes = await connectToCollection(); + const result = await codes.updateOne( + { + email + }, + { + $set: { + code, + createdAt: new Date(), + uid: v4() + } + }, + { + upsert: true + } + ); + return result; +} +// checkCode +export async function checkSendable({ email }: { email: string }) { + const codes = await connectToCollection(); + const result = await codes.findOne({ + email, + createdAt: { + $gt: new Date(new Date().getTime() - 60 * 1000) + } + }); + return !result; +} +// checkCode +export async function checkCode({ email, code }: { email: string; code: string }) { + const codes = await connectToCollection(); + const result = await codes.findOne({ + email, + code, + createdAt: { + $gt: new Date(new Date().getTime() - 5 * 60 * 1000) + } + }); + return result; +} +export async function getInfoByUid({ uid }: { uid: string }) { + const codes = await connectToCollection(); + const result = await codes.findOne({ + uid + }); + return result; +} +export async function deleteByUid({ uid }: { uid: string }) { + const codes = await connectToCollection(); + const result = await codes.deleteOne({ + uid + }); + return result; +} diff --git a/frontend/desktop/src/services/backend/db/mergeUserCode.ts b/frontend/desktop/src/services/backend/db/mergeUserCode.ts new file mode 100644 index 00000000000..cc3c7a67cda --- /dev/null +++ b/frontend/desktop/src/services/backend/db/mergeUserCode.ts @@ -0,0 +1,75 @@ +import { v4 } from 'uuid'; +import { connectToDatabase } from './mongodb'; +import { ProviderType } from 'prisma/global/generated/client'; + +export type TMergeUserCode = { + code: string; + providerId: string; + providerType: ProviderType; + uid: string; + createdAt: Date; +}; + +async function connectToCollection() { + const client = await connectToDatabase(); + const collection = client.db().collection('mergeUserCode'); + await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 * 1 }); + await collection.createIndex({ uid: 1 }, { unique: true }); + await collection.createIndex({ code: 1 }, { unique: true }); + await collection.createIndex({ providerId: 1, providerType: 1 }, { unique: true }); + return collection; +} + +// addOrUpdateCode +export async function addOrUpdateCode({ + providerId, + providerType, + code +}: { + providerId: string; + providerType: ProviderType; + code: string; +}) { + const codes = await connectToCollection(); + const result = await codes.updateOne( + { + providerId, + providerType + }, + { + $set: { + code, + createdAt: new Date(), + uid: v4() + } + }, + { + upsert: true + } + ); + return result; +} +export async function checkCode({ + providerType, + code +}: { + providerType: ProviderType; + code: string; +}) { + const codes = await connectToCollection(); + const result = await codes.findOne({ + providerType, + code, + createdAt: { + $gt: new Date(new Date().getTime() - 1 * 60 * 1000) + } + }); + return result; +} +export async function deleteByUid({ uid }: { uid: string }) { + const codes = await connectToCollection(); + const result = await codes.deleteOne({ + uid + }); + return result; +} diff --git a/frontend/desktop/src/services/backend/db/verifyCode.ts b/frontend/desktop/src/services/backend/db/verifyCode.ts index 6dad9c4b3ae..f7bea40c6bf 100644 --- a/frontend/desktop/src/services/backend/db/verifyCode.ts +++ b/frontend/desktop/src/services/backend/db/verifyCode.ts @@ -1,29 +1,44 @@ +import { v4 } from 'uuid'; import { connectToDatabase } from './mongodb'; - -type TVerification_Codes = { - phone: string; +export type SmsType = 'phone' | 'email'; +export type TVerification_Codes = { + id: string; + smsType: SmsType; code: string; + uid: string; createdAt: Date; }; -async function connectToUserCollection() { +async function connectToCollection() { const client = await connectToDatabase(); - const collection = client.db().collection('verification_codes'); + const collection = client.db().collection('sms_verification_codes'); await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 * 5 }); + await collection.createIndex({ uid: 1 }, { unique: true }); + await collection.createIndex({ id: 1, smsType: 1 }, { unique: true }); return collection; } // addOrUpdateCode -export async function addOrUpdateCode({ phone, code }: { phone: string; code: string }) { - const codes = await connectToUserCollection(); +export async function addOrUpdateCode({ + id, + smsType, + code +}: { + id: string; + code: string; + smsType: SmsType; +}) { + const codes = await connectToCollection(); const result = await codes.updateOne( { - phone + id, + smsType }, { $set: { code, - createdAt: new Date() + createdAt: new Date(), + uid: v4() } }, { @@ -33,27 +48,49 @@ export async function addOrUpdateCode({ phone, code }: { phone: string; code: st return result; } // checkCode -export async function checkSendable({ phone }: { phone: string }) { - const codes = await connectToUserCollection(); +export async function checkSendable({ id, smsType }: { id: string; smsType: SmsType }) { + const codes = await connectToCollection(); const result = await codes.findOne({ - phone, + id, + smsType, createdAt: { - // 在区间范围内找到就是已经发送过了,不能再发了 $gt: new Date(new Date().getTime() - 60 * 1000) } }); return !result; } // checkCode -export async function checkCode({ phone, code }: { phone: string; code: string }) { - const codes = await connectToUserCollection(); +export async function checkCode({ + id, + smsType, + code +}: { + id: string; + code: string; + smsType: SmsType; +}) { + const codes = await connectToCollection(); const result = await codes.findOne({ - phone, + id, + smsType, code, createdAt: { - // 5分钟内有效 $gt: new Date(new Date().getTime() - 5 * 60 * 1000) } }); - return !!result; + return result; +} +export async function getInfoByUid({ uid }: { uid: string }) { + const codes = await connectToCollection(); + const result = await codes.findOne({ + uid + }); + return result; +} +export async function deleteByUid({ uid }: { uid: string }) { + const codes = await connectToCollection(); + const result = await codes.deleteOne({ + uid + }); + return result; } diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index 1b7cddf082b..98100e580bf 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -38,7 +38,7 @@ export const inviteHandler = ({ const secretKey = conf.invite?.lafSecretKey || ''; const baseUrl = conf.invite?.lafBaseURL || ''; - if (inviteEnabled || !baseUrl || inviterId === inviteeId) return; + if (!inviteEnabled || !baseUrl || inviterId === inviteeId) return; const payload = { inviterId, @@ -118,6 +118,7 @@ async function signUp({ user }; } catch (e) { + console.log(e); return null; } } @@ -180,14 +181,14 @@ export async function findUser({ userUid }: { userUid: string }) { } export const getGlobalToken = async ({ provider, - id, + providerId, name, avatar_url, password, inviterId }: { provider: ProviderType; - id: string; + providerId: string; name: string; avatar_url: string; password?: string; @@ -199,7 +200,7 @@ export const getGlobalToken = async ({ where: { providerId_providerType: { providerType: provider, - providerId: id + providerId } } }); @@ -210,7 +211,7 @@ export const getGlobalToken = async ({ if (!_user) { if (!enableSignUp()) throw new Error('Failed to signUp user'); const result = await signUpByPassword({ - id, + id: providerId, name, avatar_url, password @@ -225,7 +226,7 @@ export const getGlobalToken = async ({ } } else { const result = await signInByPassword({ - id, + id: providerId, password }); // password is wrong @@ -237,7 +238,7 @@ export const getGlobalToken = async ({ if (!enableSignUp()) throw new Error('Failed to signUp user'); const result = await signUp({ provider, - id, + id: providerId, name, avatar_url }); @@ -252,7 +253,7 @@ export const getGlobalToken = async ({ } else { const result = await signIn({ provider, - id + id: providerId }); result && (user = result.user); } diff --git a/frontend/desktop/src/services/backend/kubernetes/admin.ts b/frontend/desktop/src/services/backend/kubernetes/admin.ts index a76b5e63034..bef250d9faf 100644 --- a/frontend/desktop/src/services/backend/kubernetes/admin.ts +++ b/frontend/desktop/src/services/backend/kubernetes/admin.ts @@ -212,7 +212,33 @@ async function removeUserTeam(kc: k8s.KubeConfig, k8s_username: string) { ); return k8s_username; } - +async function removeUser(kc: k8s.KubeConfig, k8s_username: string) { + const group = 'user.sealos.io'; + const version = 'v1'; + const plural = 'users'; + const updateTime = k8sFormatTime(new Date()); + const client = kc.makeApiClient(k8s.CustomObjectsApi); + const patchs = [ + { op: 'add', path: '/metadata/labels/user.sealos.io~1status', value: 'Deleted' }, + { op: 'replace', path: '/metadata/labels/updateTime', value: updateTime } + ]; + await client.patchClusterCustomObject( + group, + version, + plural, + k8s_username, + patchs, + undefined, + undefined, + undefined, + { + headers: { + 'Content-Type': 'application/json-patch+json' + } + } + ); + return k8s_username; +} export const getUserCr = async (kc: KubeConfig, name: string) => { const resourceKind = 'User'; const group = 'user.sealos.io'; @@ -295,3 +321,30 @@ export const setUserTeamDelete = async (k8s_username: string) => { }); return body; }; +export const setUserDelete = async (k8s_username: string) => { + const kc = K8sApiDefault(); + const group = 'user.sealos.io'; + const version = 'v1'; + const plural = 'users'; + try { + const userCr = await getUserCr(kc, k8s_username); + // @ts-ignore + if (userCr.metadata?.labels?.['user.sealos.io/status'] === 'Deleted') return true; + } catch (e) { + return true; + } + await removeUser(kc, k8s_username); + + const body = await watchCustomClusterObject({ + kc, + group, + version, + plural, + fn(_, cur) { + return cur?.metadata?.labels?.['user.sealos.io/status'] === 'Deleted'; + }, + name: k8s_username, + interval: 100 + }); + return !!body; +}; diff --git a/frontend/desktop/src/services/backend/middleware/access.ts b/frontend/desktop/src/services/backend/middleware/access.ts new file mode 100644 index 00000000000..4c8ed8ad120 --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/access.ts @@ -0,0 +1,32 @@ +import { jsonRes } from '../response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { verifyAccessToken, verifyAuthenticationToken } from '../auth'; +import { AccessTokenPayload, AuthenticationTokenPayload } from '@/types/token'; + +export const filterAccessToken = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: AccessTokenPayload) => void +) => { + const userData = await verifyAccessToken(req.headers); + if (!userData) + return jsonRes(res, { + code: 401, + message: 'invalid token' + }); + else await Promise.resolve(next(userData)); +}; + +export const filterAuthenticationToken = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: AuthenticationTokenPayload) => Promise +) => { + const userData = await verifyAuthenticationToken(req.headers); + if (!userData) + return jsonRes(res, { + code: 401, + message: 'invalid token' + }); + else await next(userData); +}; diff --git a/frontend/desktop/src/services/backend/middleware/amount.ts b/frontend/desktop/src/services/backend/middleware/amount.ts new file mode 100644 index 00000000000..0edc2c0757d --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/amount.ts @@ -0,0 +1,25 @@ +import { jsonRes } from '../response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { globalPrisma } from '../db/init'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; + +export const accountBalanceGuard = + (userUid: string) => async (res: NextApiResponse, next?: () => void) => { + const account = await globalPrisma.account.findUnique({ + where: { + userUid + } + }); + if (!account) + return jsonRes(res, { + code: 404, + message: RESOURCE_STATUS.ACCOUNT_NOT_FOUND + }); + const balance = Number(account.balance || 0) - Number(account.deduction_balance || 0); + if (balance < 0) + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.INSUFFICENT_BALANCE + }); + await Promise.resolve(next?.()); + }; diff --git a/frontend/desktop/src/services/backend/middleware/checkResource.ts b/frontend/desktop/src/services/backend/middleware/checkResource.ts new file mode 100644 index 00000000000..1f63487e4f0 --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/checkResource.ts @@ -0,0 +1,170 @@ +import { NextApiResponse } from 'next'; +import { globalPrisma, prisma } from '../db/init'; +import { getUserKubeconfigNotPatch, K8sApiDefault } from '../kubernetes/admin'; +import { jsonRes } from '../response'; +import { generateAuthenticationToken } from '../auth'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +export const resourceGuard = + (userUid: string) => async (res: NextApiResponse, next?: () => void) => { + const userCr = await prisma.userCr.findUnique({ + where: { + userUid + }, + include: { + userWorkspace: { + include: { + workspace: true + } + } + } + }); + if (!userCr) + return jsonRes(res, { + message: RESOURCE_STATUS.USER_CR_NOT_FOUND, + code: 404 + }); + const userWorkspaces = userCr.userWorkspace; + const privateUserWorkspace = userWorkspaces.find((w) => w.isPrivate); + if (!privateUserWorkspace) + return jsonRes(res, { + message: RESOURCE_STATUS.PRIVATE_WORKSPACE_NOT_FOUND, + code: 404 + }); + const OwnerWorspaces = userWorkspaces.filter( + (w) => w.role === 'OWNER' && w.status === JoinStatus.IN_WORKSPACE + ); + if (OwnerWorspaces.length > 1) + return jsonRes(res, { + message: RESOURCE_STATUS.REMAIN_WORKSACE_OWNER, + code: 409 + }); + + // const cvmUrl = `https://cloudserver.${global.AppConfig.cloud.domain}` + const baseTemplateUrl = global.AppConfig.common.templateUrl; + const baseObjectStorageUrl = global.AppConfig.common.objectstorageUrl; + const baseApplaunchPadUrl = global.AppConfig.common.applaunchpadUrl; + const baseDbproviderUrl = global.AppConfig.common.dbproviderUrl; + + const kc = await getUserKubeconfigNotPatch(userCr.crName); + if (!kc) + return jsonRes(res, { + message: RESOURCE_STATUS.KUBECONFIG_NOT_FOUND, + code: 404 + }); + const authorization = encodeURI(kc); + const genReq = (url: string) => + fetch(url, { + headers: { + authorization + } + }); + const fetchFilter = async (resource: Response) => { + const result: { isOk: boolean; data: any } = { + isOk: false, + data: undefined + }; + if (!resource.ok) { + await jsonRes(res, { + code: 500, + message: RESOURCE_STATUS.GET_RESOURCE_ERROR + }); + } else { + result.isOk = true; + result.data = await resource.clone().json(); + } + return result; + }; + if (baseTemplateUrl) { + const templateUrl = baseTemplateUrl + '/api/instance/list'; + const result = await fetchFilter(await genReq(templateUrl)); + if (!result.isOk) return; + if (result.data?.data?.items?.length !== 0) + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.REMAIN_TEMPLATE + }); + } + if (baseObjectStorageUrl) { + const objectStorageUrl = baseObjectStorageUrl + '/api/bucket/list'; + const result = await fetchFilter(await genReq(objectStorageUrl)); + if (!result.isOk) return; + if (result.data?.data?.list.length !== 0) + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.REMAIN_OBJECT_STORAGE + }); + } + if (baseApplaunchPadUrl) { + const applaunchPadUrl = baseApplaunchPadUrl + '/api/getApps'; + const result = await fetchFilter(await genReq(applaunchPadUrl)); + if (!result.isOk) return; + if (result.data?.data?.length !== 0) + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.REMAIN_APP + }); + } + if (baseDbproviderUrl) { + const dbproviderUrl = baseDbproviderUrl + '/api/getDBList'; + const result = await fetchFilter(await genReq(dbproviderUrl)); + if (!result.isOk) return; + if (result.data?.data?.length !== 0) + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.REMAIN_DATABASE + }); + } + await Promise.resolve(next?.()); + }; + +export const otherRegionResourceGuard = + (userUid: string, userId: string) => async (res: NextApiResponse, next?: () => void) => { + const regionList = await globalPrisma.region.findMany(); + const regionTarget = regionList.filter( + (region) => region.uid !== global.AppConfig.cloud.regionUID + ); + const otherCheckResp = await Promise.all( + regionTarget.map((region) => + fetch( + process.env.NODE_ENV === 'development' + ? `http://127.0.0.1:3000/api/auth/delete/checkResource` + : `https://${region.domain}/api/auth/delete/checkResource`, + { + headers: { + authorization: encodeURI( + generateAuthenticationToken({ + userUid: userUid, + userId: userId + }) + ) + } + } + ) + ) + ); + if (otherCheckResp.some((resp) => !resp.ok)) + return jsonRes(res, { + code: 500, + message: RESOURCE_STATUS.GET_RESOURCE_ERROR + }); + const otherData = await Promise.all(otherCheckResp.map((resp) => resp.clone().json())); + for (let i = 0; i < otherData.length; i++) { + const resp = otherData[i]; + if (resp?.message === RESOURCE_STATUS.INTERNAL_SERVER_ERROR) { + return jsonRes(res, { + code: 500, + message: RESOURCE_STATUS.GET_RESOURCE_ERROR + }); + } + if ( + resp?.message !== RESOURCE_STATUS.RESULT_SUCCESS && + resp?.message !== RESOURCE_STATUS.USER_CR_NOT_FOUND + ) + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.REMAIN_OTHER_REGION_RESOURCE + }); + } + await Promise.resolve(next?.()); + }; diff --git a/frontend/desktop/src/services/backend/middleware/error.ts b/frontend/desktop/src/services/backend/middleware/error.ts new file mode 100644 index 00000000000..e16f318b9d6 --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/error.ts @@ -0,0 +1,17 @@ +import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '../response'; +import { RESPONSE_MESSAGE } from '@/types/response/utils'; + +export const ErrorHandler = + (handler: NextApiHandler, message: string = RESPONSE_MESSAGE.INTERNAL_SERVER_ERROR) => + async (req: NextApiRequest, res: NextApiResponse) => { + try { + await handler(req, res); + } catch (error) { + console.log(error); + jsonRes(res, { + message, + code: 500 + }); + } + }; diff --git a/frontend/desktop/src/services/backend/middleware/mergeUser.ts b/frontend/desktop/src/services/backend/middleware/mergeUser.ts new file mode 100644 index 00000000000..5dc96d5c0dc --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/mergeUser.ts @@ -0,0 +1,99 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/services/backend/response'; +import { ProviderType } from 'prisma/global/generated/client'; +import { globalPrisma } from '@/services/backend/db/init'; +import { checkCode } from '../db/mergeUserCode'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; + +export const filterMergeUser = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { providerType: ProviderType; providerId: string }) => Promise +) => { + const { code, providerType } = req.body as { + providerType?: ProviderType; + code?: string; + }; + if (!code) + return jsonRes(res, { + code: 400, + message: 'invalid code' + }); + if ( + providerType === undefined || + providerType === null || + !Object.values(ProviderType).includes(providerType) + ) + return jsonRes(res, { + code: 400, + message: 'invalid providerType' + }); + const result = await checkCode({ providerType, code }); + if (!result) + return jsonRes(res, { + code: 409, + message: 'invalid code' + }); + const providerId = result.providerId; + await next({ + providerType, + providerId + }); +}; + +export const mergeUserGuard = + (userUid: string, providerType: ProviderType, providerId: string) => + async (res: NextApiResponse, next: (data: { mergeUserUid: string }) => Promise) => { + const mergeUserOauthprovider = await globalPrisma.oauthProvider.findUnique({ + where: { + providerId_providerType: { + providerType, + providerId + } + } + }); + if (!mergeUserOauthprovider) + return jsonRes(res, { + code: 404, + message: USER_MERGE_STATUS.OAUTH_PROVIDER_NOT_FOUND + }); + + const mergeUserUid = mergeUserOauthprovider.userUid; + const mergeUser = await globalPrisma.user.findUnique({ + where: { + uid: mergeUserUid + }, + include: { + oauthProvider: true + } + }); + + if (!mergeUser) + return jsonRes(res, { + code: 404, + message: USER_MERGE_STATUS.USER_NOT_FOUND + }); + const user = await globalPrisma.user.findUnique({ + where: { + uid: userUid + }, + include: { + oauthProvider: true + } + }); + if (!user) + return jsonRes(res, { + code: 404, + message: USER_MERGE_STATUS.USER_NOT_FOUND + }); + const curTypeList = user.oauthProvider.map((oauthProvider) => oauthProvider.providerType); + const canMerge = mergeUser.oauthProvider.every((o) => !curTypeList.includes(o.providerType)); + if (!canMerge) + return jsonRes(res, { + code: 409, + message: USER_MERGE_STATUS.EXIST_SAME_OAUTH_PROVIDER + }); + await next({ + mergeUserUid + }); + }; diff --git a/frontend/desktop/src/services/backend/middleware/oauth.ts b/frontend/desktop/src/services/backend/middleware/oauth.ts new file mode 100644 index 00000000000..f375ec212c1 --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/oauth.ts @@ -0,0 +1,284 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '../response'; +import { isNumber } from 'lodash'; +import { TgithubToken, TgithubUser, TWechatToken, TWechatUser } from '@/types/user'; +import * as jwt from 'jsonwebtoken'; +import { userCanMerge } from '@/utils/tools'; +import { ProviderType } from 'prisma/global/generated/client'; +import { globalPrisma } from '../db/init'; +import { addOrUpdateCode } from '../db/mergeUserCode'; +import { v4 } from 'uuid'; +import { BIND_STATUS } from '@/types/response/bind'; +import { UNBIND_STATUS } from '@/types/response/unbind'; + +export const OauthCodeFilter = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { code: string; inviterId?: string }) => void +) => { + const { code } = req.body as { code?: string }; + if (!code) + return jsonRes(res, { + message: 'code is invalid', + code: 400 + }); + const { inviterId } = req.body as { inviterId?: string }; + await Promise.resolve( + next?.({ + code, + inviterId + }) + ); +}; + +export const OAuthEnvFilter = + (clientId?: string, clientSecret?: string) => + async (next?: (d: { clientID: string; clientSecret: string }) => void) => { + if (!clientId) throw Error('clientID NOT FOUND'); + if (!clientSecret) throw Error('clientSecret NOT FOUND'); + await Promise.resolve(next?.({ clientID: clientId, clientSecret })); + }; + +// the env will be changeds +export const wechatOAuthEnvFilter = () => + OAuthEnvFilter( + global.AppConfig?.desktop.auth.idp.wechat?.clientID, + global.AppConfig?.desktop.auth.idp.wechat?.clientSecret + ); +export const githubOAuthEnvFilter = () => + OAuthEnvFilter( + global.AppConfig?.desktop.auth.idp.github?.clientID, + global.AppConfig?.desktop.auth.idp.github?.clientSecret + ); +export const googleOAuthEnvFilter = () => { + return (next?: (d: { clientID: string; clientSecret: string; callbackURL: string }) => void) => + OAuthEnvFilter( + global.AppConfig?.desktop.auth.idp.google?.clientID, + global.AppConfig?.desktop.auth.idp.google?.clientSecret + )(async (originData) => { + const callbackURL = global.AppConfig?.desktop.auth.callbackURL; + if (!callbackURL) throw Error('callbackURL NOT FOUND'); + await Promise.resolve( + next?.({ + ...originData, + callbackURL + }) + ); + }); +}; + +export const googleOAuthGuard = + (clientId: string, clientSecret: string, code: string, callbackUrl: string) => + async ( + res: NextApiResponse, + next: (data: { id: string; name: string; avatar_url: string }) => void + ) => { + const url = `https://oauth2.googleapis.com/token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}&redirect_uri=${callbackUrl}&grant_type=authorization_code`; + const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json' } }); + if (!response.ok) + return jsonRes(res, { + code: 401, + message: 'Unauthorized' + }); + const __data = (await response.json()) as { + access_token: string; + scope: string; + token_type: string; + id_token: string; + }; + const userInfo = jwt.decode(__data.id_token) as { + iss: string; + azp: string; + aud: string; + sub: string; + at_hash: string; + name: string; + picture: string; + given_name: string; + family_name: string; + locale: string; + iat: number; + exp: number; + }; + const name = userInfo.name; + const id = userInfo.sub; + const avatar_url = userInfo.picture; + if (!id) throw Error('get userInfo error'); + // @ts-ignore + await Promise.resolve( + next?.({ + id, + name, + avatar_url + }) + ); + }; +export const githubOAuthGuard = + (clientId: string, clientSecret: string, code: string) => + async ( + res: NextApiResponse, + next: (data: { id: string; name: string; avatar_url: string }) => void + ) => { + const url = ` https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`; + const __data = (await ( + await fetch(url, { method: 'POST', headers: { Accept: 'application/json' } }) + ).json()) as TgithubToken; + const access_token = __data.access_token; + if (!access_token) { + return jsonRes(res, { + message: 'Failed to authenticate with GitHub', + code: 500, + data: 'access_token is null' + }); + } + const userUrl = `https://api.github.com/user`; + const response = await fetch(userUrl, { + headers: { + Authorization: `Bearer ${access_token}` + } + }); + if (!response.ok) + return jsonRes(res, { + code: 401, + message: 'Unauthorized' + }); + const result = (await response.json()) as TgithubUser; + const id = result.id; + if (!isNumber(id)) throw Error(); + // @ts-ignore + await Promise.resolve( + next?.({ + id: id + '', + name: result.login, + avatar_url: result.avatar_url + }) + ); + }; + +export const wechatOAuthGuard = + (clientId: string, clientSecret: string, code: string) => + async ( + res: NextApiResponse, + next: (data: { id: string; name: string; avatar_url: string }) => void + ) => { + const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${clientId}&secret=${clientSecret}&code=${code}&grant_type=authorization_code`; + const { access_token, openid } = (await ( + await fetch(url, { headers: { Accept: 'application/json' } }) + ).json()) as TWechatToken; + + if (!access_token || !openid) { + return jsonRes(res, { + message: 'Failed to authenticate with wechat', + code: 401, + data: 'access_token is null' + }); + } + const userUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`; + const response = await fetch(userUrl); + if (!response.ok) + return jsonRes(res, { + code: 401, + message: 'Unauthorized' + }); + const result = (await response.json()) as TWechatUser; + const id = result.unionid; + if (!id) throw Error('get wechat unionid error'); + const name = result.nickname; + const avatar_url = result.headimgurl; + return await Promise.resolve( + next?.({ + id, + name, + avatar_url + }) + ); + }; + +export const bindGuard = + (providerType: ProviderType) => + (providerId: string, userUid: string) => + async (res: NextApiResponse, next?: () => void) => { + const oauthProvider = await globalPrisma.oauthProvider.findUnique({ + where: { + providerId_providerType: { + providerType, + providerId + } + } + }); + // already exist user + if (oauthProvider) { + if (providerType === 'EMAIL') + return jsonRes(res, { + code: 409, + message: BIND_STATUS.MERGE_USER_PROVIDER_CONFLICT + }); + const mergeUserUid = oauthProvider.userUid; + const [mergeUserOauthProviders, curUserOauthProviders] = await globalPrisma.$transaction([ + globalPrisma.oauthProvider.findMany({ + where: { + userUid: mergeUserUid + } + }), + globalPrisma.oauthProvider.findMany({ + where: { + userUid + } + }) + ]); + const canMerge = userCanMerge(mergeUserOauthProviders, curUserOauthProviders); + if (!canMerge) { + return jsonRes(res, { + code: 409, + message: BIND_STATUS.MERGE_USER_PROVIDER_CONFLICT + }); + } else { + const code = v4(); + await addOrUpdateCode({ + providerId, + providerType, + code + }); + return jsonRes(res, { + code: 203, + message: BIND_STATUS.MERGE_USER_CONTINUE, + data: { + code + } + }); + } + } else { + await Promise.resolve(next?.()); + } + }; + +export const unbindGuard = + (providerType: ProviderType) => + (providerId: string, userUid: string) => + async (res: NextApiResponse, next?: () => void) => { + const oauthProvider = await globalPrisma.oauthProvider.findUnique({ + where: { + providerId_providerType: { + providerType, + providerId + }, + userUid + } + }); + if (!oauthProvider) + return jsonRes(res, { + code: 409, + message: UNBIND_STATUS.PROVIDER_NOT_FOUND + }); + await Promise.resolve(next?.()); + }; +export const unbindPhoneGuard = unbindGuard(ProviderType.PHONE); +export const bindPhoneGuard = bindGuard(ProviderType.PHONE); +export const bindEmailGuard = bindGuard(ProviderType.EMAIL); +export const unbindEmailGuard = unbindGuard(ProviderType.EMAIL); +export const unbindGithubGuard = unbindGuard(ProviderType.GITHUB); +export const bindGithubGuard = bindGuard(ProviderType.GITHUB); +export const unbindGoogleGuard = unbindGuard(ProviderType.GOOGLE); +export const bindGoogleGuard = bindGuard(ProviderType.GOOGLE); +export const unbindWechatGuard = unbindGuard(ProviderType.WECHAT); +export const bindWechatGuard = bindGuard(ProviderType.WECHAT); diff --git a/frontend/desktop/src/services/backend/middleware/sms.ts b/frontend/desktop/src/services/backend/middleware/sms.ts new file mode 100644 index 00000000000..8ee09ddf9d3 --- /dev/null +++ b/frontend/desktop/src/services/backend/middleware/sms.ts @@ -0,0 +1,194 @@ +import { jsonRes } from '../response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { + SmsType, + TVerification_Codes, + checkCode, + checkSendable, + deleteByUid, + getInfoByUid +} from '../db/verifyCode'; +import { isEmail } from '@/utils/crypto'; +import { EMAIL_STATUS } from '@/types/response/email'; + +export const filterPhoneParams = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { phoneNumbers: string }) => void +) => { + const { id: phoneNumbers } = req.body as { id?: string }; + if (!phoneNumbers) + return jsonRes(res, { + message: 'phoneNumbers is invalid', + code: 400 + }); + await Promise.resolve(next({ phoneNumbers })); +}; +export const filterEmailParams = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { email: string }) => void +) => { + const { id: email } = req.body as { id?: string }; + if (!email || !isEmail(email)) + return jsonRes(res, { + message: EMAIL_STATUS.INVALID_PARAMS, + code: 400 + }); + await Promise.resolve(next({ email })); +}; +export const filterPhoneVerifyParams = ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { phoneNumbers: string; code: string; inviterId?: string }) => void +) => + filterPhoneParams(req, res, async (data) => { + const { code, inviterId } = req.body as { + code?: string; + inviterId?: string; + }; + if (!code) + return jsonRes(res, { + message: 'code is invalid', + code: 400 + }); + + await Promise.resolve( + next({ + ...data, + code, + inviterId + }) + ); + }); +export const filterEmailVerifyParams = ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { email: string; code: string; inviterId?: string }) => void +) => + filterEmailParams(req, res, async (data) => { + const { code, inviterId } = req.body as { + code?: string; + inviterId?: string; + }; + if (!code) + return jsonRes(res, { + message: EMAIL_STATUS.INVALID_PARAMS, + code: 400 + }); + await Promise.resolve( + next({ + ...data, + code, + inviterId + }) + ); + }); + +export const filterCodeUid = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { uid: string }) => void +) => { + const { uid } = req.body as { uid?: string }; + if (!uid) + return jsonRes(res, { + message: 'uid is invalid', + code: 400 + }); + return await Promise.resolve( + next({ + uid + }) + ); +}; + +export const filterCf = async (req: NextApiRequest, res: NextApiResponse, next: () => void) => { + const { cfToken } = req.body as { cfToken?: string }; + const verifyEndpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; + const secret = process.env.CF_SECRET_KEY; + if (secret) { + if (!cfToken) + return jsonRes(res, { + message: 'cfToken is invalid', + code: 400 + }); + const verifyRes = await fetch(verifyEndpoint, { + method: 'POST', + body: `secret=${encodeURIComponent(secret)}&response=${encodeURIComponent(cfToken)}`, + headers: { + 'content-type': 'application/x-www-form-urlencoded' + } + }); + const data = await verifyRes.json(); + if (!data.success) + return jsonRes(res, { + message: 'cfToken is invalid', + code: 400 + }); + } + return await Promise.resolve(next()); +}; + +// once code +export const verifyCodeUidGuard = + (uid: string) => + async (res: NextApiResponse, next: (d: { smsInfo: TVerification_Codes }) => void) => { + const oldSmsInfo = await getInfoByUid({ uid }); + if (!oldSmsInfo) + return jsonRes(res, { + message: 'uid is expired', + code: 409 + }); + await Promise.resolve(next({ smsInfo: oldSmsInfo })); + // once code + await deleteByUid({ uid: oldSmsInfo.uid }); + }; + +export const verifySmsCodeGuard = + (smsType: SmsType) => + (id: string, code: string) => + async (res: NextApiResponse, next: (d: { smsInfo: TVerification_Codes }) => void) => { + const smsInfo = await checkCode({ id, smsType, code }); + if (!smsInfo) { + return jsonRes(res, { + message: 'SMS code is wrong', + code: 409 + }); + } + return await Promise.resolve(next({ smsInfo })); + }; + +export const verifyPhoneCodeGuard = verifySmsCodeGuard('phone'); +export const verifyEmailCodeGuard = verifySmsCodeGuard('email'); + +// need to get queryParam from after filter +export const sendSmsCodeGuard = + (smsType: SmsType) => (id: string) => async (res: NextApiResponse, next?: () => void) => { + if (!(await checkSendable({ smsType, id }))) { + return jsonRes(res, { + message: 'code already sent', + code: 409 + }); + } + await Promise.resolve(next?.()); + }; +export const sendNewSmsCodeGuard = + (smsType: SmsType) => + (codeUid: string, smsId: string) => + (res: NextApiResponse, next: (d: { smsInfo: TVerification_Codes }) => void) => + sendSmsCodeGuard(smsType)(smsId)(res, async () => { + const oldSmsInfo = await getInfoByUid({ uid: codeUid }); + if (!oldSmsInfo) + return jsonRes(res, { + message: 'uid is expired', + code: 409 + }); + await Promise.resolve(next({ smsInfo: oldSmsInfo })); + }); + +export const sendPhoneCodeGuard = sendSmsCodeGuard('phone'); +export const sendEmailCodeGuard = sendSmsCodeGuard('email'); + +export const sendNewPhoneCodeGuard = sendNewSmsCodeGuard('phone'); +export const sendNewEmailCodeGuard = sendNewSmsCodeGuard('email'); diff --git a/frontend/desktop/src/services/backend/sms.ts b/frontend/desktop/src/services/backend/sms.ts new file mode 100644 index 00000000000..e208168097c --- /dev/null +++ b/frontend/desktop/src/services/backend/sms.ts @@ -0,0 +1,150 @@ +//@ts-ignore +import { getAppConfig } from '@/pages/api/platform/getAppConfig'; +import { retrySerially } from '@/utils/tools'; +import Dysmsapi, * as dysmsapi from '@alicloud/dysmsapi20170525'; +//@ts-ignore +import * as OpenApi from '@alicloud/openapi-client'; +//@ts-ignore +import * as Util from '@alicloud/tea-util'; +import nodemailer from 'nodemailer'; +const getTransporter = () => { + if (!global.nodemailer) { + const emailConfig = global.AppConfig.desktop.auth.idp.sms?.email; + if (!emailConfig) throw Error('email transporter config error'); + const transporter = nodemailer.createTransport({ + pool: true, + host: emailConfig.host, + + port: emailConfig.port, + secure: true, // use TLS + auth: { + user: emailConfig.user, + pass: emailConfig.password + } + }); + global.nodemailer = transporter; + } + return global.nodemailer; +}; +export const smsReq = async (phoneNumbers: string) => { + const aliConfig = global.AppConfig.desktop.auth.idp.sms?.ali; + if (!aliConfig) throw Error('config error'); + const { signName, templateCode, accessKeyID: accessKeyId, accessKeySecret } = aliConfig; + // for dev + if (process.env.NODE_ENV === 'development' && !process.env.DEV_SMS_ENABLED) { + const code = '123456'; + return code; + } + const code = Math.floor(Math.random() * 900000 + 100000).toString(); + const sendSmsRequest = new dysmsapi.SendSmsRequest({ + phoneNumbers, + signName, + templateCode, + templateParam: `{"code":${code}}` + }); + const config = new OpenApi.Config({ + accessKeyId, + accessKeySecret + }); + + const client = new Dysmsapi(config); + const runtime = new Util.RuntimeOptions({}); + await retrySerially(async () => { + try { + const _result = await client.sendSmsWithOptions(sendSmsRequest, runtime); + + if (!_result) { + throw new Error('sms result is null'); + } + if (_result.statusCode !== 200) { + throw new Error(`sms result status code is ${_result.statusCode} + ${_result.body} + ${phoneNumbers}, + ${new Date()} + `); + } + if (_result.body.code !== 'OK') { + throw new Error(` + ${_result.body.message} + ${phoneNumbers}, + ${new Date()}`); + } + return _result; + } catch (error) { + return Promise.reject(error); + } + }, 3); + return code; +}; +export const emailSmsReq = async (email: string) => { + const emailConfig = global.AppConfig.desktop.auth.idp.sms?.email; + if (!emailConfig) throw Error('config error'); + + const code = Math.floor(Math.random() * 900000 + 100000).toString(); + const transporter = getTransporter(); + + await retrySerially( + () => + transporter.sendMail({ + from: emailConfig.user, + to: email, + subject: '【sealos】验证码', + html: ` + + + + + 【sealos】验证码 + + + +
+

尊敬的用户,您正在进行邮箱绑定操作。请输入以下验证码完成验证。

+

您的验证码是:

+

${code}

+
+ + ` + }), + 3 + ); + return code; +}; diff --git a/frontend/desktop/src/services/backend/svc/access.ts b/frontend/desktop/src/services/backend/svc/access.ts new file mode 100644 index 00000000000..fb9c9cdc92a --- /dev/null +++ b/frontend/desktop/src/services/backend/svc/access.ts @@ -0,0 +1,57 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '../response'; +import { ProviderType } from 'prisma/global/generated/client'; +import { getGlobalToken } from '../globalAuth'; + +export const getGlobalTokenSvc = + ( + avatar_url: string, + providerId: string, + name: string, + providerType: ProviderType, + password?: string, + inviterId?: string + ) => + async (res: NextApiResponse, next?: () => void) => { + const data = await getGlobalToken({ + provider: providerType, + providerId, + avatar_url, + name, + inviterId, + password + }); + if (!data) + return jsonRes(res, { + code: 401, + message: 'Unauthorized' + }); + return jsonRes(res, { + data, + code: 200, + message: 'Successfully' + }); + }; + +export const getGlobalTokenByGithubSvc = ( + avatar_url: string, + providerId: string, + name: string, + inviterId?: string +) => getGlobalTokenSvc(avatar_url, providerId, name, ProviderType.GITHUB, undefined, inviterId); +export const getGlobalTokenByWechatSvc = ( + avatar_url: string, + providerId: string, + name: string, + inviterId?: string +) => getGlobalTokenSvc(avatar_url, providerId, name, ProviderType.WECHAT, undefined, inviterId); +export const getGlobalTokenByPhoneSvc = (phone: string, inviterId?: string) => + getGlobalTokenSvc('', phone, phone, ProviderType.PHONE, undefined, inviterId); +export const getGlobalTokenByPasswordSvc = (name: string, password: string, inviterId?: string) => + getGlobalTokenSvc('', name, name, ProviderType.PASSWORD, password, inviterId); +export const getGlobalTokenByGoogleSvc = ( + avatar_url: string, + providerId: string, + name: string, + inviterId?: string +) => getGlobalTokenSvc(avatar_url, providerId, name, ProviderType.GOOGLE, undefined, inviterId); diff --git a/frontend/desktop/src/services/backend/svc/bindProvider.ts b/frontend/desktop/src/services/backend/svc/bindProvider.ts new file mode 100644 index 00000000000..409b810b495 --- /dev/null +++ b/frontend/desktop/src/services/backend/svc/bindProvider.ts @@ -0,0 +1,243 @@ +import { globalPrisma } from '../db/init'; +import { NextApiResponse } from 'next'; +import { ProviderType } from 'prisma/global/generated/client'; +import { jsonRes } from '../response'; +import { findUser } from '../globalAuth'; +import { BIND_STATUS } from '@/types/response/bind'; +import { CHANGE_BIND_STATUS } from '@/types/response/changeBind'; +import { UNBIND_STATUS } from '@/types/response/unbind'; +import { PROVIDER_STATUS } from '@/types/response/utils'; +async function addOauthProvider({ + providerType, + providerId, + userUid +}: { + providerType: ProviderType; + providerId: string; + userUid: string; +}) { + try { + const oauthProvider = await globalPrisma.oauthProvider.create({ + data: { + providerId, + providerType, + user: { + connect: { + uid: userUid + } + } + } + }); + if (!oauthProvider) return null; + return { + oauthProvider + }; + } catch (e) { + return null; + } +} + +async function removeOauthProvider({ + provider, + id, + userUid +}: { + provider: ProviderType; + id: string; + userUid: string; +}) { + try { + const oauthProvider = await globalPrisma.oauthProvider.delete({ + where: { + providerId_providerType: { + providerId: id, + providerType: provider + }, + userUid + } + }); + if (!oauthProvider) return null; + return { + oauthProvider + }; + } catch (e) { + return null; + } +} + +async function updateOauthProvider({ + provider, + newId, + oldId, + userUid +}: { + provider: ProviderType; + oldId: string; + newId: string; + userUid: string; +}) { + try { + const oauthProvider = await globalPrisma.oauthProvider.update({ + where: { + providerId_providerType: { + providerId: oldId, + providerType: provider + }, + userUid + }, + data: { + providerId: newId + } + }); + if (!oauthProvider) return null; + return { + oauthProvider + }; + } catch (e) { + return null; + } +} + +export const changeBindProviderSvc = + (oldProviderId: string, newProviderId: string, providerType: ProviderType, userUid: string) => + async (res: NextApiResponse, next?: () => void) => { + if (providerType === ProviderType.PASSWORD) + return jsonRes(res, { + code: 409, + message: CHANGE_BIND_STATUS.NOT_SUPPORT + }); + + const user = await globalPrisma.user.findUnique({ where: { uid: userUid } }); + if (!user) + return jsonRes(res, { + code: 409, + message: CHANGE_BIND_STATUS.USER_NOT_FOUND + }); + if (newProviderId === oldProviderId) + return jsonRes(res, { + code: 200, + message: CHANGE_BIND_STATUS.RESULT_SUCCESS + }); + const result = await updateOauthProvider({ + provider: providerType, + oldId: oldProviderId, + newId: newProviderId, + userUid + }); + if (!result) throw Error(CHANGE_BIND_STATUS.DB_ERROR); + else { + jsonRes(res, { + code: 200, + message: CHANGE_BIND_STATUS.RESULT_SUCCESS + }); + await Promise.resolve(next?.()); + } + }; + +const bindProviderSvc = + (providerId: string, providerType: ProviderType, userUid: string) => + async (res: NextApiResponse, next?: () => void) => { + if (providerType === ProviderType.PASSWORD) + return jsonRes(res, { + code: 409, + message: BIND_STATUS.NOT_SUPPORT + }); + const user = await globalPrisma.user.findUnique({ where: { uid: userUid } }); + if (!user) + return jsonRes(res, { + code: 409, + message: BIND_STATUS.USER_NOT_FOUND + }); + const result = await addOauthProvider({ + providerType, + providerId, + userUid + }); + if (!result) throw Error(BIND_STATUS.DB_ERROR); + + return jsonRes(res, { + code: 200, + message: BIND_STATUS.RESULT_SUCCESS + }); + }; +const unbindProviderSvc = + (providerId: string, providerType: ProviderType, userUid: string) => + async (res: NextApiResponse, next?: () => void) => { + if (providerType === ProviderType.PASSWORD) + return jsonRes(res, { + code: 409, + message: UNBIND_STATUS.NOT_SUPPORT + }); + const user = await findUser({ userUid }); + if (!user) + return jsonRes(res, { + code: 409, + message: UNBIND_STATUS.USER_NOT_FOUND + }); + if ( + !user.oauthProvider.find( + (o) => o.providerType === providerType && o.providerId === providerId + ) + ) + return jsonRes(res, { + code: 409, + message: PROVIDER_STATUS.PROVIDER_NOT_FOUND + }); + if (providerType !== ProviderType.EMAIL) { + // the number of provider must more than 1 + let minCount = 1; + if (user.oauthProvider.find((o) => o.providerType === ProviderType.EMAIL)) { + minCount++; + } + if (user.oauthProvider.length <= minCount) + return jsonRes(res, { + code: 409, + message: UNBIND_STATUS.RESOURCE_CONFLICT + }); + } + + const result = await removeOauthProvider({ + provider: providerType, + id: providerId, + userUid + }); + if (!result) throw new Error(UNBIND_STATUS.DB_ERROR); + return jsonRes(res, { + code: 200, + message: UNBIND_STATUS.RESULT_SUCCESS + }); + }; + +export const bindPhoneSvc = (phoneNumbers: string, userUid: string) => + bindProviderSvc(phoneNumbers, ProviderType.PHONE, userUid); +export const bindGithubSvc = (id: string, userUid: string) => + bindProviderSvc(id, ProviderType.GITHUB, userUid); +export const bindWechatSvc = (id: string, userUid: string) => + bindProviderSvc(id, ProviderType.WECHAT, userUid); +export const bindGoogleSvc = (id: string, userUid: string) => + bindProviderSvc(id, ProviderType.GOOGLE, userUid); +export const bindEmailSvc = (email: string, userUid: string) => + bindProviderSvc(email, ProviderType.EMAIL, userUid); + +export const unbindPhoneSvc = (phoneNumbers: string, userUid: string) => + unbindProviderSvc(phoneNumbers, ProviderType.PHONE, userUid); +export const unbindGithubSvc = (id: string, userUid: string) => + unbindProviderSvc(id, ProviderType.GITHUB, userUid); +export const unbindWechatSvc = (id: string, userUid: string) => + unbindProviderSvc(id, ProviderType.WECHAT, userUid); +export const unbindGoogleSvc = (id: string, userUid: string) => + unbindProviderSvc(id, ProviderType.GOOGLE, userUid); +export const unbindEmailSvc = (email: string, userUid: string) => + unbindProviderSvc(email, ProviderType.EMAIL, userUid); + +export const changePhoneBindingSvc = ( + oldProviderId: string, + newProviderId: string, + userUid: string +) => changeBindProviderSvc(oldProviderId, newProviderId, ProviderType.PHONE, userUid); + +export const changeEmailBindingSvc = ( + oldProviderId: string, + newProviderId: string, + userUid: string +) => changeBindProviderSvc(oldProviderId, newProviderId, ProviderType.EMAIL, userUid); diff --git a/frontend/desktop/src/services/backend/svc/deleteUser.ts b/frontend/desktop/src/services/backend/svc/deleteUser.ts new file mode 100644 index 00000000000..22f18379a75 --- /dev/null +++ b/frontend/desktop/src/services/backend/svc/deleteUser.ts @@ -0,0 +1,92 @@ +import { NextApiResponse } from 'next'; +import { jsonRes } from '../response'; +import { globalPrisma, prisma } from '../db/init'; +import { v4 } from 'uuid'; +import { TransactionType, TransactionStatus, AuditAction } from 'prisma/global/generated/client'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { DeleteUserEvent } from '@/types/db/event'; + +export const deleteUserSvc = (userUid: string) => async (res: NextApiResponse) => { + const user = await globalPrisma.user.findUnique({ + where: { + uid: userUid + }, + include: { + oauthProvider: true + } + }); + if (!user) + return jsonRes(res, { + message: RESOURCE_STATUS.USER_NOT_FOUND, + code: 404 + }); + const oauthProviderList = user.oauthProvider; + if (oauthProviderList.length === 0) + return jsonRes(res, { + message: RESOURCE_STATUS.OAUTHPROVIDER_NOT_FOUND, + code: 404 + }); + const txUid = v4(); + const infoUid = v4(); + const regionResults = await globalPrisma.region.findMany(); + if (!regionResults) throw Error('region list is null'); + const regionList = regionResults.map((r) => r.uid); + // add task ( catch by outer ) + await globalPrisma.$transaction(async (tx) => { + for await (const oauthProvider of oauthProviderList) { + await tx.oauthProvider.delete({ + where: { + uid: oauthProvider.uid + } + }); + const eventName = DeleteUserEvent['_DELETE_OAUTH_PROVIDER']; + const _data = { + userUid, + providerType: oauthProvider.providerType, + providerId: oauthProvider.providerId, + message: `${oauthProvider.providerType}:${oauthProvider.providerId}, delete` + }; + await tx.eventLog.create({ + data: { + eventName, + mainId: userUid, + data: JSON.stringify(_data) + } + }); + } + await tx.eventLog.create({ + data: { + eventName: DeleteUserEvent['_PUB_TRANSACTION'], + mainId: userUid, + data: JSON.stringify({ + message: `${userUid} publish delete user transaction` + }) + } + }); + await tx.precommitTransaction.create({ + data: { + uid: txUid, + status: TransactionStatus.READY, + infoUid, + transactionType: TransactionType.DELETE_USER + } + }); + await tx.deleteUserTransactionInfo.create({ + data: { + uid: infoUid, + userUid + } + }); + await tx.transactionDetail.createMany({ + data: regionList.map((regionUid) => ({ + status: TransactionStatus.READY, + transactionUid: txUid, + regionUid + })) + }); + }); + return jsonRes(res, { + message: RESOURCE_STATUS.RESULT_SUCCESS, + code: 200 + }); +}; diff --git a/frontend/desktop/src/services/backend/svc/mergeUser.ts b/frontend/desktop/src/services/backend/svc/mergeUser.ts new file mode 100644 index 00000000000..2fc90ca4975 --- /dev/null +++ b/frontend/desktop/src/services/backend/svc/mergeUser.ts @@ -0,0 +1,105 @@ +import { NextApiResponse } from 'next'; +import { jsonRes } from '../response'; +import { globalPrisma } from '../db/init'; +import { v4 } from 'uuid'; +import { TransactionType, TransactionStatus, AuditAction } from 'prisma/global/generated/client'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; +import { MergeUserEvent } from '@/types/db/event'; + +export const mergeUserSvc = + (userUid: string, mergeUserUid: string) => async (res: NextApiResponse) => { + const user = await globalPrisma.user.findUnique({ + where: { + uid: userUid + }, + include: { + oauthProvider: true + } + }); + if (!user) + return jsonRes(res, { + message: USER_MERGE_STATUS.USER_NOT_FOUND, + code: 404 + }); + const txUid = v4(); + const infoUid = v4(); + const regionResults = await globalPrisma.region.findMany(); + if (!regionResults) throw Error('region list is null'); + const regionList = regionResults.map((r) => r.uid); + const oauthProviderList = await globalPrisma.oauthProvider.findMany({ + where: { + userUid: mergeUserUid + } + }); + // add task ( catch by outer ) + await globalPrisma.$transaction(async (tx) => { + // optimistic + for await (const oauthProvider of oauthProviderList) { + await tx.oauthProvider.findUniqueOrThrow({ + where: { + uid: oauthProvider.uid, + userUid: mergeUserUid + } + }); + await tx.oauthProvider.update({ + where: { + uid: oauthProvider.uid, + userUid: mergeUserUid + }, + data: { + userUid + } + }); + const eventName = MergeUserEvent['_MERGE_OAUTH_PROVIDER']; + const _data = { + mergeUserUid, + userUid, + providerType: oauthProvider.providerType, + providerId: oauthProvider.providerId, + message: `${oauthProvider.providerType}: ${oauthProvider.providerId}, update` + }; + await tx.eventLog.create({ + data: { + eventName, + mainId: userUid, + data: JSON.stringify(_data) + } + }); + } + await tx.precommitTransaction.create({ + data: { + uid: txUid, + status: TransactionStatus.READY, + infoUid, + transactionType: TransactionType.MERGE_USER + } + }); + await tx.eventLog.create({ + data: { + eventName: MergeUserEvent['_PUB_TRANSACTION'], + mainId: userUid, + data: JSON.stringify({ + message: `${userUid} publish merge user transaction` + }) + } + }); + await tx.mergeUserTransactionInfo.create({ + data: { + uid: infoUid, + mergeUserUid, + userUid + } + }); + await tx.transactionDetail.createMany({ + data: regionList.map((regionUid) => ({ + status: TransactionStatus.READY, + transactionUid: txUid, + regionUid + })) + }); + }); + return jsonRes(res, { + message: USER_MERGE_STATUS.RESULT_SUCCESS, + code: 200 + }); + }; diff --git a/frontend/desktop/src/services/backend/svc/sms.ts b/frontend/desktop/src/services/backend/svc/sms.ts new file mode 100644 index 00000000000..4f9dd65c75e --- /dev/null +++ b/frontend/desktop/src/services/backend/svc/sms.ts @@ -0,0 +1,23 @@ +import { NextApiResponse } from 'next'; +import { jsonRes } from '../response'; +import { addOrUpdateCode, SmsType } from '../db/verifyCode'; +import { emailSmsReq, smsReq } from '../sms'; + +export const sendSmsCodeResp = + (smsType: SmsType, id: string, code: string) => + async (res: NextApiResponse, next?: () => void) => { + await addOrUpdateCode({ id, smsType, code }); + return jsonRes(res, { + message: 'successfully', + code: 200 + }); + }; +export const sendPhoneCodeSvc = (phone: string) => async (res: NextApiResponse) => { + console.log('svc!'); + const code = await smsReq(phone); + return sendSmsCodeResp('phone', phone, code)(res); +}; +export const sendEmailCodeSvc = (email: string) => async (res: NextApiResponse) => { + const code = await emailSmsReq(email); + return sendSmsCodeResp('email', email, code)(res); +}; diff --git a/frontend/desktop/src/services/backend/team.ts b/frontend/desktop/src/services/backend/team.ts index 1da03e0d9a5..a1717e86de3 100644 --- a/frontend/desktop/src/services/backend/team.ts +++ b/frontend/desktop/src/services/backend/team.ts @@ -1,22 +1,32 @@ import { - RoleAction, + deleteRequestCrd, generateRequestCrd, ROLE_LIST, - UserRole, - watchEventType, - deleteRequestCrd + RoleAction, + UserRole } from '@/types/team'; -import { KubeConfig } from '@kubernetes/client-node'; +import { KubeConfig, V1Status } from '@kubernetes/client-node'; import { K8sApiDefault } from './kubernetes/admin'; -import { ApplyYaml } from './kubernetes/user'; +import { ApplyYaml, GetCRD } from './kubernetes/user'; import { prisma } from '@/services/backend/db/init'; import { JoinStatus, Role } from 'prisma/region/generated/client'; -import { UserRoleToRole } from '@/utils/tools'; +import { roleToUserRole, UserRoleToRole, vaildManage } from '@/utils/tools'; +import { createHash } from 'node:crypto'; const _applyRoleRequest = - (kc: KubeConfig, nsid: string) => - (action: 'Grant' | 'Deprive' | 'Update', types: watchEventType[]) => - (k8s_username: string, role: UserRole) => { + (kc: KubeConfig, nsid: string, idempotent: boolean = false) => + (action: 'Grant' | 'Deprive' | 'Update') => + async (k8s_username: string, role: UserRole) => { + const hash = createHash('sha256'); + const props = { + user: k8s_username, + namespace: nsid, + action, + role: ROLE_LIST[role] + }; + let name = ''; + hash.update(JSON.stringify(props) + new Date().getTime()); + name = hash.digest('hex'); const createCR = () => ApplyYaml( kc, @@ -24,7 +34,8 @@ const _applyRoleRequest = user: k8s_username, namespace: nsid, action, - role: ROLE_LIST[role] + role: ROLE_LIST[role], + name }) ); return new Promise((resolve, reject) => { @@ -43,6 +54,7 @@ const _applyRoleRequest = wrap(); }); }; + export const applyDeleteRequest = (user: string) => { const kc = new KubeConfig(); kc.loadFromDefault(); @@ -70,7 +82,7 @@ export const applyDeleteRequest = (user: string) => { }); }; -type ModifyTeamBaseParam = { +type ModifyWorkspaceBaseParam = { k8s_username: string; // 目标 // ns_uid: string; // 目标 workspaceId: string; @@ -78,61 +90,100 @@ type ModifyTeamBaseParam = { action: RoleAction; }; // 只修改,不包含邀请的业务逻辑,应该是确保数据库已经有记录,只修改记录,并且修改k8s的rolebinding -export const modifyTeamRole = async ({ +export const modifyWorkspaceRole = async ({ k8s_username, // ns_uid, workspaceId, ...props -}: - | (ModifyTeamBaseParam & { - action: 'Grant' | 'Create'; - }) - | (ModifyTeamBaseParam & { - action: 'Deprive' | 'Modify'; - pre_role: UserRole; - }) - | (ModifyTeamBaseParam & { - action: 'Change'; - pre_k8s_username: string; - })) => { +}: ModifyWorkspaceBaseParam & + ( + | { + action: 'Grant' | 'Create'; + } + | { + action: 'Deprive' | 'Modify'; + pre_role: UserRole; + } + | { + action: 'Change'; + pre_k8s_username: string; + } + | { + action: 'Merge'; + pre_role: UserRole; + } + )) => { const kc = K8sApiDefault(); const applyRoleRequest = _applyRoleRequest(kc, workspaceId); - const grantApply = applyRoleRequest('Grant', [watchEventType.ADDED]); - const depriveApply = applyRoleRequest('Deprive', [watchEventType.DELETED]); - const updateApply = applyRoleRequest('Update', []); - // let result: TeamRolebinding | null = null; + const grantApply = applyRoleRequest('Grant'); + const depriveApply = applyRoleRequest('Deprive'); + const updateApply = applyRoleRequest('Update'); if (props.action === 'Grant') { - // result = await grantApply(k8s_username, props.role); - // console.log(result) } else if (props.action === 'Deprive') { - // 保证存在 if (props.pre_role !== props.role) return null; - // result = await depriveApply(k8s_username, props.role); } else if (props.action === 'Change') { - // 移交自己的权限 + // abdicate role if (props.role === UserRole.Owner && props.pre_k8s_username) { - // 先把自己的权限去掉 - // result = + // remove owner await updateApply(props.pre_k8s_username, UserRole.Developer); - // 再补充新的权限上来 - // result = + // add owner await updateApply(k8s_username, UserRole.Owner); } } else if (props.action === 'Create') { - // 创建新的团队 + // create new workspace if (props.role === UserRole.Owner) { await grantApply(k8s_username, UserRole.Owner); } } else if (props.action === 'Modify') { - // 修改他人权限 - // 相同权限,不管 + // modify other role + // same role if (props.pre_role === props.role) return null; - updateApply(k8s_username, props.role); - } // if (!result) return null; - // return result; + await updateApply(k8s_username, props.role); + } +}; +// 4 conditions +/** + * | same role | user role >= user merge role | merge user role > user role | target user out of workspace | + * | deprive merge user | deprive merge user | deprive merge user & update to merge user role | deprive merge user & grant merge user role | + * + * */ +export const mergeUserWorkspaceRole = async ({ + workspaceId, + mergeUserCrName, + userCrName, + userRole, + mergeUserRole +}: { + workspaceId: string; + mergeUserCrName: string; + userCrName: string; + userRole?: Role; + mergeUserRole: Role; +}) => { + const kc = K8sApiDefault(); + const applyRoleRequest = _applyRoleRequest(kc, workspaceId, true); + const grantApply = applyRoleRequest('Grant'); + const depriveApply = applyRoleRequest('Deprive'); + const updateApply = applyRoleRequest('Update'); + // handle pre user + await depriveApply(mergeUserCrName, roleToUserRole(mergeUserRole)); + // handle new user + const targetUserExist = !(userRole === undefined || userRole === null); + if (targetUserExist) { + const targetUserRoleisHigher = vaildManage(roleToUserRole(userRole))( + roleToUserRole(mergeUserRole), + true + ); + if (!targetUserRoleisHigher) { + await updateApply(userCrName, roleToUserRole(mergeUserRole)); + } + } else { + await grantApply(userCrName, roleToUserRole(mergeUserRole)); + } + // handle new user }; // ================================== // 以下是邀请的业务逻辑,只负责改数据库 @@ -230,3 +281,82 @@ export const acceptInvite = async ({ } }); }; + +export const mergeUserModifyBinding = async ({ + mergeUserCrUid, + workspaceUid, + userCrUid, + userRole, + mergeUserRole +}: { + mergeUserCrUid: string; + workspaceUid: string; + userCrUid: string; + userRole?: Role; + mergeUserRole: Role; +}) => { + let role; + if (undefined === userRole || userRole === null) { + const role = mergeUserRole; + // user is not in workspace + await prisma.$transaction([ + prisma.userWorkspace.create({ + data: { + role, + userCrUid: userCrUid, + workspaceUid, + status: JoinStatus.IN_WORKSPACE, + isPrivate: false + } + }), + prisma.userWorkspace.delete({ + where: { + workspaceUid_userCrUid: { + userCrUid: mergeUserCrUid, + workspaceUid + } + } + }) + ]); + } else { + const userRoleisHigher = vaildManage(roleToUserRole(userRole))( + roleToUserRole(mergeUserRole), + true + ); + if (userRoleisHigher) { + role = userRole; + } else { + role = mergeUserRole; + } + await prisma.$transaction([ + prisma.userWorkspace.findUniqueOrThrow({ + where: { + workspaceUid_userCrUid: { + userCrUid: userCrUid, + workspaceUid + }, + role: userRole + } + }), + prisma.userWorkspace.update({ + where: { + workspaceUid_userCrUid: { + userCrUid: userCrUid, + workspaceUid + } + }, + data: { + role + } + }), + prisma.userWorkspace.delete({ + where: { + workspaceUid_userCrUid: { + userCrUid: mergeUserCrUid, + workspaceUid + } + } + }) + ]); + } +}; diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index ee720c14258..754605dbe39 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -1,15 +1,16 @@ // for service -import process from 'process'; - export const enablePassword = () => global.AppConfig.desktop.auth.idp.password?.enabled || false; export const enableGithub = () => global.AppConfig.desktop.auth.idp.github?.enabled || false; export const enableSms = () => global.AppConfig.desktop.auth.idp.sms?.ali?.enabled || false; +export const enableEmailSms = () => global.AppConfig.desktop.auth.idp.sms?.email?.enabled || false; export const enableWechat = () => global.AppConfig.desktop.auth.idp.wechat?.enabled || false; export const enableGoogle = () => global.AppConfig.desktop.auth.idp.google?.enabled || false; export const enableSignUp = () => global.AppConfig.desktop.auth.signUpEnabled || false; export const enableApi = () => global.AppConfig.common.apiEnabled || false; export const enableOAuth2 = () => global.AppConfig.desktop.auth.idp.oauth2?.enabled || false; - +export const getBillingUrl = () => global.AppConfig.desktop.auth.billingUrl || ''; +export const getWorkorderUrl = () => global.AppConfig.desktop.auth.workorderUrl || ''; +export const getCvmUrl = () => global.AppConfig.desktop.auth.cloudVitrualMachineUrl || ''; export const getTeamLimit = () => global.AppConfig.desktop.teamManagement?.maxTeamCount || 50; export const getTeamInviteLimit = () => global.AppConfig.desktop.teamManagement?.maxTeamMemberCount || 50; diff --git a/frontend/desktop/src/stores/callback.ts b/frontend/desktop/src/stores/callback.ts index 6e1451f2db7..2decf0363fc 100644 --- a/frontend/desktop/src/stores/callback.ts +++ b/frontend/desktop/src/stores/callback.ts @@ -1,9 +1,22 @@ +import { ProviderType } from 'prisma/global/generated/client'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; - +export enum MergeUserStatus { + IDLE, + CONFLICT, + CANMERGE +} +type MergeUserData = { + code: string; + providerType: ProviderType; +}; type CallbackState = { workspaceInviteCode?: string; + mergeUserData?: MergeUserData; + mergeUserStatus: MergeUserStatus; + setMergeUserData: (d?: MergeUserData) => void; + setMergeUserStatus: (s: MergeUserStatus) => void; setWorkspaceInviteCode: (id?: string) => void; }; @@ -11,6 +24,18 @@ const useCallbackStore = create()( persist( immer((set, get) => ({ workspaceInviteCode: undefined, + mergeUserData: undefined, + mergeUserStatus: MergeUserStatus.IDLE, + setMergeUserData(mergeUserData) { + set({ + mergeUserData + }); + }, + setMergeUserStatus(mergeUserStatus) { + set({ + mergeUserStatus + }); + }, setWorkspaceInviteCode: (id?: string) => { set((state) => ({ workspaceInviteCode: id })); } diff --git a/frontend/desktop/src/stores/session.ts b/frontend/desktop/src/stores/session.ts index 4d1d40a41d7..72eeb15355e 100644 --- a/frontend/desktop/src/stores/session.ts +++ b/frontend/desktop/src/stores/session.ts @@ -2,8 +2,12 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { Session, sessionKey } from '@/types'; -import { OauthProvider, Provider } from '@/types/user'; - +import { OauthProvider } from '@/types/user'; +type StatePayload = { + rad: string; + action: OauthAction; +}; +export type OauthAction = 'LOGIN' | 'BIND' | 'UNBIND' | 'PROXY'; type SessionState = { session?: Session; token: string; @@ -14,13 +18,16 @@ type SessionState = { delSession: () => void; isUserLogin: () => boolean; /* - when proxy oauth2.0 ,the domainState need to be used - */ - generateState: (domainState?: string) => string; - compareState: (state: string) => boolean; + when proxy oauth2.0 ,the domainState need to be used + */ + generateState: (action?: OauthAction, domainState?: string) => string; + compareState: (state: string) => { + isSuccess: boolean; + action: string; + statePayload: string[]; + }; setProvider: (provider?: OauthProvider) => void; setToken: (token: string) => void; - setState: (state: string) => void; lastWorkSpaceId: string; setWorkSpaceId: (id: string) => void; }; @@ -45,21 +52,29 @@ const useSessionStore = create()( set({ session: undefined }); }, isUserLogin: () => !!get().session?.user, - generateState: (domainState) => { - let state = new Date().getTime().toString(); - console.log(domainState); - if (!!domainState) state = domainState; + // [LOGIN/UNBIND/BIND]_STATE + // PROXY_DOMAINSTATE, DOMAINSTATE = URL_[LOGIN/UNBIND/BIND]_STATE + generateState: (action = 'LOGIN', domainState) => { + let state = action as string; + if (domainState && action === 'PROXY') { + state = state + '_' + domainState; + } else { + state = state + '_' + new Date().getTime().toString(); + } set({ oauth_state: state }); return state; }, - setState: (state) => { - set({ oauth_state: state }); - }, compareState: (state: string) => { - let result = state === get().oauth_state; - console.log(result, get().oauth_state, state, 'compareState'); + // fix wechat + let isSuccess = decodeURIComponent(state) === decodeURIComponent(get().oauth_state); + console.log(state, get().oauth_state); + const [action, ...statePayload] = state.split('_'); set({ oauth_state: undefined }); - return result; + return { + isSuccess, + action, + statePayload + }; }, setProvider: (provider?: OauthProvider) => { set({ provider }); diff --git a/frontend/desktop/src/types/api.ts b/frontend/desktop/src/types/api.ts index f53ba5bf39e..9c86ffa2826 100644 --- a/frontend/desktop/src/types/api.ts +++ b/frontend/desktop/src/types/api.ts @@ -1,5 +1,5 @@ -export type ApiResp = { +export type ApiResp = { code?: number; - message?: string; + message?: U; data?: T; }; diff --git a/frontend/desktop/src/types/crd.ts b/frontend/desktop/src/types/crd.ts index 0b81a3308ae..680172f158d 100644 --- a/frontend/desktop/src/types/crd.ts +++ b/frontend/desktop/src/types/crd.ts @@ -1,4 +1,3 @@ -import { KubeConfig } from '@kubernetes/client-node'; import { APPTYPE, displayType } from './app'; import { LicenseFrontendKey } from '@/constants/account'; diff --git a/frontend/desktop/src/types/db/event.ts b/frontend/desktop/src/types/db/event.ts new file mode 100644 index 00000000000..d2a39fa002c --- /dev/null +++ b/frontend/desktop/src/types/db/event.ts @@ -0,0 +1,13 @@ +export enum MergeUserEvent { + '_MERGE_OAUTH_PROVIDER' = '_MERGE_OAUTH_PROVIDER', + '_PUB_TRANSACTION' = '_PUB_TRANSACTION', + '_MERGE_WORKSPACE' = '_MERGE_WORKSPACE', + '_COMMIT' = '_COMMIT' +} +// export type EventName = 'DELETE_USER'; +export enum DeleteUserEvent { + '_DELETE_OAUTH_PROVIDER' = '_DELETE_OAUTH_PROVIDER', + '_PUB_TRANSACTION' = '_PUB_TRANSACTION', + '_DELETE_USERCR' = '_DELETE_USERCR', + '_COMMIT' = '_COMMIT' +} diff --git a/frontend/desktop/src/types/index.ts b/frontend/desktop/src/types/index.ts index dc461c73158..deff17a71ee 100644 --- a/frontend/desktop/src/types/index.ts +++ b/frontend/desktop/src/types/index.ts @@ -1,5 +1,8 @@ import { type MongoClient } from 'mongodb'; import { type AppConfigType } from './system'; +import { Cron } from 'croner'; +import { Transporter } from 'nodemailer'; +import SMTPPool from 'nodemailer/lib/smtp-pool'; export * from './api'; export * from './session'; export * from './app'; @@ -7,13 +10,17 @@ export * from './crd'; export * from './payment'; export * from './system'; export * from './login'; -export * from './valuation'; +export * from './tools'; export * from './license'; export * from './region'; declare global { var mongodb: MongoClient | null; var AppConfig: AppConfigType; + var commitCroner: Cron | undefined; + var finishCroner: Cron | undefined; + var runCroner: Cron | undefined; var WechatAccessToken: string | undefined; var WechatExpiresIn: number | undefined; + var nodemailer: Transporter | undefined; } diff --git a/frontend/desktop/src/types/oauth.ts b/frontend/desktop/src/types/oauth.ts deleted file mode 100644 index 4f00987b145..00000000000 --- a/frontend/desktop/src/types/oauth.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type OAuthToken = { - accessToken: string; - accessTokenExpiresAt?: Date; - clientId: string; - refreshToken?: string; - refreshTokenExpiresAt?: Date; - userId: string; - scope?: string[]; - grants: Grant[]; -}; - -export enum Grant { - authorization_code = 'authorization_code', - - client_credentials = 'client_credentials', - - refresh_token = 'refresh_token', - - password = 'password' -} - -export type OAuthClient = { - clientId: string; - clientSecret: string; - redirectUris: string[]; - grants: Grant[]; -}; -export type OAuthAuthorizationCode = { - authorizationCode: string; - expiresAt: Date; - redirectUri: string; - scope?: string[]; - clientId: string; - userId: string; -}; -export type OAuthUser = { - // email: { type: String, default: '' }, - // firstname: { type: String }, - // lastname: { type: String }, - // password: { type: String }, - // username: { type: String } - // realUserId -}; diff --git a/frontend/desktop/src/types/response/bind.ts b/frontend/desktop/src/types/response/bind.ts new file mode 100644 index 00000000000..656857a1d83 --- /dev/null +++ b/frontend/desktop/src/types/response/bind.ts @@ -0,0 +1,13 @@ +import { MERGE_USER_READY, PROVIDER_STATUS, RESPONSE_MESSAGE } from './utils'; + +enum _BIND_STATUS { + USER_NOT_FOUND = 'USER_NOT_FOUND', + NOT_SUPPORT = 'NOT_SUPPORT', + RESOURCE_CONFLICT = 'RESOURCE_CONFLICT', + DB_ERROR = 'DB_ERROR' +} +export const BIND_STATUS = Object.assign( + Object.assign({}, MERGE_USER_READY, _BIND_STATUS, RESPONSE_MESSAGE), + PROVIDER_STATUS +); +export type BIND_STATUS = typeof BIND_STATUS; diff --git a/frontend/desktop/src/types/response/changeBind.ts b/frontend/desktop/src/types/response/changeBind.ts new file mode 100644 index 00000000000..624e42296dd --- /dev/null +++ b/frontend/desktop/src/types/response/changeBind.ts @@ -0,0 +1,16 @@ +import { MERGE_USER_READY, PROVIDER_STATUS, RESPONSE_MESSAGE } from './utils'; + +enum _CHANGE_BIND_STATUS { + USER_NOT_FOUND = 'USER_NOT_FOUND', + NOT_SUPPORT = 'NOT_SUPPORT', + // OLD_PROVIDER_NOT_EXIST = 'PROVIDER_NOT_EXIST', + // NEW_PROVIDER_USED_CONFLICT = 'NEW_PROVIDER_USED_CONFLICT', + // NEW_PROVIDER_USED_MERGE = 'NEW_PROVIDER_USED_MERGE', + RESOURCE_CONFLICT = 'RESOURCE_CONFLICT', + DB_ERROR = 'DB_ERROR' +} +export const CHANGE_BIND_STATUS = Object.assign( + Object.assign({}, _CHANGE_BIND_STATUS, MERGE_USER_READY, RESPONSE_MESSAGE), + PROVIDER_STATUS +); +export type CHANGE_BIND_STATUS = typeof CHANGE_BIND_STATUS; diff --git a/frontend/desktop/src/types/response/checkResource.ts b/frontend/desktop/src/types/response/checkResource.ts new file mode 100644 index 00000000000..e1c4d1ca96f --- /dev/null +++ b/frontend/desktop/src/types/response/checkResource.ts @@ -0,0 +1,25 @@ +import { _ACCOUNT_STATUS, RESPONSE_MESSAGE } from './utils'; + +enum _RESOURCE_STATUS { + USER_CR_NOT_FOUND = 'USER_CR_NOT_FOUND', + USER_NOT_FOUND = 'USER_NOT_FOUND', + OAUTHPROVIDER_NOT_FOUND = 'OAUTHPROVIDER_NOT_FOUND', + PRIVATE_WORKSPACE_NOT_FOUND = 'PRIVATE_WORKSPACE_NOT_FOUND', + GET_RESOURCE_ERROR = 'GET_RESOURCE_ERROR', + REMAIN_OTHER_REGION_RESOURCE = 'REMAIN_OTHER_REGION_RESOURCE', + REMAIN_WORKSACE_OWNER = 'REMAIN_WORKSACE_OWNER', + REMAIN_CVM = 'REMAIN_CVM', + REMAIN_APP = 'REMAIN_APP', + REMAIN_TEMPLATE = 'REMAIN_TEMPLATE', + REMAIN_OBJECT_STORAGE = 'REMAIN_OBJECT_STORAGE', + REMAIN_DATABASE = 'REMAIN_DATABASE', + KUBECONFIG_NOT_FOUND = 'KUBECONFIG_NOT_FOUND' +} + +export const RESOURCE_STATUS = Object.assign( + {}, + _RESOURCE_STATUS, + RESPONSE_MESSAGE, + _ACCOUNT_STATUS +); +export type RESOURCE_STATUS = typeof RESOURCE_STATUS; diff --git a/frontend/desktop/src/types/response/email.ts b/frontend/desktop/src/types/response/email.ts new file mode 100644 index 00000000000..8f860dd4b01 --- /dev/null +++ b/frontend/desktop/src/types/response/email.ts @@ -0,0 +1,11 @@ +import { RESPONSE_MESSAGE } from './utils'; + +enum _EMAIL_STATUS { + USER_NOT_FOUND = 'USER_NOT_FOUND', + INVALID_PARAMS = 'INVALID_PARAMS', + NOT_SUPPORT = 'NOT_SUPPORT', + RESOURCE_CONFLICT = 'RESOURCE_CONFLICT', + DB_ERROR = 'DB_ERROR' +} +export const EMAIL_STATUS = Object.assign({}, _EMAIL_STATUS, RESPONSE_MESSAGE); +export type EMAIL_STATUS = typeof EMAIL_STATUS; diff --git a/frontend/desktop/src/types/response/merge.ts b/frontend/desktop/src/types/response/merge.ts new file mode 100644 index 00000000000..fb9c25153ba --- /dev/null +++ b/frontend/desktop/src/types/response/merge.ts @@ -0,0 +1,15 @@ +import { _ACCOUNT_STATUS, RESPONSE_MESSAGE } from './utils'; + +enum _USER_MERGE_STATUS { + NOT_SUPPORT = 'NOT_SUPPORT', + OAUTH_PROVIDER_NOT_FOUND = 'OAUTH_PROVIDER_NOT_FOUND', + EXIST_SAME_OAUTH_PROVIDER = 'EXIST_SAME_OAUTH_PROVIDER', + USER_NOT_FOUND = 'USER_NOT_FOUND' +} +export const USER_MERGE_STATUS = Object.assign( + {}, + _USER_MERGE_STATUS, + RESPONSE_MESSAGE, + _ACCOUNT_STATUS +); +export type USER_MERGE_STATUS = typeof USER_MERGE_STATUS; diff --git a/frontend/desktop/src/types/response/unbind.ts b/frontend/desktop/src/types/response/unbind.ts new file mode 100644 index 00000000000..a9a8ebdb796 --- /dev/null +++ b/frontend/desktop/src/types/response/unbind.ts @@ -0,0 +1,10 @@ +import { PROVIDER_STATUS, RESPONSE_MESSAGE } from './utils'; + +enum _UNBIND_STATUS { + USER_NOT_FOUND = 'USER_NOT_FOUND', + NOT_SUPPORT = 'NOT_SUPPORT', + RESOURCE_CONFLICT = 'RESOURCE_CONFLICT', + DB_ERROR = 'DB_ERROR' +} +export const UNBIND_STATUS = Object.assign({}, _UNBIND_STATUS, RESPONSE_MESSAGE, PROVIDER_STATUS); +export type UNBIND_STATUS = typeof UNBIND_STATUS; diff --git a/frontend/desktop/src/types/response/utils.ts b/frontend/desktop/src/types/response/utils.ts new file mode 100644 index 00000000000..a6e333a38c9 --- /dev/null +++ b/frontend/desktop/src/types/response/utils.ts @@ -0,0 +1,17 @@ +export enum RESPONSE_MESSAGE { + RESULT_SUCCESS = 'RESULT_SUCCESS', + INTERNAL_SERVER_ERROR = 'INTERNAL SERVER ERROR' +} + +export enum _ACCOUNT_STATUS { + INSUFFICENT_BALANCE = 'INSUFFICENT_BALANCE', + ACCOUNT_NOT_FOUND = 'ACCOUNT_NOT_FOUND' +} +export enum MERGE_USER_READY { + MERGE_USER_CONTINUE = 'USER_MERGE', + MERGE_USER_PROVIDER_CONFLICT = 'PROVIDER_CONFLICT' +} +export enum PROVIDER_STATUS { + PROVIDER_NOT_FOUND = 'PROVIDER_NOT_FOUND', + PROVIDER_EXIST = 'PROVIDER_EXIST' +} diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index e4e8f99c45b..383c8359cd4 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -12,8 +12,17 @@ export type CommonConfigType = { apiEnabled: boolean; rechargeEnabled: boolean; cfSiteKey?: string; + templateUrl?: string; + objectstorageUrl: string; + applaunchpadUrl: string; + dbproviderUrl: string; }; -export type CommonClientConfigType = DeepRequired>; +export type CommonClientConfigType = DeepRequired< + Omit< + CommonConfigType, + 'apiEnabled' | 'objectstorageUrl' | 'applaunchpadUrl' | 'dbproviderUrl' | 'templateUrl' + > +>; export type DatabaseConfigType = { mongodbURI: string; globalCockroachdbURI: string; @@ -66,6 +75,8 @@ export type AuthConfigType = { baiduToken?: string; jwt: JwtConfigType; billingUrl?: string; + workorderUrl?: string; + cloudVitrualMachineUrl: string; invite?: { enabled: boolean; lafSecretKey: string; @@ -101,6 +112,13 @@ export type AuthConfigType = { accessKeyID: string; accessKeySecret?: string; }; + email?: { + enabled: boolean; + host: string; + port: number; + user: string; + password: string; + }; }; oauth2?: { enabled: boolean; @@ -126,9 +144,12 @@ export type AuthClientConfigType = DeepRequired< 'idp.wechat.clientSecret', 'idp.google.clientSecret', 'idp.sms.ali', + 'idp.sms.email', 'idp.oauth2.clientSecret', 'jwt', - 'billingUrl' + 'billingUrl', + 'workorderUrl', + 'cloudVitrualMachineUrl' ] > >; diff --git a/frontend/desktop/src/types/team.ts b/frontend/desktop/src/types/team.ts index feebe6a18de..621938df581 100644 --- a/frontend/desktop/src/types/team.ts +++ b/frontend/desktop/src/types/team.ts @@ -1,3 +1,5 @@ +import { K8sApiDefault } from '@/services/backend/kubernetes/admin'; +import * as k8s from '@kubernetes/client-node'; import { UUID, createHash } from 'crypto'; import * as yaml from 'js-yaml'; export type RoleAction = 'Grant' | 'Deprive' | 'Change' | 'Create' | 'Modify'; @@ -30,21 +32,14 @@ type CRD = { role: RoleType; }; }; -export enum watchEventType { - ADDED = 'ADDED', - MODIFIED = 'MODIFIED', - DELETED = 'DELETED', - BOOKMARK = 'BOOKMARK' -} -export const generateRequestCrd = (props: CRD['spec'] & { namespace: string }) => { - const hash = createHash('sha256'); - hash.update(JSON.stringify(props) + new Date().getTime()); - const name = hash.digest('hex'); +export const generateRequestCrd = ({ + ...props +}: CRD['spec'] & { namespace: string; name: string }) => { const requestCrd: CRD = { apiVersion: 'user.sealos.io/v1', kind: 'Operationrequest', metadata: { - name, + name: props.name, namespace: props.namespace }, spec: { @@ -53,7 +48,6 @@ export const generateRequestCrd = (props: CRD['spec'] & { namespace: string }) = role: props.role } }; - try { const result = yaml.dump(requestCrd); return result; diff --git a/frontend/desktop/src/types/token.ts b/frontend/desktop/src/types/token.ts index 69e41d0a2b4..37cfe813e92 100644 --- a/frontend/desktop/src/types/token.ts +++ b/frontend/desktop/src/types/token.ts @@ -9,3 +9,8 @@ export type AccessTokenPayload = { workspaceUid: string; workspaceId: string; } & AuthenticationTokenPayload; + +export type CronJobTokenPayload = { + mergeUserUid: string; + userUid: string; +}; diff --git a/frontend/desktop/src/types/tools.ts b/frontend/desktop/src/types/tools.ts index 731908114cb..29a110ea3e8 100644 --- a/frontend/desktop/src/types/tools.ts +++ b/frontend/desktop/src/types/tools.ts @@ -29,3 +29,16 @@ export type OmitPathArr = Paths extends [infer Hea ? OmitPathArr, Rest> : never : Obj; +export type ValueOf = T[keyof T]; +export type AsyncResult< + TData extends unknown, + StatusUnion extends unknown, + SuccessStatus extends StatusUnion +> = [TData] extends [never] + ? { status: StatusUnion } + : + | { + status: SuccessStatus; + data: TData; + } + | { status: Exclude }; diff --git a/frontend/desktop/src/types/user.ts b/frontend/desktop/src/types/user.ts index 229cbc5d9f1..4fb14d82b02 100644 --- a/frontend/desktop/src/types/user.ts +++ b/frontend/desktop/src/types/user.ts @@ -1,5 +1,6 @@ +import { _ACCOUNT_STATUS, RESPONSE_MESSAGE } from './response/utils'; import { InvitedStatus, UserRole } from './team'; - +import { ProviderType } from 'prisma/global/generated/client'; export type TgithubToken = { access_token: string; expires_in: number; @@ -63,19 +64,32 @@ export type TgithubUser = { created_at: string; updated_at: string; }; +export type TgoogleUser = { + iss: string; + azp: string; + aud: string; + sub: string; + at_hash: string; + name: string; + picture: string; + given_name: string; + family_name: string; + locale: string; + iat: number; + exp: number; +}; // if default, uid export const PROVIDERS = [ - 'github', - 'wechat', - 'phone', - 'uid', - 'password_user', - 'google', - 'wechat_open', - 'oauth2' + 'GITHUB', + 'WECHAT', + 'PHONE', + 'PASSWORD', + 'GOOGLE', + 'WECHAT_OPEN', + 'OAUTH2', + 'EMAIL' ] as const; -export type Provider = (typeof PROVIDERS)[number]; -export type OauthProvider = Exclude; +export type OauthProvider = Exclude; export type TUserExist = { user: string; exist: boolean }; export type K8s_user = { diff --git a/frontend/desktop/src/types/valuation.ts b/frontend/desktop/src/types/valuation.ts deleted file mode 100644 index da4244ceb53..00000000000 --- a/frontend/desktop/src/types/valuation.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type ValuationStandard = { - name: string; - unit: string; - price: string; -}; -export type ValuationBillingRecord = { - price: number; - resourceType: string; -}; -export type ValuationData = { - apiVersion: 'account.sealos.io/v1'; - kind: 'PriceQuery'; - metadata: any; - spec: {}; - status: { - billingRecords: ValuationBillingRecord[]; - }; -}; diff --git a/frontend/desktop/src/utils/crypto.ts b/frontend/desktop/src/utils/crypto.ts index d7596eed45c..2aa2fc65c8b 100644 --- a/frontend/desktop/src/utils/crypto.ts +++ b/frontend/desktop/src/utils/crypto.ts @@ -11,3 +11,5 @@ export const verifyPassword = (password: string, hash: string): boolean => hashPassword(password) === hash; export const strongPassword = (password: string): boolean => /^(?=.*\S).{8,}$/.test(password); export const strongUsername = (username: string): boolean => /^[a-zA-Z0-9_-]{3,16}$/.test(username); +export const isEmail = (email: string) => + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); diff --git a/frontend/desktop/src/utils/format.ts b/frontend/desktop/src/utils/format.ts index 5f24f135ef4..aa33bfcf4d3 100644 --- a/frontend/desktop/src/utils/format.ts +++ b/frontend/desktop/src/utils/format.ts @@ -52,3 +52,23 @@ export const getRemainingTime = (expirationTime: number) => { const formattedTime = `${hours}小时${minutes}分钟`; return formattedTime; }; + +export function maskEmail(email: string): string { + const atIndex = email.indexOf('@'); + if (atIndex === -1) { + return email; + } + + const username = email.substring(0, atIndex); + const domain = email.substring(atIndex); + + if (username.length <= 3) { + return username + '****' + domain; + } + + const maskedUsername = + username.substring(0, 3) + + '*'.repeat(username.length - 4) + + username.substring(username.length - 1); + return maskedUsername + domain; +} diff --git a/frontend/desktop/src/utils/tools.ts b/frontend/desktop/src/utils/tools.ts index 529d9bcf2b3..0ae4f9c55f9 100644 --- a/frontend/desktop/src/utils/tools.ts +++ b/frontend/desktop/src/utils/tools.ts @@ -1,6 +1,10 @@ import { InvitedStatus, UserNsStatus, UserRole } from '@/types/team'; import dayjs from 'dayjs'; import { JoinStatus, Role } from 'prisma/region/generated/client'; +import { Prisma as GlobalPrisma } from 'prisma/global/generated/client'; +import { OauthProvider } from '@/types/user'; +import { Prisma } from '@prisma/client/extension'; +import { globalPrisma } from '@/services/backend/db/init'; export const validateNumber = (num: number) => typeof num === 'number' && isFinite(num) && num > 0; @@ -15,6 +19,7 @@ export function appWaitSeconds(ms: number) { }, ms); }); } + export async function getBase64FromRemote(url: string) { try { const res = await fetch(url); @@ -99,6 +104,7 @@ export const joinStatusToNStatus = (js: JoinStatus): InvitedStatus => { export const NStatusToJoinStatus = (ns: InvitedStatus): JoinStatus => { return [JoinStatus.INVITED, JoinStatus.IN_WORKSPACE, JoinStatus.NOT_IN_WORKSPACE][ns]; }; + export function compareFirstLanguages(acceptLanguageHeader: string) { const indexOfZh = acceptLanguageHeader.indexOf('zh'); const indexOfEn = acceptLanguageHeader.indexOf('en'); @@ -106,3 +112,19 @@ export function compareFirstLanguages(acceptLanguageHeader: string) { if (indexOfEn === -1 || indexOfZh < indexOfEn) return 'zh'; return 'en'; } + +type TOauthProviders = Prisma.Result< + typeof globalPrisma.oauthProvider, + GlobalPrisma.OauthProviderDefaultArgs, + 'findMany' +>; + +export function userCanMerge( + mergeUserOauthProviders: TOauthProviders, + userOauthProviders: TOauthProviders +) { + const curTypeList = userOauthProviders.map((o) => o.providerType); + const mergeTypeSet = new Set(mergeUserOauthProviders.map((o) => o.providerType)); + const canMerge = curTypeList.every((t) => !mergeTypeSet.has(t)); + return canMerge; +} diff --git a/frontend/desktop/tsconfig.json b/frontend/desktop/tsconfig.json index e4ff73c013e..ac5ba948a69 100644 --- a/frontend/desktop/tsconfig.json +++ b/frontend/desktop/tsconfig.json @@ -7,6 +7,7 @@ "@/*": ["src/*"], "prisma/*": ["prisma/*"] }, + "isolatedModules": true, "types": ["@types/jest"] }, "include": [ diff --git a/frontend/packages/ui/src/components/icons/line/WarnTriange.tsx b/frontend/packages/ui/src/components/icons/line/WarnTriange.tsx new file mode 100644 index 00000000000..18e811ed36b --- /dev/null +++ b/frontend/packages/ui/src/components/icons/line/WarnTriange.tsx @@ -0,0 +1,24 @@ +import { createIcon } from '@chakra-ui/react'; + +const WarnTriangeIcon = createIcon({ + displayName: 'WarnTriangeIcon', + viewBox: '0 0 25 24', + path: ( + + + + + + + + + + + ) +}); + +export default WarnTriangeIcon; diff --git a/frontend/packages/ui/src/components/index.ts b/frontend/packages/ui/src/components/index.ts index 986c1693e24..db02c0a1cc6 100644 --- a/frontend/packages/ui/src/components/index.ts +++ b/frontend/packages/ui/src/components/index.ts @@ -50,7 +50,7 @@ import useMessage from './Message/index'; import EditTabs from './EditTabs'; import YamlCode from './YamlCode'; import ProviderIcon from './icons/ProviderIcon'; - +import WarnTriangeIcon from './icons/line/WarnTriange'; export { SealosMenu } from './Menu'; export { Tabs } from './Tabs'; export { MyRangeSlider } from './RangeSlider'; @@ -115,5 +115,6 @@ export { SortPolygonUpIcon, SortPolygonDownIcon, ProviderIcon, - WebHostIcon + WebHostIcon, + WarnTriangeIcon }; diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1852b5c992c..123703a8e2c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + croner: + specifier: ^8.0.2 + version: 8.0.2 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -156,6 +159,9 @@ importers: next-pwa: specifier: ^5.6.0 version: 5.6.0(@babel/core@7.23.5)(next@13.3.0)(webpack@5.89.0) + nodemailer: + specifier: ^6.9.13 + version: 6.9.13 nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -232,6 +238,9 @@ importers: '@types/node': specifier: 18.15.11 version: 18.15.11 + '@types/nodemailer': + specifier: ^6.4.15 + version: 6.4.15 '@types/nprogress': specifier: ^0.2.1 version: 0.2.3 @@ -9851,6 +9860,12 @@ packages: resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} dev: true + /@types/nodemailer@6.4.15: + resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==} + dependencies: + '@types/node': 20.10.0 + dev: true + /@types/nprogress@0.2.3: resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} dev: true @@ -11737,6 +11752,11 @@ packages: luxon: 3.4.4 dev: false + /croner@8.0.2: + resolution: {integrity: sha512-HgSdlSUX8mIgDTTiQpWUP4qY4IFRMsduPCYdca34Pelt8MVdxdaDOzreFtCscA6R+cRZd7UbD1CD3uyx6J3X1A==} + engines: {node: '>=18.0'} + dev: false + /croner@8.1.0: resolution: {integrity: sha512-sz990XOUPR8dG/r5BRKMBd15MYDDUu8oeSaxFD5DqvNgHSZw8Psd1s689/IGET7ezxRMiNlCIyGeY1Gvxp/MLg==} engines: {node: '>=18.0'} @@ -17888,6 +17908,11 @@ packages: hasBin: true dev: false + /nodemailer@6.9.13: + resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + engines: {node: '>=6.0.0'} + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} From 1b92dcde5bc48b91eb0b48e531e557b57bf6c182 Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:27:30 +0800 Subject: [PATCH 4/6] update: Set default k8sVersion in license-system (#4889) --- .../workorder/src/pages/api/cronjob/init.ts | 12 +++++++++++- .../workorder/src/pages/api/workorder/check.ts | 2 +- .../src/pages/cluster/components/ConfigForm.tsx | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/providers/workorder/src/pages/api/cronjob/init.ts b/frontend/providers/workorder/src/pages/api/cronjob/init.ts index 4e69390a50d..54493c36b4d 100644 --- a/frontend/providers/workorder/src/pages/api/cronjob/init.ts +++ b/frontend/providers/workorder/src/pages/api/cronjob/init.ts @@ -2,6 +2,8 @@ import { jsonRes } from '@/services/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; import { Cron } from 'croner'; +const adminToken = process.env.ADMIN_API_TOKEN; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { action } = req.query; @@ -11,7 +13,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) global.cronJobWorkOrders = new Cron( process.env.NODE_ENV === 'production' ? '0,30 10-17 * * 1-5' : '* * * * *', async () => { - const result = await (await fetch(`${baseurl}/api/workorder/check`)).json(); + const result = await ( + await fetch(`${baseurl}/api/workorder/check`, { + method: 'GET', + headers: { + Authorization: `Bearer ${adminToken}`, + 'Content-Type': 'application/json' + } + }) + ).json(); const now = new Date(); console.log( `Cron Job Run at: ${now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}` diff --git a/frontend/providers/workorder/src/pages/api/workorder/check.ts b/frontend/providers/workorder/src/pages/api/workorder/check.ts index 3e1010cc6be..71ec45664ad 100644 --- a/frontend/providers/workorder/src/pages/api/workorder/check.ts +++ b/frontend/providers/workorder/src/pages/api/workorder/check.ts @@ -7,7 +7,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; const feishuUrl = process.env.ADMIN_FEISHU_URL; const feishuCallBackUrl = process.env.ADMIN_FEISHU_CALLBACK_URL; -const adminToken = process.env.ADMIN_API_TOKEN; const MINUTES_IN_A_WEEK = 7 * 24 * 60; const getFeishuForm = ({ @@ -95,6 +94,7 @@ const getFeishuForm = ({ export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { + const adminToken = req.headers['authorization']?.split(' ')[1]; if (!adminToken) { return jsonRes(res, { code: 401, diff --git a/service/license/src/pages/cluster/components/ConfigForm.tsx b/service/license/src/pages/cluster/components/ConfigForm.tsx index 28cf5c6891f..c5da908a954 100644 --- a/service/license/src/pages/cluster/components/ConfigForm.tsx +++ b/service/license/src/pages/cluster/components/ConfigForm.tsx @@ -60,7 +60,7 @@ export default function CommandForm({ path: '/root/.ssh/id_rsa', password: '' }, - k8sVersion: '1.25.6', + k8sVersion: '1.27.11', podSubnet: '100.64.0.0/10', serviceSubnet: '10.96.0.0/22', selfSigned: true, From 88961c5b64ae7bbc2f63598bd252fe1ef90847bb Mon Sep 17 00:00:00 2001 From: zijiren <84728412+zijiren233@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:32:16 +0800 Subject: [PATCH 5/6] feat: strip bin and trimpath (#4860) --- controllers/account/Makefile | 4 +++- controllers/app/Makefile | 2 +- controllers/db/adminer/Makefile | 2 +- controllers/job/heartbeat/Makefile | 2 +- controllers/job/init/Makefile | 4 +++- controllers/license/Makefile | 4 ++-- controllers/node/Makefile | 2 +- controllers/objectstorage/Makefile | 4 +++- controllers/resources/Makefile | 4 +++- controllers/terminal/Makefile | 2 +- controllers/user/Makefile | 2 +- service/account/Makefile | 4 +++- webhooks/admission/Makefile | 2 +- 13 files changed, 24 insertions(+), 14 deletions(-) diff --git a/controllers/account/Makefile b/controllers/account/Makefile index 6f6ceb3c0e9..fe63ee8f5f4 100644 --- a/controllers/account/Makefile +++ b/controllers/account/Makefile @@ -63,7 +63,9 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build $(shell [ -n "${CRYPTOKEY}" ] && echo "-ldflags '-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}'") -o bin/manager main.go + LD_FLAGS="-s -w"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/app/Makefile b/controllers/app/Makefile index 9f69081f38c..91c2ede6e51 100644 --- a/controllers/app/Makefile +++ b/controllers/app/Makefile @@ -70,7 +70,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build -o bin/manager cmd/main.go + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/db/adminer/Makefile b/controllers/db/adminer/Makefile index 61fc5f08479..92bd69e2cd7 100644 --- a/controllers/db/adminer/Makefile +++ b/controllers/db/adminer/Makefile @@ -63,7 +63,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 go build -o bin/manager main.go + CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/job/heartbeat/Makefile b/controllers/job/heartbeat/Makefile index 27000b99280..84e977e363e 100644 --- a/controllers/job/heartbeat/Makefile +++ b/controllers/job/heartbeat/Makefile @@ -48,7 +48,7 @@ vet: ## Run go vet against code. .PHONY: build build: fmt vet ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build -o bin/heartbeat-${GOARCH} cmd/main.go && chmod +x bin/heartbeat-${GOARCH} && cp bin/heartbeat-${GOARCH} bin/manager + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/heartbeat-${GOARCH} cmd/main.go && chmod +x bin/heartbeat-${GOARCH} && cp bin/heartbeat-${GOARCH} bin/manager .PHONY: run diff --git a/controllers/job/init/Makefile b/controllers/job/init/Makefile index 0a70765c430..3392aa8b514 100644 --- a/controllers/job/init/Makefile +++ b/controllers/job/init/Makefile @@ -50,7 +50,9 @@ vet: ## Run go vet against code. CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal/controller build: fmt vet ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build $(shell [ -n "${CRYPTOKEY}" ] && echo "-ldflags '-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}'") -o bin/preset-${GOARCH} cmd/preset/main.go && chmod +x bin/preset-${GOARCH} && cp bin/preset-${GOARCH} bin/manager + LD_FLAGS="-s -w"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/preset-${GOARCH} cmd/preset/main.go && chmod +x bin/preset-${GOARCH} && cp bin/preset-${GOARCH} bin/manager .PHONY: run diff --git a/controllers/license/Makefile b/controllers/license/Makefile index 95c4ce484fc..d9086310865 100644 --- a/controllers/license/Makefile +++ b/controllers/license/Makefile @@ -68,10 +68,10 @@ test: manifests generate fmt vet envtest ## Run tests. CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal build: manifests generate fmt vet ## Build manager binary. - LD_FLAGS=""; \ + LD_FLAGS="-s -w"; \ [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X ${CONTROLLER_PKG}/crypto.encryptionKey=${CRYPTOKEY} -X ${CONTROLLER_PKG}/database.cryptoKey=${CRYPTOKEY}"; \ [ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_LICENSE}/util/key.EncryptionKey=${LICENSE_KEY}"; \ - CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -o bin/manager cmd/manager/main.go + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager cmd/manager/main.go .PHONY: run diff --git a/controllers/node/Makefile b/controllers/node/Makefile index 8b7a684ea88..48cd9990fb5 100644 --- a/controllers/node/Makefile +++ b/controllers/node/Makefile @@ -63,7 +63,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build -o bin/manager main.go + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/objectstorage/Makefile b/controllers/objectstorage/Makefile index 34e0ee2cc67..cbe43898401 100644 --- a/controllers/objectstorage/Makefile +++ b/controllers/objectstorage/Makefile @@ -63,7 +63,9 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build $(shell [ -n "${CRYPTOKEY}" ] && echo "-ldflags '-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}'") -o bin/manager main.go + LD_FLAGS="-s -w"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/resources/Makefile b/controllers/resources/Makefile index 23032dc2865..d6733814fb5 100644 --- a/controllers/resources/Makefile +++ b/controllers/resources/Makefile @@ -63,7 +63,9 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build $(shell [ -n "${CRYPTOKEY}" ] && echo "-ldflags '-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}'") -o bin/manager main.go + LD_FLAGS="-s -w"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/terminal/Makefile b/controllers/terminal/Makefile index 92442516e15..1fe5b0a7c45 100644 --- a/controllers/terminal/Makefile +++ b/controllers/terminal/Makefile @@ -65,7 +65,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -o bin/manager main.go + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "-s -w" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/controllers/user/Makefile b/controllers/user/Makefile index dfb4ca10285..3fada975794 100644 --- a/controllers/user/Makefile +++ b/controllers/user/Makefile @@ -65,7 +65,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -o bin/manager main.go + CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "-s -w" -trimpath -o bin/manager main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. diff --git a/service/account/Makefile b/service/account/Makefile index fcf211938e0..a00677e3a90 100644 --- a/service/account/Makefile +++ b/service/account/Makefile @@ -43,7 +43,9 @@ clean: .PHONY: build build: ## Build service-hub binary. - CGO_ENABLED=0 GOOS=linux go build $(shell [ -n "${CRYPTOKEY}" ] && echo "-ldflags '-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY}'") -o bin/manager main.go + LD_FLAGS="-s -w"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY}"; \ + CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: docker-build docker-build: build diff --git a/webhooks/admission/Makefile b/webhooks/admission/Makefile index b1b0208a824..979100c8d54 100644 --- a/webhooks/admission/Makefile +++ b/webhooks/admission/Makefile @@ -65,7 +65,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. - CGO_ENABLED=0 GOOS=linux go build -o bin/manager cmd/main.go + CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -trimpath -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. From 4288104427fc497567f7768671e217a76af740d4 Mon Sep 17 00:00:00 2001 From: zijiren <84728412+zijiren233@users.noreply.github.com> Date: Sat, 13 Jul 2024 18:54:05 +0800 Subject: [PATCH 6/6] fix: concat ldflags str (#4893) --- controllers/account/Makefile | 2 +- controllers/job/init/Makefile | 2 +- controllers/license/Makefile | 2 +- controllers/objectstorage/Makefile | 2 +- controllers/resources/Makefile | 2 +- service/account/Makefile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/controllers/account/Makefile b/controllers/account/Makefile index fe63ee8f5f4..06035dfcf40 100644 --- a/controllers/account/Makefile +++ b/controllers/account/Makefile @@ -64,7 +64,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. LD_FLAGS="-s -w"; \ - [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: run diff --git a/controllers/job/init/Makefile b/controllers/job/init/Makefile index 3392aa8b514..49d5c8ad124 100644 --- a/controllers/job/init/Makefile +++ b/controllers/job/init/Makefile @@ -51,7 +51,7 @@ CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal/controller build: fmt vet ## Build manager binary. LD_FLAGS="-s -w"; \ - [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/preset-${GOARCH} cmd/preset/main.go && chmod +x bin/preset-${GOARCH} && cp bin/preset-${GOARCH} bin/manager diff --git a/controllers/license/Makefile b/controllers/license/Makefile index d9086310865..c71b79652e6 100644 --- a/controllers/license/Makefile +++ b/controllers/license/Makefile @@ -69,7 +69,7 @@ CONTROLLER_PKG=github.com/labring/sealos/controllers/pkg CONTROLLER_LICENSE=github.com/labring/sealos/controllers/license/internal build: manifests generate fmt vet ## Build manager binary. LD_FLAGS="-s -w"; \ - [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X ${CONTROLLER_PKG}/crypto.encryptionKey=${CRYPTOKEY} -X ${CONTROLLER_PKG}/database.cryptoKey=${CRYPTOKEY}"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_PKG}/crypto.encryptionKey=${CRYPTOKEY} -X ${CONTROLLER_PKG}/database.cryptoKey=${CRYPTOKEY}"; \ [ -n "$(LICENSE_KEY)" ] && LD_FLAGS+=" -X ${CONTROLLER_LICENSE}/util/key.EncryptionKey=${LICENSE_KEY}"; \ CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager cmd/manager/main.go diff --git a/controllers/objectstorage/Makefile b/controllers/objectstorage/Makefile index cbe43898401..ec50253fed7 100644 --- a/controllers/objectstorage/Makefile +++ b/controllers/objectstorage/Makefile @@ -64,7 +64,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. LD_FLAGS="-s -w"; \ - [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: run diff --git a/controllers/resources/Makefile b/controllers/resources/Makefile index d6733814fb5..867f334d4f6 100644 --- a/controllers/resources/Makefile +++ b/controllers/resources/Makefile @@ -64,7 +64,7 @@ test: manifests generate fmt vet envtest ## Run tests. .PHONY: build build: ## Build manager binary. LD_FLAGS="-s -w"; \ - [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY} -X github.com/labring/sealos/controllers/pkg/database.cryptoKey=${CRYPTOKEY}"; \ CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: run diff --git a/service/account/Makefile b/service/account/Makefile index a00677e3a90..9754adf8fb8 100644 --- a/service/account/Makefile +++ b/service/account/Makefile @@ -44,7 +44,7 @@ clean: .PHONY: build build: ## Build service-hub binary. LD_FLAGS="-s -w"; \ - [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+="-X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY}"; \ + [ -n "$(CRYPTOKEY)" ] && LD_FLAGS+=" -X github.com/labring/sealos/controllers/pkg/crypto.encryptionKey=${CRYPTOKEY}"; \ CGO_ENABLED=0 GOOS=linux go build -ldflags "$${LD_FLAGS}" -trimpath -o bin/manager main.go .PHONY: docker-build