From b6053c1c2601fafa0752cdaac4501606bd020a20 Mon Sep 17 00:00:00 2001 From: Jiahui <4543bxy@gmail.com> Date: Wed, 25 Sep 2024 18:13:43 +0800 Subject: [PATCH 01/63] fix get default property (#5108) --- service/account/common/account.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/account/common/account.go b/service/account/common/account.go index a98f1a3b3b9..41473ec3533 100644 --- a/service/account/common/account.go +++ b/service/account/common/account.go @@ -3,10 +3,10 @@ package common import "time" type PropertyQuery struct { - Name string `json:"name,omitempty" bson:"name,omitempty" example:"cpu"` + Name string `json:"name" bson:"name" example:"cpu"` Alias string `json:"alias,omitempty" bson:"alias,omitempty" example:"gpu-tesla-v100"` - UnitPrice float64 `json:"unit_price,omitempty" bson:"unit_price,omitempty" example:"10000"` - Unit string `json:"unit,omitempty" bson:"unit,omitempty" example:"1m"` + UnitPrice float64 `json:"unit_price" bson:"unit_price" example:"10000"` + Unit string `json:"unit" bson:"unit" example:"1m"` } type TimeCostsMap [][]interface{} From 54a3bc28684df2f8427c2ff1cc3969a0dd79c534 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Thu, 26 Sep 2024 11:44:29 +0800 Subject: [PATCH 02/63] style(costcenter): recharge discount (#5111) --- frontend/packages/ui/src/theme/colors.ts | 4 + .../costcenter/public/locales/en/common.json | 7 +- .../costcenter/public/locales/zh/common.json | 7 +- .../src/components/RechargeModal.tsx | 187 +++++++++++++----- .../src/components/icons/GiftIcon.tsx | 36 ++++ .../src/components/icons/HelpIcon.tsx | 17 ++ .../providers/costcenter/src/layout/index.tsx | 1 - .../costcenter/src/layout/sidebar.tsx | 28 ++- .../costcenter/src/pages/api/price/bonus.ts | 71 ++----- .../costcenter/src/pages/api/price/index.ts | 61 ------ .../providers/costcenter/src/service/auth.ts | 1 - .../costcenter/src/service/request.ts | 1 - 12 files changed, 227 insertions(+), 194 deletions(-) create mode 100644 frontend/providers/costcenter/src/components/icons/GiftIcon.tsx create mode 100644 frontend/providers/costcenter/src/components/icons/HelpIcon.tsx delete mode 100644 frontend/providers/costcenter/src/pages/api/price/index.ts diff --git a/frontend/packages/ui/src/theme/colors.ts b/frontend/packages/ui/src/theme/colors.ts index 0857f17af1e..d53d379d28a 100644 --- a/frontend/packages/ui/src/theme/colors.ts +++ b/frontend/packages/ui/src/theme/colors.ts @@ -72,6 +72,10 @@ const baseColors = { 800: '#005B9C', 900: '#004B82' }, + royalBlue: { + 100: '#E1EAFF', + 700: '#2B5FD9' + }, yellow: { 25: '#FFFDFA', 50: '#FFFAEB', diff --git a/frontend/providers/costcenter/public/locales/en/common.json b/frontend/providers/costcenter/public/locales/en/common.json index f5c2d960d45..5eaf94ad580 100644 --- a/frontend/providers/costcenter/public/locales/en/common.json +++ b/frontend/providers/costcenter/public/locales/en/common.json @@ -87,7 +87,7 @@ "Stripe Success": "pay with Stripe successfully", "Stripe Cancel": "cancel to pay with Stripe", "GPU Unit": "Card", - "port_unit": "", + "port_unit": "", "Gpu valuation": "GPU Price Table", "common valuation": "Basic Valuation", "Billing Details": "Billing Details", @@ -233,5 +233,8 @@ "duration": "duration", "GPU": "GPU", "usage": "usage", - "resource": "resource" + "resource": "resource", + "Double": "Double", + "first_recharge_tips": "Partial specifications can enjoy double the amount of the initial recharge.", + "first_recharge_title": "Double on First Recharge" } diff --git a/frontend/providers/costcenter/public/locales/zh/common.json b/frontend/providers/costcenter/public/locales/zh/common.json index 2678089b168..4fbce721f53 100644 --- a/frontend/providers/costcenter/public/locales/zh/common.json +++ b/frontend/providers/costcenter/public/locales/zh/common.json @@ -84,7 +84,7 @@ "Stripe Success": "Stripe 支付完成", "Stripe Cancel": "Stripe 支付取消", "GPU Unit": "卡", - "port_unit": "个", + "port_unit": "个", "Name": "名称", "Price": "价格", "Unit": "单位", @@ -233,5 +233,8 @@ "duration": "时间", "GPU": "GPU", "usage": "用量", - "resource": "资源" + "resource": "资源", + "Double": "双倍", + "first_recharge_tips": "部分规格首次充值可享双倍赠送金额", + "first_recharge_title": "首充双倍" } diff --git a/frontend/providers/costcenter/src/components/RechargeModal.tsx b/frontend/providers/costcenter/src/components/RechargeModal.tsx index cbfa3d1a146..259f0743dda 100644 --- a/frontend/providers/costcenter/src/components/RechargeModal.tsx +++ b/frontend/providers/costcenter/src/components/RechargeModal.tsx @@ -5,9 +5,9 @@ import { default as CurrencySymbol, default as Currencysymbol } from '@/componen import OuterLink from '@/components/outerLink'; import { useCustomToast } from '@/hooks/useCustomToast'; import useEnvStore from '@/stores/env'; +import useSessionStore from '@/stores/session'; import { ApiResp } from '@/types/api'; import { Pay, Payment } from '@/types/payment'; -import { getFavorable } from '@/utils/favorable'; import { deFormatMoney, formatMoney } from '@/utils/format'; import { Box, @@ -29,13 +29,16 @@ import { Text, useDisclosure } from '@chakra-ui/react'; +import { MyTooltip } from '@sealos/ui'; import { Stripe } from '@stripe/stripe-js'; import { useMutation, useQuery } from '@tanstack/react-query'; import type { AxiosInstance } from 'axios'; import { isNumber } from 'lodash'; import { useTranslation } from 'next-i18next'; import { QRCodeSVG } from 'qrcode.react'; -import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import GiftIcon from './icons/GiftIcon'; +import HelpIcon from './icons/HelpIcon'; const StripeForm = (props: { tradeNO?: string; complete: number; @@ -122,10 +125,12 @@ const BonusBox = (props: { onClick: () => void; selected: boolean; bouns: number; + isFirst?: boolean; amount: number; }) => { const { t } = useTranslation(); const currency = useEnvStore((s) => s.currency); + return ( - - {t('Bonus')} - - {props.bouns} - + {props.isFirst ? ( + + + {t('Double')}! + + + + + {props.bouns} + + + + ) : ( + + {t('Bonus')} + + {props.bouns} + + )} @@ -283,33 +329,51 @@ const RechargeModal = forwardRef( cancalPay(); _onClose(); }; - + const { session } = useSessionStore(); const { toast } = useCustomToast(); const { data: bonuses, isSuccess } = useQuery( - ['bonus'], + ['bonus', session.user.id], () => - request.get< + request.post< any, ApiResp<{ - steps: number[]; - ratios: number[]; - specialDiscount: [number, number][]; + discount: { + defaultSteps: Record; + firstRechargeDiscount: Record; + }; }> >('/api/price/bonus'), {} ); - const ratios = bonuses?.data?.ratios || []; - const steps = bonuses?.data?.steps || []; - const specialBonus = bonuses?.data?.specialDiscount; + const [defaultSteps, ratios, steps, specialBonus] = useMemo(() => { + const defaultSteps = Object.entries(bonuses?.data?.discount.defaultSteps || {}).toSorted( + (a, b) => +a[0] - +b[0] + ); + const ratios = defaultSteps.map(([key, value]) => value); + const steps = defaultSteps.map(([key, value]) => +key); + const specialBonus = Object.entries( + bonuses?.data?.discount.firstRechargeDiscount || {} + ).toSorted((a, b) => +a[0] - +b[0]); + const temp: number[] = []; + specialBonus.forEach(([k, v]) => { + const step = +k; + if (steps.findIndex((v) => step === v) === -1) { + temp.push(+k); + } + }); + steps.unshift(...temp); + ratios.unshift(...temp.map(() => 0)); + return [defaultSteps, ratios, steps, specialBonus]; + }, [bonuses?.data?.discount.defaultSteps, bonuses?.data?.discount.firstRechargeDiscount]); const [amount, setAmount] = useState(() => 0); - const getBonus = useCallback( - (amount: number) => { - if (isSuccess && ratios && steps && ratios.length === steps.length) - return getFavorable(steps, ratios, specialBonus)(amount); - else return 0; - }, - [isSuccess, ratios, steps, specialBonus] - ); + const getBonus = (amount: number) => { + let ratio = 0; + let specialIdx = specialBonus.findIndex(([k]) => +k === amount); + if (specialIdx >= 0) return Math.floor((amount * specialBonus[specialIdx][1]) / 100); + const step = [...steps].reverse().findIndex((step) => amount >= step); + if (ratios.length > step && step > -1) ratio = [...ratios].reverse()[step]; + return Math.floor((amount * ratio) / 100); + }; const { stripeEnabled, wechatEnabled } = useEnvStore(); useEffect(() => { if (steps && steps.length > 0) { @@ -346,7 +410,6 @@ const RechargeModal = forwardRef( - - {t('Select Amount')} - + + + {t('Select Amount')} + + + + + {t('first_recharge_title')}! + + {t('first_recharge_tips')}}> + + + + {steps.map((amount, index) => ( +a[0] === amount) >= 0} bouns={getBonus(amount)} onClick={() => { setSelectAmount(index); @@ -419,11 +499,13 @@ const RechargeModal = forwardRef( variant={'unstyled'} onChange={(str, v) => { const maxAmount = 10_000_000; - if (!isNumber(v) || isNaN(v)) { + if (!str || !isNumber(v) || isNaN(v)) { setAmount(0); + return; } if (v > maxAmount) { setAmount(maxAmount); + return; } setAmount(v); }} @@ -506,12 +588,13 @@ const RechargeModal = forwardRef( ) : ( <> - {t('Recharge Amount')} + {t('Recharge Amount')} - + {payType === 'wechat' ? ( + + + + + + + + + + + + + + + ) +}); +export default GiftIcon; diff --git a/frontend/providers/costcenter/src/components/icons/HelpIcon.tsx b/frontend/providers/costcenter/src/components/icons/HelpIcon.tsx new file mode 100644 index 00000000000..245d97bb262 --- /dev/null +++ b/frontend/providers/costcenter/src/components/icons/HelpIcon.tsx @@ -0,0 +1,17 @@ +import { createIcon } from '@chakra-ui/react'; + +export const HelpIcon = createIcon({ + displayName: 'HelpIcon', + viewBox: '0 0 16 16', + path: ( + + + + ) +}); +export default HelpIcon; diff --git a/frontend/providers/costcenter/src/layout/index.tsx b/frontend/providers/costcenter/src/layout/index.tsx index f08b791245b..2c58b4b04c1 100644 --- a/frontend/providers/costcenter/src/layout/index.tsx +++ b/frontend/providers/costcenter/src/layout/index.tsx @@ -2,7 +2,6 @@ import useSessionStore from '@/stores/session'; import { Box, Flex, Link, Text } from '@chakra-ui/react'; import { useEffect, useState } from 'react'; import { createSealosApp, sealosApp } from 'sealos-desktop-sdk/app'; -import styles from './index.module.scss'; import SideBar from './sidebar'; export default function Layout({ children }: any) { diff --git a/frontend/providers/costcenter/src/layout/sidebar.tsx b/frontend/providers/costcenter/src/layout/sidebar.tsx index 7ca81b42aeb..7f9a59b14df 100644 --- a/frontend/providers/costcenter/src/layout/sidebar.tsx +++ b/frontend/providers/costcenter/src/layout/sidebar.tsx @@ -1,20 +1,20 @@ -import { Flex, Text, Img, Divider, Box } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; +import dashbordIcon from '@/assert/dashboard.svg'; +import dashboard_a_icon from '@/assert/dashboard_black.svg'; import letter_icon from '@/assert/format_letter_spacing_standard.svg'; import letter_a_icon from '@/assert/format_letter_spacing_standard_black.svg'; -import receipt_icon from '@/assert/receipt_long.svg'; -import receipt_a_icon from '@/assert/receipt_long_black.svg'; +import invoice_a_icon from '@/assert/invoice-active.svg'; +import invoice_icon from '@/assert/invoice.svg'; import layers_icon from '@/assert/layers.svg'; import layers_a_icon from '@/assert/layers_black.svg'; import linechart_icon from '@/assert/lineChart.svg'; import linechart_a_icon from '@/assert/lineChart_black.svg'; -import invoice_icon from '@/assert/invoice.svg'; -import invoice_a_icon from '@/assert/invoice-active.svg'; -import dashbordIcon from '@/assert/dashboard.svg'; -import dashboard_a_icon from '@/assert/dashboard_black.svg'; -import type { StaticImageData } from 'next/image'; -import { useTranslation } from 'next-i18next'; +import receipt_icon from '@/assert/receipt_long.svg'; +import receipt_a_icon from '@/assert/receipt_long_black.svg'; import useEnvStore from '@/stores/env'; +import { Box, Divider, Flex, Img, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import type { StaticImageData } from 'next/image'; +import { useRouter } from 'next/router'; type Menu = { id: string; @@ -88,8 +88,6 @@ export default function SideBar() { return ( {t(item.value)} - {[0, 2, 4].includes(idx) && } + {([0, 2].includes(idx) || (idx === 4 && invoiceEnabled)) && ( + + )} ); })} diff --git a/frontend/providers/costcenter/src/pages/api/price/bonus.ts b/frontend/providers/costcenter/src/pages/api/price/bonus.ts index d35bbcb47da..b1dd15ecd18 100644 --- a/frontend/providers/costcenter/src/pages/api/price/bonus.ts +++ b/frontend/providers/costcenter/src/pages/api/price/bonus.ts @@ -1,7 +1,5 @@ -import { authSession } from '@/service/backend/auth'; -import { ApplyYaml, CRDMeta, GetCRD, GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; +import { makeAPIClientByHeader } from '@/service/backend/region'; import { jsonRes } from '@/service/backend/response'; -import * as yaml from 'js-yaml'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { @@ -9,69 +7,24 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (!global.AppConfig.costCenter.recharge.enabled) { throw new Error('recharge is not enabled'); } - const kc = await authSession(req.headers); - const user = kc.getCurrentUser(); - if (user === null) { - return jsonRes(resp, { code: 403, message: 'user null' }); - } - const namespace = GetUserDefaultNameSpace(user.name); - const name = new Date().getTime() + 'bonusquery'; + const client = await makeAPIClientByHeader(req, resp); + if (!client) return null; - const meta: CRDMeta = { - group: 'account.sealos.io', - version: 'v1', - namespace, - plural: 'billinginfoqueries' - }; - const crdSchema = { - apiVersion: `account.sealos.io/v1`, - kind: 'BillingInfoQuery', - metadata: { - name, - namespace - }, - spec: { - queryType: 'Recharge' - } - }; - const result1 = await ApplyYaml(kc, yaml.dump(crdSchema)); - const result = await new Promise<{ - discountRates: number[]; - discountSteps: number[]; - specialDiscount: Record; - }>((resolve, reject) => { - let retry = 3; - const wrap = () => - GetCRD(kc, meta, name) - .then((res) => { - const body = res.body as { status: any }; - if (!body.status) return Promise.reject(); - const { result, status } = body.status as Record; - if (status.toLocaleLowerCase() === 'completed') resolve(JSON.parse(result)); - else return Promise.reject(); - }) - .catch(async (err) => { - if (retry-- >= 0) { - await new Promise((res) => setTimeout(res, 1000)); - await wrap(); - } else reject(err); - }); - wrap(); - }); - if (!result) + const response = await client.post<{ + discount: { + defaultSteps: Record; + firstRechargeDiscount: Record; + }; + }>('/account/v1alpha1/recharge-discount'); + const data = response.data; + if (!data || response.status !== 200) return jsonRes(resp, { code: 404, message: 'bonus is not found' }); return jsonRes(resp, { code: 200, - data: { - ratios: result.discountRates, - steps: result.discountSteps, - specialDiscount: Object.entries(result.specialDiscount || {}).map<[number, number]>( - ([k, v]) => [+k, v] - ) - } + data }); } catch (error) { console.log(error); diff --git a/frontend/providers/costcenter/src/pages/api/price/index.ts b/frontend/providers/costcenter/src/pages/api/price/index.ts deleted file mode 100644 index fafbf57cd23..00000000000 --- a/frontend/providers/costcenter/src/pages/api/price/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { authSession } from '@/service/backend/auth'; -import { CRDMeta, GetCRD, GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; -import { jsonRes } from '@/service/backend/response'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { ApplyYaml } from '@/service/backend/kubernetes'; -import * as yaml from 'js-yaml'; -import { ValuationBillingRecord, ValuationData } from '@/types/valuation'; -export default async function handler(req: NextApiRequest, resp: NextApiResponse) { - try { - const kc = await authSession(req.headers); - - // get user account payment amount - const user = kc.getCurrentUser(); - if (user === null) { - return jsonRes(resp, { code: 403, message: 'user null' }); - } - const namespace = GetUserDefaultNameSpace(user.name); - const name = 'price'; - const crdSchema = { - apiVersion: `account.sealos.io/v1`, - kind: 'PriceQuery', - metadata: { - name, - namespace - }, - spec: {} - }; - const meta: CRDMeta = { - group: 'account.sealos.io', - version: 'v1', - namespace, - plural: 'pricequeries' - }; - try { - await ApplyYaml(kc, yaml.dump(crdSchema)); - } catch {} - const billingRecords = await new Promise((resolve, reject) => { - let retry = 3; - const wrap = () => - GetCRD(kc, meta, name) - .then((res) => { - const crd = res.body as ValuationData; - resolve(crd.status.billingRecords); - }) - .catch((err) => { - if (retry-- >= 0) wrap(); - else reject(err); - }); - wrap(); - }); - return jsonRes<{ billingRecords: ValuationBillingRecord[] }>(resp, { - code: 200, - data: { - billingRecords - } - }); - } catch (error) { - console.log(error); - jsonRes(resp, { code: 500, message: 'get price error' }); - } -} diff --git a/frontend/providers/costcenter/src/service/auth.ts b/frontend/providers/costcenter/src/service/auth.ts index c2bc5ed42d1..c271967630a 100644 --- a/frontend/providers/costcenter/src/service/auth.ts +++ b/frontend/providers/costcenter/src/service/auth.ts @@ -21,7 +21,6 @@ export const verifyJWT = (token: string, if (!token) return resolve(null); verify(token, secret, (err, payload) => { if (err) { - // console.log(err); resolve(null); } else if (!payload) { resolve(null); diff --git a/frontend/providers/costcenter/src/service/request.ts b/frontend/providers/costcenter/src/service/request.ts index 1a064104b90..541e505c283 100644 --- a/frontend/providers/costcenter/src/service/request.ts +++ b/frontend/providers/costcenter/src/service/request.ts @@ -14,7 +14,6 @@ request.interceptors.request.use( // auto append service prefix let _headers: RawAxiosRequestHeaders = config.headers || {}; const session = useSessionStore.getState().session; - if (config.url && config.url?.startsWith('/api/')) { _headers['Authorization'] = encodeURIComponent(session?.kubeconfig || ''); } From 8c0c34592369874ce178c956af713dba3c17a9ea Mon Sep 17 00:00:00 2001 From: yy <56745951+lingdie@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:52:49 +0800 Subject: [PATCH 03/63] devobx ignore extra ports. (#5112) --- controllers/devbox/internal/controller/devbox_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index 530a059c9ae..a9e15005f92 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -503,7 +503,8 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runt // set up ports and env by using runtime ports and devbox extra ports ports := runtime.Spec.Config.Ports - ports = append(ports, devbox.Spec.NetworkSpec.ExtraPorts...) + // TODO: add extra ports to pod, currently not support + // ports = append(ports, devbox.Spec.NetworkSpec.ExtraPorts...) envs := runtime.Spec.Config.Env envs = append(envs, devbox.Spec.ExtraEnvs...) From 7b0be9352fa422f1abca8f232cd8c864db548f7e Mon Sep 17 00:00:00 2001 From: yy <56745951+lingdie@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:29:28 +0800 Subject: [PATCH 04/63] release: sealos v5.0.1 beta2 (#5110) * add csi-s3 * set jwt token as env --- .github/workflows/cloud-release.yml | 4 +-- deploy/cloud/scripts/init.sh | 36 +++++++++++++++++-- frontend/desktop/deploy/Kubefile | 3 ++ .../deploy/manifests/configmap.yaml.tmpl | 6 ++-- frontend/desktop/deploy/scripts/init.sh | 3 -- scripts/cloud/build-offline-tar.sh | 1 + scripts/cloud/install.sh | 6 ++-- 7 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cloud-release.yml b/.github/workflows/cloud-release.yml index a273309ba68..fae28f00e44 100644 --- a/.github/workflows/cloud-release.yml +++ b/.github/workflows/cloud-release.yml @@ -99,7 +99,7 @@ jobs: sudo mv /tmp/sealos /usr/bin/sealos sudo sealos version - name: Build - run: export CLOUD_VERSION=${{ github.event.release.tag_name }} && export ARCH=arm64 && bash ./scripts/cloud/build-offline-tar.sh + run: export CLOUD_VERSION=${{ github.event.release.tag_name }} && VERSION=${{ github.event.release.tag_name }} && export ARCH=arm64 && bash ./scripts/cloud/build-offline-tar.sh - name: Setup ossutil uses: manyuanrong/setup-ossutil@v2.0 with: @@ -111,4 +111,4 @@ jobs: - name: Upload run: | ossutil cp ./sealos-cloud.tar.gz oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-arm64.tar.gz - ossutil cp ./sealos-cloud.tar.gz.md5 oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-arm64.tar.gz.md5 \ No newline at end of file + ossutil cp ./sealos-cloud.tar.gz.md5 oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-arm64.tar.gz.md5 diff --git a/deploy/cloud/scripts/init.sh b/deploy/cloud/scripts/init.sh index a46e384c9d8..e52a10f1f23 100644 --- a/deploy/cloud/scripts/init.sh +++ b/deploy/cloud/scripts/init.sh @@ -10,9 +10,12 @@ cockroachdbGlobalUri="" localRegionUID="" tlsCrtPlaceholder="" -tlsKeyPlaceholder="" acmednsSecretPlaceholder="" + saltKey="" +jwtInternal="" +jwtRegional="" +jwtGlobal="" function prepare { # source .env @@ -36,6 +39,9 @@ function prepare { # gen regionUID if not set or not found in secret gen_regionUID + # gen jwt tokens + gen_jwt_tokens + # create tls secret create_tls_secret } @@ -132,6 +138,7 @@ function gen_cockroachdbUri() { cockroachdbGlobalUri="$cockroachdbUri/global" } +# TODO: use a better way to check saltKey function gen_saltKey() { password_salt=$(kubectl get configmap desktop-frontend-config -n sealos -o jsonpath='{.data.config\.yaml}' | grep "salt:" | awk '{print $2}' 2>/dev/null | tr -d '"' || true) if [[ -z "$password_salt" ]]; then @@ -141,6 +148,28 @@ function gen_saltKey() { fi } +# TODO: use a better way to check jwt tokens +function gen_jwt_tokens() { + jwt_internal=$(kubectl get configmap desktop-frontend-config -n sealos -o jsonpath='{.data.config\.yaml}' | grep "internal:" | awk '{print $2}' 2>/dev/null | tr -d '"' || true) + if [[ -z "$jwt_internal" ]]; then + jwtInternal=$(tr -dc 'a-z0-9' /dev/null | tr -d '"' || true) + if [[ -z "$jwt_regional" ]]; then + jwtRegional=$(tr -dc 'a-z0-9' /dev/null | tr -d '"' || true) + if [[ -z "$jwt_global" ]]; then + jwtGlobal=$(tr -dc 'a-z0-9' /dev/null | tr -d '"' || true) if [[ -z "$uid" ]]; then @@ -176,7 +205,10 @@ function sealos_run_desktop { --env regionUID="$localRegionUID" \ --env databaseMongodbURI="${mongodbUri}/sealos-auth?authSource=admin" \ --env databaseLocalCockroachdbURI="$cockroachdbLocalUri" \ - --env databaseGlobalCockroachdbURI="$cockroachdbGlobalUri" + --env databaseGlobalCockroachdbURI="$cockroachdbGlobalUri" \ + --env jwtInternal="$jwtInternal" \ + --env jwtRegional="$jwtRegional" \ + --env jwtGlobal="$jwtGlobal" } function sealos_run_controller { diff --git a/frontend/desktop/deploy/Kubefile b/frontend/desktop/deploy/Kubefile index cb8685bde5f..fe24acf60e3 100644 --- a/frontend/desktop/deploy/Kubefile +++ b/frontend/desktop/deploy/Kubefile @@ -13,5 +13,8 @@ ENV databaseMongodbURI="" ENV databaseGlobalCockroachdbURI="" ENV databaseLocalCockroachdbURI="" ENV passwordSalt="randomSalt" +ENV jwtInternal="" +ENV jwtRegional="" +ENV jwtGlobal="" CMD ["bash scripts/init.sh"] diff --git a/frontend/desktop/deploy/manifests/configmap.yaml.tmpl b/frontend/desktop/deploy/manifests/configmap.yaml.tmpl index 77c3634b860..ac9e9c33a5f 100644 --- a/frontend/desktop/deploy/manifests/configmap.yaml.tmpl +++ b/frontend/desktop/deploy/manifests/configmap.yaml.tmpl @@ -43,9 +43,9 @@ data: invite: enabled: false jwt: - internal: "" - regional: "" - global: "" + internal: "{{ .jwtInternal }}" + regional: "{{ .jwtRegional }}" + global: "{{ .jwtGlobal }}" idp: password: enabled: true diff --git a/frontend/desktop/deploy/scripts/init.sh b/frontend/desktop/deploy/scripts/init.sh index b6ebb7a1476..2c2113eccfe 100644 --- a/frontend/desktop/deploy/scripts/init.sh +++ b/frontend/desktop/deploy/scripts/init.sh @@ -6,8 +6,5 @@ if [[ -n "$cm_exists" ]]; then echo "desktop-frontend-config already exists, skip create desktop config" else echo "create desktop config" - sed -i -e "s;;$(tr -cd 'a-z0-9' ;$(tr -cd 'a-z0-9' ;$(tr -cd 'a-z0-9' Date: Thu, 26 Sep 2024 16:03:47 +0800 Subject: [PATCH 05/63] fix desktop and costcenter configs (#5114) --- deploy/cloud/scripts/init.sh | 5 +++-- frontend/desktop/deploy/manifests/configmap.yaml.tmpl | 1 + frontend/desktop/deploy/manifests/rbac.yaml | 3 +++ frontend/providers/costcenter/deploy/Kubefile | 1 + .../costcenter/deploy/manifests/configmap.yaml.tmpl | 4 ++++ 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/deploy/cloud/scripts/init.sh b/deploy/cloud/scripts/init.sh index e52a10f1f23..1787b1a498c 100644 --- a/deploy/cloud/scripts/init.sh +++ b/deploy/cloud/scripts/init.sh @@ -289,8 +289,9 @@ function sealos_run_frontend { --env cloudPort="$cloudPort" \ --env certSecretName="wildcard-cert" \ --env transferEnabled="true" \ - --env rechargeEnabled="false" - + --env rechargeEnabled="false" \ + --env jwtInternal="$jwtInternal" + echo "run template frontend" sealos run tars/frontend-template.tar \ --env cloudDomain=$cloudDomain \ diff --git a/frontend/desktop/deploy/manifests/configmap.yaml.tmpl b/frontend/desktop/deploy/manifests/configmap.yaml.tmpl index ac9e9c33a5f..dd6d744efd6 100644 --- a/frontend/desktop/deploy/manifests/configmap.yaml.tmpl +++ b/frontend/desktop/deploy/manifests/configmap.yaml.tmpl @@ -39,6 +39,7 @@ data: proxyAddress: "" callbackURL: "https://{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}/callback" signUpEnabled: true + billingUrl: "http://account-service.account-system.svc:2333" baiduToken: "" invite: enabled: false diff --git a/frontend/desktop/deploy/manifests/rbac.yaml b/frontend/desktop/deploy/manifests/rbac.yaml index a42c1180a4f..5a3e98d49e7 100644 --- a/frontend/desktop/deploy/manifests/rbac.yaml +++ b/frontend/desktop/deploy/manifests/rbac.yaml @@ -89,6 +89,9 @@ rules: - apiGroups: ["notification.sealos.io"] resources: ["notifications"] verbs: ["list", "get", "create", "update", "patch", "watch"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/frontend/providers/costcenter/deploy/Kubefile b/frontend/providers/costcenter/deploy/Kubefile index 9c6af5ae51c..139414db66a 100644 --- a/frontend/providers/costcenter/deploy/Kubefile +++ b/frontend/providers/costcenter/deploy/Kubefile @@ -10,5 +10,6 @@ ENV cloudDomain="127.0.0.1.nip.io" ENV cloudPort="" ENV transferEnabled="true" ENV rechargeEnabled="true" +ENV jwtInternal="" CMD ["kubectl apply -f manifests"] diff --git a/frontend/providers/costcenter/deploy/manifests/configmap.yaml.tmpl b/frontend/providers/costcenter/deploy/manifests/configmap.yaml.tmpl index 8e7dba149b3..490f2207df2 100644 --- a/frontend/providers/costcenter/deploy/manifests/configmap.yaml.tmpl +++ b/frontend/providers/costcenter/deploy/manifests/configmap.yaml.tmpl @@ -15,6 +15,10 @@ data: costCenter: transferEnabled: true currencyType: "shellCoin" + auth: + jwt: + internal: "{{ .jwtInternal }}" + billing: "{{ .jwtInternal }}" invoice: enabled: false feiShuBotURL: "" From 2fd2f07892660ce4fa0ad0e167082fbeb372d579 Mon Sep 17 00:00:00 2001 From: zijiren <84728412+zijiren233@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:09:26 +0800 Subject: [PATCH 06/63] fix: cronjob arm64 image (#5113) --- frontend/providers/cronjob/src/utils/json2Yaml.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/providers/cronjob/src/utils/json2Yaml.ts b/frontend/providers/cronjob/src/utils/json2Yaml.ts index 0a739dfbde8..982ea8ce2de 100644 --- a/frontend/providers/cronjob/src/utils/json2Yaml.ts +++ b/frontend/providers/cronjob/src/utils/json2Yaml.ts @@ -35,7 +35,7 @@ export const json2CronJob = (data: CronJobEditType) => { } if (data.jobType === 'launchpad') { - data.imageName = 'nowinkey/curl-kubectl:v1.0.4'; + data.imageName = 'labring4docker/curl-kubectl:v1.0.0'; data.runCMD = `["/bin/sh", "-c"]`; const resources = { requests: { From b78691a56704884d1da0544a2779b8f42b45d3f1 Mon Sep 17 00:00:00 2001 From: xzy Date: Thu, 26 Sep 2024 17:49:03 +0800 Subject: [PATCH 07/63] sync resourcequota objectstorage/size status used (#5102) --- .../controllers/objectstorageuser_controller.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/controllers/objectstorage/controllers/objectstorageuser_controller.go b/controllers/objectstorage/controllers/objectstorageuser_controller.go index adc3ccd0f28..722d912da05 100644 --- a/controllers/objectstorage/controllers/objectstorageuser_controller.go +++ b/controllers/objectstorage/controllers/objectstorageuser_controller.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "fmt" + "strconv" "strings" "time" @@ -148,6 +149,7 @@ func (r *ObjectStorageUserReconciler) Reconcile(ctx context.Context, req ctrl.Re } quota := resourceQuota.Spec.Hard.Name(ResourceObjectStorageSize, resource.BinarySI) + used := resourceQuota.Status.Used.Name(ResourceObjectStorageSize, resource.BinarySI) updated := r.initObjectStorageUser(user, username, quota.Value()) @@ -201,6 +203,14 @@ func (r *ObjectStorageUserReconciler) Reconcile(ctx context.Context, req ctrl.Re updated = true } + if used.Value() != size { + resourceQuota.Status.Used[ResourceObjectStorageSize] = resource.MustParse(strconv.FormatInt(size, 10)) + if err := r.Status().Update(ctx, resourceQuota); err != nil { + r.Logger.Error(err, "failed to update status", "name", resourceQuota.Name, "namespace", userNamespace) + return ctrl.Result{}, err + } + } + if user.Status.ObjectsCount != objectsCount { user.Status.ObjectsCount = objectsCount updated = true From d21832075623248f831df5f04b7a8962c3c83fb6 Mon Sep 17 00:00:00 2001 From: Jiahui <4543bxy@gmail.com> Date: Fri, 27 Sep 2024 14:51:17 +0800 Subject: [PATCH 08/63] fix scripts: init account jwt secret (#5117) --- deploy/cloud/scripts/init.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/cloud/scripts/init.sh b/deploy/cloud/scripts/init.sh index 1787b1a498c..70f9c07e0a3 100644 --- a/deploy/cloud/scripts/init.sh +++ b/deploy/cloud/scripts/init.sh @@ -243,7 +243,8 @@ function sealos_run_controller { --env DEFAULT_NAMESPACE="account-system" \ --env GLOBAL_COCKROACH_URI="$cockroachdbGlobalUri" \ --env LOCAL_COCKROACH_URI="$cockroachdbLocalUri" \ - --env LOCAL_REGION="$localRegionUID" + --env LOCAL_REGION="$localRegionUID" \ + --env ACCOUNT_API_JWT_SECRET="$jwtInternal" sealos run tars/account-service.tar --env cloudDomain="$cloudDomain" --env cloudPort="$cloudPort" From ebe7f51afe023810fc4e3538f4f2e46007d43002 Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Sat, 28 Sep 2024 12:01:32 +0800 Subject: [PATCH 09/63] feat: launchpad support secret userDomains (#5119) * feat: support secret userDomains * update deploy --- frontend/providers/applaunchpad/.env.template | 9 +-------- frontend/providers/applaunchpad/data/config.yaml | 8 +++++--- frontend/providers/applaunchpad/deploy/Kubefile | 3 ++- .../applaunchpad/deploy/manifests/deploy.yaml.tmpl | 7 ++++++- .../providers/applaunchpad/src/api/platform.ts | 1 - .../src/pages/api/platform/getInitData.ts | 14 ++++++++------ .../src/pages/api/v1alpha/createApp.ts | 5 +++++ .../app/edit/components/CustomAccessModal.tsx | 4 ++-- .../src/pages/app/edit/components/Form.tsx | 2 +- .../providers/applaunchpad/src/store/static.ts | 12 ++++-------- frontend/providers/applaunchpad/src/types/app.d.ts | 4 ++-- .../providers/applaunchpad/src/types/index.d.ts | 11 +++++++---- frontend/providers/applaunchpad/src/utils/adapt.ts | 4 ++-- .../applaunchpad/src/utils/deployYaml2Json.ts | 7 +++++-- 14 files changed, 50 insertions(+), 41 deletions(-) diff --git a/frontend/providers/applaunchpad/.env.template b/frontend/providers/applaunchpad/.env.template index a27352112d3..3285ba37b23 100644 --- a/frontend/providers/applaunchpad/.env.template +++ b/frontend/providers/applaunchpad/.env.template @@ -1,8 +1 @@ -NEXT_PUBLIC_MOCK_USER= -SEALOS_DOMAIN="cloud.sealos.io" -DOMAIN_PORT= -FASTGPT_KEY= -CURRENCY= -MONITOR_URL= -INGRESS_SECRET= -GUIDE_ENABLED= \ No newline at end of file +NEXT_PUBLIC_MOCK_USER= \ No newline at end of file diff --git a/frontend/providers/applaunchpad/data/config.yaml b/frontend/providers/applaunchpad/data/config.yaml index 13db2236905..f7ab8b485c6 100644 --- a/frontend/providers/applaunchpad/data/config.yaml +++ b/frontend/providers/applaunchpad/data/config.yaml @@ -2,19 +2,21 @@ cloud: domain: 127.0.0.1.nip.io desktopDomain: 127.0.0.1.nip.io port: "" - userDomain: - - 127.0.0.1.nip.io + userDomains: + - name: 127.0.0.1.nip.io + secretName: wildcard-cert common: guideEnabled: false apiEnabled: false launchpad: - ingressTlsSecretName: wildcard-cert eventAnalyze: enabled: false fastGPTKey: "" components: monitor: url: http://launchpad-monitor.sealos.svc.cluster.local:8428 + billing: + url: "http://account-service.account-system.svc:2333" appResourceFormSliderConfig: default: cpu: [100, 200, 500, 1000, 2000, 3000, 4000, 8000] diff --git a/frontend/providers/applaunchpad/deploy/Kubefile b/frontend/providers/applaunchpad/deploy/Kubefile index e052ade7ab7..bc58eb95ca7 100644 --- a/frontend/providers/applaunchpad/deploy/Kubefile +++ b/frontend/providers/applaunchpad/deploy/Kubefile @@ -9,7 +9,8 @@ ENV cloudDomain="127.0.0.1.nip.io" ENV cloudPort="" ENV certSecretName="wildcard-cert" -ENV ingressTlsSecretName="wildcard-cert" + ENV monitorUrl="http://launchpad-monitor.sealos.svc.cluster.local:8428" +ENV billingUrl="http://account-service.account-system.svc:2333" CMD ["kubectl apply -f manifests"] diff --git a/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl index 78793ae6eb3..4df46f43427 100644 --- a/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl @@ -15,17 +15,22 @@ data: cloud: domain: {{ .cloudDomain }} port: {{ if .cloudPort }}:{{ .cloudPort }}{{ end }} + desktopDomain: {{ .cloudDomain }} + userDomains: + - name: {{ .cloudDomain }} + secretName: {{ .certSecretName }} common: guideEnabled: false apiEnabled: false launchpad: - ingressTlsSecretName: {{ .ingressTlsSecretName }} eventAnalyze: enabled: false fastGPTKey: "" components: monitor: url: {{ .monitorUrl }} + billing: + url: {{ .billingUrl }} appResourceFormSliderConfig: default: cpu: [100, 200, 500, 1000, 2000, 3000, 4000, 8000] diff --git a/frontend/providers/applaunchpad/src/api/platform.ts b/frontend/providers/applaunchpad/src/api/platform.ts index e8fe277b932..c64afb13d50 100644 --- a/frontend/providers/applaunchpad/src/api/platform.ts +++ b/frontend/providers/applaunchpad/src/api/platform.ts @@ -1,6 +1,5 @@ import type { Response as InitDataType } from '@/pages/api/platform/getInitData'; import { GET, POST } from '@/services/request'; -import { EnvResponse } from '@/types'; import type { AccountCRD, UserQuotaItemType, userPriceType } from '@/types/user'; import { AuthCnamePrams } from './params'; import { UpdateUserGuideParams } from '@/pages/api/guide/updateGuide'; diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts index f48dcbffff6..b3d6be33b70 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts @@ -10,13 +10,12 @@ import { getGpuNode } from './resourcePrice'; export type Response = { SEALOS_DOMAIN: string; DOMAIN_PORT: string; - INGRESS_SECRET: string; SHOW_EVENT_ANALYZE: boolean; FORM_SLIDER_LIST_CONFIG: FormSliderListType; CURRENCY: Coin; guideEnabled: boolean; fileMangerConfig: FileMangerType; - SEALOS_USER_DOMAIN: string[]; + SEALOS_USER_DOMAINS: { name: string; secretName: string }[]; DESKTOP_DOMAIN: string; }; @@ -24,7 +23,12 @@ export const defaultAppConfig: AppConfigType = { cloud: { domain: 'cloud.sealos.io', port: '', - userDomain: ['cloud.sealos.io'], + userDomains: [ + { + name: 'cloud.sealos.io', + secretName: 'wildcard-cert' + } + ], desktopDomain: 'cloud.sealos.io' }, common: { @@ -33,7 +37,6 @@ export const defaultAppConfig: AppConfigType = { gpuEnabled: false }, launchpad: { - ingressTlsSecretName: 'wildcard-cert', eventAnalyze: { enabled: false, fastGPTKey: '' @@ -83,13 +86,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) data: { SEALOS_DOMAIN: global.AppConfig.cloud.domain, DOMAIN_PORT: global.AppConfig.cloud.port?.toString() || '', - INGRESS_SECRET: global.AppConfig.launchpad.ingressTlsSecretName, SHOW_EVENT_ANALYZE: global.AppConfig.launchpad.eventAnalyze.enabled, FORM_SLIDER_LIST_CONFIG: global.AppConfig.launchpad.appResourceFormSliderConfig, guideEnabled: global.AppConfig.common.guideEnabled, fileMangerConfig: global.AppConfig.launchpad.fileManger, CURRENCY: Coin.shellCoin, - SEALOS_USER_DOMAIN: global.AppConfig.cloud.userDomain || [], + SEALOS_USER_DOMAINS: global.AppConfig.cloud.userDomains || [], DESKTOP_DOMAIN: global.AppConfig.cloud.desktopDomain } }); diff --git a/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts b/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts index dd6ff058b61..39e612a01e1 100644 --- a/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts +++ b/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts @@ -17,6 +17,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< kubeconfig: await authSession(req.headers) }); + appForm.networks = appForm.networks.map((network) => ({ + ...network, + domain: global.AppConfig.cloud.domain + })); + const parseYamls = formData2Yamls(appForm); const yamls = parseYamls.map((item) => item.value); diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/CustomAccessModal.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/CustomAccessModal.tsx index 66ff4e18c4d..86dfeafb451 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/CustomAccessModal.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/CustomAccessModal.tsx @@ -19,7 +19,7 @@ import { Tip } from '@sealos/ui'; import { InfoOutlineIcon } from '@chakra-ui/icons'; import { useRequest } from '@/hooks/useRequest'; import { postAuthCname } from '@/api/platform'; -import { SEALOS_USER_DOMAIN } from '@/store/static'; +import { SEALOS_USER_DOMAINS } from '@/store/static'; export type CustomAccessModalParams = { publicDomain: string; @@ -46,7 +46,7 @@ const CustomAccessModal = ({ const completePublicDomain = useMemo(() => `${publicDomain}.${domain}`, [publicDomain, domain]); const cnameTips = useMemo(() => { - return SEALOS_USER_DOMAIN.map((item) => `${publicDomain}.${item}`).join(` ${t('or')} `); + return SEALOS_USER_DOMAINS.map((item) => `${publicDomain}.${item.name}`).join(` ${t('or')} `); }, [publicDomain, t]); const { mutate: authCNAME, isLoading } = useRequest({ 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 ecf65c89678..e7480bd72c1 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/Form.tsx @@ -5,7 +5,7 @@ import { defaultSliderKey, ProtocolList } from '@/constants/app'; import { GpuAmountMarkList } from '@/constants/editApp'; import { useToast } from '@/hooks/useToast'; import { useGlobalStore } from '@/store/global'; -import { SEALOS_DOMAIN, SEALOS_USER_DOMAIN } from '@/store/static'; +import { SEALOS_DOMAIN } from '@/store/static'; import { useUserStore } from '@/store/user'; import type { QueryType } from '@/types'; import type { AppEditType } from '@/types/app'; diff --git a/frontend/providers/applaunchpad/src/store/static.ts b/frontend/providers/applaunchpad/src/store/static.ts index 87a94ba236a..10c8bd2a79e 100644 --- a/frontend/providers/applaunchpad/src/store/static.ts +++ b/frontend/providers/applaunchpad/src/store/static.ts @@ -2,10 +2,9 @@ import { getInitData } from '@/api/platform'; import { Coin } from '@/constants/app'; export let SEALOS_DOMAIN = 'cloud.sealos.io'; -export let SEALOS_USER_DOMAIN = ['cloud.sealos.io']; +export let SEALOS_USER_DOMAINS = [{ name: 'cloud.sealos.io', secretName: 'wildcard-cert' }]; export let DESKTOP_DOMAIN = 'cloud.sealos.io'; export let DOMAIN_PORT = ''; -export let INGRESS_SECRET = 'wildcard-cert'; export let SHOW_EVENT_ANALYZE = false; export let CURRENCY = Coin.shellCoin; export let UPLOAD_LIMIT = 50; @@ -15,9 +14,8 @@ export const loadInitData = async () => { try { const res = await getInitData(); SEALOS_DOMAIN = res.SEALOS_DOMAIN; - SEALOS_USER_DOMAIN = res.SEALOS_USER_DOMAIN; + SEALOS_USER_DOMAINS = res.SEALOS_USER_DOMAINS; DOMAIN_PORT = res.DOMAIN_PORT; - INGRESS_SECRET = res.INGRESS_SECRET; SHOW_EVENT_ANALYZE = res.SHOW_EVENT_ANALYZE; CURRENCY = res.CURRENCY; UPLOAD_LIMIT = res.fileMangerConfig.uploadLimit; @@ -27,7 +25,6 @@ export const loadInitData = async () => { return { SEALOS_DOMAIN, DOMAIN_PORT, - INGRESS_SECRET, CURRENCY, FORM_SLIDER_LIST_CONFIG: res.FORM_SLIDER_LIST_CONFIG, DESKTOP_DOMAIN: res.DESKTOP_DOMAIN @@ -35,8 +32,7 @@ export const loadInitData = async () => { } catch (error) {} return { - SEALOS_DOMAIN, - INGRESS_SECRET + SEALOS_DOMAIN }; }; @@ -45,7 +41,7 @@ export const serverLoadInitData = () => { try { SEALOS_DOMAIN = global.AppConfig.cloud.domain || 'cloud.sealos.io'; DOMAIN_PORT = global.AppConfig.cloud.port || ''; - INGRESS_SECRET = global.AppConfig.launchpad.ingressTlsSecretName || 'wildcard-cert'; SHOW_EVENT_ANALYZE = global.AppConfig.launchpad.eventAnalyze.enabled; + SEALOS_USER_DOMAINS = global.AppConfig.cloud.userDomains; } catch (error) {} }; diff --git a/frontend/providers/applaunchpad/src/types/app.d.ts b/frontend/providers/applaunchpad/src/types/app.d.ts index 952ea06bcb9..69db7c38f67 100644 --- a/frontend/providers/applaunchpad/src/types/app.d.ts +++ b/frontend/providers/applaunchpad/src/types/app.d.ts @@ -76,9 +76,9 @@ export interface AppEditType { port: number; protocol: ProtocolType; openPublicDomain: boolean; - publicDomain: string; // default domain // domainPrefix + publicDomain: string; // domainPrefix customDomain: string; // custom domain - domain: string; + domain: string; // Main promoted domain }[]; envs: { key: string; diff --git a/frontend/providers/applaunchpad/src/types/index.d.ts b/frontend/providers/applaunchpad/src/types/index.d.ts index 7f0a91ef042..8caee522f67 100644 --- a/frontend/providers/applaunchpad/src/types/index.d.ts +++ b/frontend/providers/applaunchpad/src/types/index.d.ts @@ -25,10 +25,14 @@ export type FileMangerType = { export type AppConfigType = { cloud: { - domain: string; + domain: string; // Main promoted domain port?: string; - userDomain: string[]; - desktopDomain: string; + // List of domains available for users + userDomains: { + name: string; + secretName: string; + }[]; + desktopDomain: string; // Domain for the desktop application }; common: { guideEnabled: boolean; @@ -36,7 +40,6 @@ export type AppConfigType = { gpuEnabled: boolean; }; launchpad: { - ingressTlsSecretName: string; eventAnalyze: { enabled: boolean; fastGPTKey?: string; diff --git a/frontend/providers/applaunchpad/src/utils/adapt.ts b/frontend/providers/applaunchpad/src/utils/adapt.ts index c63fcc077de..0c05b3787f0 100644 --- a/frontend/providers/applaunchpad/src/utils/adapt.ts +++ b/frontend/providers/applaunchpad/src/utils/adapt.ts @@ -219,7 +219,7 @@ export enum YamlKindEnum { } export const adaptAppDetail = async (configs: DeployKindsType[]): Promise => { - const { SEALOS_DOMAIN, SEALOS_USER_DOMAIN } = await getInitData(); + const { SEALOS_DOMAIN, SEALOS_USER_DOMAINS } = await getInitData(); const deployKindsMap: { [YamlKindEnum.StatefulSet]?: V1StatefulSet; [YamlKindEnum.Deployment]?: V1Deployment; @@ -314,7 +314,7 @@ export const adaptAppDetail = async (configs: DeployKindsType[]): Promise domain.endsWith(user)); + !SEALOS_USER_DOMAINS.some((item) => domain.endsWith(item.name)); return { networkName: ingress?.metadata?.name || '', diff --git a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts index 1dcd33e1eda..1f10a7a932e 100644 --- a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts +++ b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts @@ -7,7 +7,7 @@ import { minReplicasKey, publicDomainKey } from '@/constants/app'; -import { INGRESS_SECRET } from '@/store/static'; +import { SEALOS_USER_DOMAINS } from '@/store/static'; import type { AppEditType } from '@/types/app'; import { pathFormat, pathToNameFormat, str2Num, strToBase64 } from '@/utils/tools'; import dayjs from 'dayjs'; @@ -274,7 +274,10 @@ export const json2Ingress = (data: AppEditType) => { ? network.customDomain : `${network.publicDomain}.${network.domain}`; - const secretName = network.customDomain ? network.networkName : INGRESS_SECRET; + const secretName = network.customDomain + ? network.networkName + : SEALOS_USER_DOMAINS.find((domain) => domain.name === network.domain)?.secretName || + 'wildcard-cert'; const ingress = { apiVersion: 'networking.k8s.io/v1', From d3ba17bc4925707a32ae999e88019aaf3af23800 Mon Sep 17 00:00:00 2001 From: zijiren <84728412+zijiren233@users.noreply.github.com> Date: Sat, 28 Sep 2024 12:42:12 +0800 Subject: [PATCH 10/63] fix: node tls reject unauthorized (#5116) * fix: node tls reject unauthorized * fix: account use svc --- .../devbox/app/api/platform/resourcePrice/route.ts | 7 +++++-- .../devbox/deploy/manifests/deploy.yaml.tmpl | 2 ++ .../template/deploy/manifests/deploy.yaml.tmpl | 2 ++ .../src/pages/api/platform/resourcePrice.ts | 13 +++++++------ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts index c921e88f894..6f14b6e4076 100644 --- a/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts +++ b/frontend/providers/devbox/app/api/platform/resourcePrice/route.ts @@ -44,11 +44,14 @@ const valuationMap: Record = { export async function GET(req: NextRequest) { try { - const { SEALOS_DOMAIN } = process.env + const { ACCOUNT_URL, SEALOS_DOMAIN } = process.env + const baseUrl = ACCOUNT_URL + ? ACCOUNT_URL + : `https://account-api.${SEALOS_DOMAIN}`; const getResourcePrice = async () => { try { const res = await fetch( - `https://account-api.${SEALOS_DOMAIN}/account/v1alpha1/properties`, + `${baseUrl}/account/v1alpha1/properties`, { method: 'POST' } diff --git a/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl index 7a1c0e08e68..6514f1538f2 100644 --- a/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -49,6 +49,8 @@ spec: value: http://launchpad-monitor.sealos.svc.cluster.local:8428 - name: SQUASH_ENABLE value: 'true' + - name: ACCOUNT_URL + value: http://account-service.account-system.svc.cluster.local:2333 securityContext: runAsNonRoot: true runAsUser: 1001 diff --git a/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl index 05621e26e09..743acd1c984 100644 --- a/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/template/deploy/manifests/deploy.yaml.tmpl @@ -80,6 +80,8 @@ spec: value: {{ .templateRepoBranch }} - name: SHOW_AUTHOR value: "false" + - name: ACCOUNT_URL + value: http://account-service.account-system.svc.cluster.local:2333 image: ghcr.io/labring/sealos-template-frontend:latest imagePullPolicy: Always volumeMounts: diff --git a/frontend/providers/template/src/pages/api/platform/resourcePrice.ts b/frontend/providers/template/src/pages/api/platform/resourcePrice.ts index d6efb4fe826..85b26b377a8 100644 --- a/frontend/providers/template/src/pages/api/platform/resourcePrice.ts +++ b/frontend/providers/template/src/pages/api/platform/resourcePrice.ts @@ -44,12 +44,13 @@ export function transformProperties(data: properties): userPriceType { } const getResourcePrice = async () => { - const res = await fetch( - `https://account-api.${process.env.SEALOS_CLOUD_DOMAIN}/account/v1alpha1/properties`, - { - method: 'POST' - } - ); + const baseUrl = process.env.ACCOUNT_URL + ? process.env.ACCOUNT_URL + : `https://account-api.${process.env.SEALOS_CLOUD_DOMAIN}`; + + const res = await fetch(`${baseUrl}/account/v1alpha1/properties`, { + method: 'POST' + }); const data = await res.json(); return transformProperties(data.data as properties); }; From 22d9a138f9a3f4cd04a4eb927639fd008972adff Mon Sep 17 00:00:00 2001 From: yy <56745951+lingdie@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:15:23 +0800 Subject: [PATCH 11/63] fix devbox phase generate. (#5120) --- controllers/devbox/.gitignore | 3 ++ .../devbox/api/v1alpha1/devbox_types.go | 6 ++-- .../crd/bases/devbox.sealos.io_devboxes.yaml | 4 --- .../devbox/deploy/manifests/deploy.yaml.tmpl | 6 +--- .../internal/controller/devbox_controller.go | 35 +++++++------------ .../internal/controller/helper/devbox.go | 27 +++++++++++++- 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/controllers/devbox/.gitignore b/controllers/devbox/.gitignore index ada68ff086c..fa6acefb0a0 100644 --- a/controllers/devbox/.gitignore +++ b/controllers/devbox/.gitignore @@ -25,3 +25,6 @@ go.work *.swp *.swo *~ + +# ignore deploy.yaml +deploy/manifests/deploy.yaml diff --git a/controllers/devbox/api/v1alpha1/devbox_types.go b/controllers/devbox/api/v1alpha1/devbox_types.go index facdbe59f9b..5c82533e142 100644 --- a/controllers/devbox/api/v1alpha1/devbox_types.go +++ b/controllers/devbox/api/v1alpha1/devbox_types.go @@ -163,16 +163,16 @@ const ( DevboxPhasePending DevboxPhase = "Pending" //DevboxPhaseStopped means Devbox is stop and stopped success DevboxPhaseStopped DevboxPhase = "Stopped" - //DevboxPhaseStopping means Devbox is stop and not stopped success + //DevboxPhaseStopping means Devbox is stopping DevboxPhaseStopping DevboxPhase = "Stopping" //DevboxPhaseError means Devbox is error DevboxPhaseError DevboxPhase = "Error" + //DevboxPhaseUnknown means Devbox is unknown + DevboxPhaseUnknown DevboxPhase = "Unknown" ) // DevboxStatus defines the observed state of Devbox type DevboxStatus struct { - // +kubebuilder:validation:Optional - DevboxPodPhase corev1.PodPhase `json:"podPhase"` // +kubebuilder:validation:Optional Network NetworkStatus `json:"network"` // +kubebuilder:validation:Optional diff --git a/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml b/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml index 7c585d60f50..eaf11b9e577 100644 --- a/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml +++ b/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml @@ -2848,10 +2848,6 @@ spec: type: object phase: type: string - podPhase: - description: PodPhase is a label for the condition of a pod at the - current time. - type: string state: description: |- ContainerState holds a possible state of container. diff --git a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl index 753ca75c462..92d3b7c6a9d 100644 --- a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -2856,10 +2856,6 @@ spec: type: object phase: type: string - podPhase: - description: PodPhase is a label for the condition of a pod at the - current time. - type: string state: description: |- ContainerState holds a possible state of container. @@ -5639,7 +5635,7 @@ metadata: name: devbox-controller-manager namespace: devbox-system spec: - replicas: 1 + replicas: 2 selector: matchLabels: control-plane: controller-manager diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index a9e15005f92..a74fef4893e 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -202,6 +202,17 @@ func (r *DevboxReconciler) syncSecret(ctx context.Context, devbox *devboxv1alpha func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error { logger := log.FromContext(ctx) + + var podList corev1.PodList + if err := r.List(ctx, &podList, client.InNamespace(devbox.Namespace), client.MatchingLabels(recLabels)); err != nil { + return err + } + // only one pod is allowed, if more than one pod found, return error + if len(podList.Items) > 1 { + return fmt.Errorf("more than one pod found") + } + logger.Info("pod list", "length", len(podList.Items)) + // update devbox status after pod is created or updated defer func() { if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { @@ -214,6 +225,7 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D // update devbox status with latestDevbox status logger.Info("updating devbox status") logger.Info("merge commit history", "devbox", devbox.Status.CommitHistory, "latestDevbox", latestDevbox.Status.CommitHistory) + devbox.Status.Phase = helper.GenerateDevboxPhase(devbox, podList) helper.UpdateDevboxStatus(devbox, latestDevbox) return r.Status().Update(ctx, latestDevbox) }); err != nil { @@ -225,17 +237,6 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync pod success", "Sync pod success") }() - var podList corev1.PodList - if err := r.List(ctx, &podList, client.InNamespace(devbox.Namespace), client.MatchingLabels(recLabels)); err != nil { - return err - } - // only one pod is allowed, if more than one pod found, return error - if len(podList.Items) > 1 { - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseError - return fmt.Errorf("more than one pod found") - } - logger.Info("pod list", "length", len(podList.Items)) - switch devbox.Spec.State { case devboxv1alpha1.DevboxStateRunning: runtimecr, err := r.getRuntime(ctx, devbox) @@ -249,14 +250,11 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D case 0: logger.Info("create pod") logger.Info("next commit history", "commit", nextCommitHistory) - devbox.Status.Phase = devboxv1alpha1.DevboxPhasePending return r.createPod(ctx, devbox, expectPod, nextCommitHistory) case 1: pod := &podList.Items[0] - devbox.Status.DevboxPodPhase = pod.Status.Phase // check pod container size, if it is 0, it means the pod is not running, return an error if len(pod.Status.ContainerStatuses) == 0 { - devbox.Status.Phase = devboxv1alpha1.DevboxPhasePending return fmt.Errorf("pod container size is 0") } devbox.Status.State = pod.Status.ContainerStatuses[0].State @@ -275,32 +273,26 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D case corev1.PodPending, corev1.PodRunning: // pod is running or pending, do nothing here logger.Info("pod is running or pending") - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseRunning // update commit history status by pod status helper.UpdateCommitHistory(devbox, pod, false) return nil case corev1.PodFailed, corev1.PodSucceeded: // pod failed or succeeded, we need delete pod and remove finalizer - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseStopped logger.Info("pod failed or succeeded, recreate pod") return r.deletePod(ctx, devbox, pod) } case false: // pod not match expectations, delete pod anyway logger.Info("pod not match expectations, recreate pod") - devbox.Status.Phase = devboxv1alpha1.DevboxPhasePending return r.deletePod(ctx, devbox, pod) } } case devboxv1alpha1.DevboxStateStopped: switch len(podList.Items) { case 0: - // update devbox status to stopped, no pod found, do nothing - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseStopped return nil case 1: pod := &podList.Items[0] - devbox.Status.DevboxPodPhase = pod.Status.Phase // update state to empty since devbox is stopped devbox.Status.State = corev1.ContainerState{} // update commit predicated status by pod status, this should be done once find a pod @@ -309,7 +301,6 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D if !pod.DeletionTimestamp.IsZero() { return r.handlePodDeleted(ctx, devbox, pod) } - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseStopped // we need delete pod because devbox state is stopped // we don't care about the pod status, just delete it return r.deletePod(ctx, devbox, pod) @@ -422,7 +413,6 @@ func (r *DevboxReconciler) createPod(ctx context.Context, devbox *devboxv1alpha1 nextCommitHistory.PredicatedStatus = devboxv1alpha1.CommitStatusPending if err := r.Create(ctx, expectPod); err != nil { logger.Error(err, "create pod failed") - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseError return err } devbox.Status.CommitHistory = append(devbox.Status.CommitHistory, nextCommitHistory) @@ -439,7 +429,6 @@ func (r *DevboxReconciler) deletePod(ctx context.Context, devbox *devboxv1alpha1 } if err := r.Delete(ctx, pod); err != nil { logger.Error(err, "delete pod failed") - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseError return err } // update commit history status because pod has been deleted diff --git a/controllers/devbox/internal/controller/helper/devbox.go b/controllers/devbox/internal/controller/helper/devbox.go index 055763ac1cf..7ce9ac497f5 100644 --- a/controllers/devbox/internal/controller/helper/devbox.go +++ b/controllers/devbox/internal/controller/helper/devbox.go @@ -76,6 +76,32 @@ func GeneratePodAnnotations(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alph return annotations } +func GenerateDevboxPhase(devbox *devboxv1alpha1.Devbox, podList corev1.PodList) devboxv1alpha1.DevboxPhase { + if len(podList.Items) > 1 { + return devboxv1alpha1.DevboxPhaseError + } + switch devbox.Spec.State { + case devboxv1alpha1.DevboxStateRunning: + if len(podList.Items) == 0 { + return devboxv1alpha1.DevboxPhasePending + } + switch podList.Items[0].Status.Phase { + case corev1.PodFailed, corev1.PodSucceeded: + return devboxv1alpha1.DevboxPhaseStopped + case corev1.PodPending: + return devboxv1alpha1.DevboxPhasePending + case corev1.PodRunning: + return devboxv1alpha1.DevboxPhaseRunning + } + case devboxv1alpha1.DevboxStateStopped: + if len(podList.Items) == 0 { + return devboxv1alpha1.DevboxPhaseStopped + } + return devboxv1alpha1.DevboxPhaseStopping + } + return devboxv1alpha1.DevboxPhaseUnknown +} + func MergeCommitHistory(devbox *devboxv1alpha1.Devbox, latestDevbox *devboxv1alpha1.Devbox) []*devboxv1alpha1.CommitHistory { res := make([]*devboxv1alpha1.CommitHistory, 0) historyMap := make(map[string]*devboxv1alpha1.CommitHistory) @@ -127,7 +153,6 @@ func UpdatePredicatedCommitStatus(devbox *devboxv1alpha1.Devbox, pod *corev1.Pod // TODO: move this function to devbox types.go func UpdateDevboxStatus(current, latest *devboxv1alpha1.Devbox) { latest.Status.Phase = current.Status.Phase - latest.Status.DevboxPodPhase = current.Status.DevboxPodPhase latest.Status.State = current.Status.State latest.Status.LastTerminationState = current.Status.LastTerminationState latest.Status.CommitHistory = MergeCommitHistory(current, latest) From 56ffcb829d61b2d5531fe4bb544e17bbe12032ba Mon Sep 17 00:00:00 2001 From: sealos-release-robot Date: Sun, 29 Sep 2024 13:13:37 +0800 Subject: [PATCH 12/63] =?UTF-8?q?=F0=9F=A4=96=20add=20release=20changelog?= =?UTF-8?q?=20using=20rebot.=20(#5123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-5.0.1-beta2.md | 32 ++++++++++++++++++++++++++++++ CHANGELOG/CHANGELOG.md | 1 + 2 files changed, 33 insertions(+) create mode 100644 CHANGELOG/CHANGELOG-5.0.1-beta2.md diff --git a/CHANGELOG/CHANGELOG-5.0.1-beta2.md b/CHANGELOG/CHANGELOG-5.0.1-beta2.md new file mode 100644 index 00000000000..1864a514d6d --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.1-beta2.md @@ -0,0 +1,32 @@ +Welcome to the v5.0.1-beta2 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* ebe7f51afe023810fc4e3538f4f2e46007d43002: feat: launchpad support secret userDomains (#5119) (@zjy365) +### Bug fixes +* 2fd2f07892660ce4fa0ad0e167082fbeb372d579: fix: cronjob arm64 image (#5113) (@zijiren233) +* d3ba17bc4925707a32ae999e88019aaf3af23800: fix: node tls reject unauthorized (#5116) (@zijiren233) +* bc0b5f72006a173f9bcac0e287fd9893efe728af: fix: some ui adjust and bug fix (#5097) (@mlhiter) +### Other work +* 8c0c34592369874ce178c956af713dba3c17a9ea: devobx ignore extra ports. (#5112) (@lingdie) +* 1243fd31e8f2f8828e35f1c47e6e6cd19155ae3e: fix desktop and costcenter configs (#5114) (@xudaotutou) +* 22d9a138f9a3f4cd04a4eb927639fd008972adff: fix devbox phase generate. (#5120) (@lingdie) +* b6053c1c2601fafa0752cdaac4501606bd020a20: fix get default property (#5108) (@bxy4543) +* 7b025ae80d215cd1fe860ed08bc69dc132185be3: fix init user (#5104) (@bxy4543) +* d21832075623248f831df5f04b7a8962c3c83fb6: fix scripts: init account jwt secret (#5117) (@bxy4543) +* 3081c5dc1abf65d751de29b52b1febeb3dbbad9a: fix(account-service): fix concurrency issue and add real name info api (#5103) (@HUAHUAI23) +* 7b0be9352fa422f1abca8f232cd8c864db548f7e: release: sealos v5.0.1 beta2 (#5110) (@lingdie) +* 7a4b7b91fe63b3b4747fc99d2753c1b7a409fd39: sealos v5.0.1-beta2 (#5071) (@lingdie) +* 54a3bc28684df2f8427c2ff1cc3969a0dd79c534: style(costcenter): recharge discount (#5111) (@xudaotutou) +* b78691a56704884d1da0544a2779b8f42b45d3f1: sync resourcequota objectstorage/size status used (#5102) (@nowinkeyy) +* c80c265fd6838bbf071cefcfa077a4e4d0274fa4: 🤖 add release changelog using rebot. (#5100) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.1-beta1...v5.0.1-beta2 + +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 eb182bc5957..8460ab33b5d 100644 --- a/CHANGELOG/CHANGELOG.md +++ b/CHANGELOG/CHANGELOG.md @@ -2,6 +2,7 @@ All notable changes to this project will be documented in this file. +- [CHANGELOG-5.0.1-beta2.md](./CHANGELOG-5.0.1-beta2.md) - [CHANGELOG-5.0.1-beta1.md](./CHANGELOG-5.0.1-beta1.md) - [CHANGELOG-5.0.0-beta5.md](./CHANGELOG-5.0.0-beta5.md) - [CHANGELOG-5.0.0-beta4.md](./CHANGELOG-5.0.0-beta4.md) From a9154f858db2b50ce72e1e0a6fc8b15a954d37a0 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Tue, 8 Oct 2024 10:20:41 +0800 Subject: [PATCH 13/63] fix invaild (#5118) --- .../desktop/public/locales/en/common.json | 8 +- .../desktop/public/locales/zh/common.json | 8 +- .../account/AccountCenter/AuthModifyList.tsx | 15 +- .../account/AccountCenter/index.tsx | 111 +++---- .../src/components/signin/auth/AuthList.tsx | 296 ++++++++++-------- .../components/signin/auth/useProtocol.tsx | 3 +- .../desktop/src/components/signin/index.tsx | 138 ++++---- .../src/pages/api/auth/email/bind/verify.ts | 12 +- .../api/auth/email/changeBinding/newSms.ts | 12 +- .../api/auth/email/changeBinding/oldSms.ts | 11 +- .../api/auth/email/changeBinding/verifyNew.ts | 16 +- .../api/auth/email/changeBinding/verifyOld.ts | 10 +- .../src/pages/api/auth/email/unbind/sms.ts | 10 +- .../src/pages/api/auth/email/unbind/verify.ts | 10 +- frontend/desktop/src/pages/api/auth/info.ts | 44 ++- .../src/pages/api/auth/phone/bind/sms.ts | 8 +- .../src/pages/api/auth/phone/bind/verify.ts | 11 +- .../api/auth/phone/changeBinding/newSms.ts | 10 +- .../api/auth/phone/changeBinding/oldSms.ts | 10 +- .../api/auth/phone/changeBinding/verifyNew.ts | 12 +- .../api/auth/phone/changeBinding/verifyOld.ts | 10 +- .../desktop/src/pages/api/auth/phone/sms.ts | 8 +- .../src/pages/api/auth/phone/unbind/sms.ts | 10 +- .../src/pages/api/auth/phone/unbind/verify.ts | 8 +- .../src/pages/api/auth/phone/verify.ts | 8 +- .../src/pages/api/platform/getAuthConfig.ts | 13 +- frontend/desktop/src/services/enable.ts | 3 +- frontend/desktop/src/types/system.ts | 36 ++- 28 files changed, 485 insertions(+), 366 deletions(-) diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 694d80f1265..7b075e654e6 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -108,6 +108,10 @@ "log_in": "Log In", "log_out": "Log Out", "login_to_your_account": "Login to your account", + "login_with_github": "Login with Github", + "login_with_google": "Login with Google", + "login_with_oauth2": "login with OAuth2.0", + "login_with_wechat": "Login with Wechat", "manage_team": "Manage Workspace", "member_list": "Member List", "memory": "Memory", @@ -191,6 +195,7 @@ "remaining_time": "Remaining Time: ", "remove": "Remove", "remove_member_tips": "Determine that you want to remove the member?", + "rename": "Rename", "scan_with_wechat": "Scan with WeChat", "sealos_copilot": "Sealos Copilot", "sealos_document": "Sealos Document", @@ -233,6 +238,5 @@ "you_can_use_the_kubectl_command_directly_from_the_terminal": "You can use the kubectl command directly from the terminal", "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", "you_have_not_purchased_the_license": "You have not purchased the License", - "yuan": "Yuan", - "rename": "Rename" + "yuan": "Yuan" } diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index f1f81e20464..a99e42166be 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -104,6 +104,10 @@ "log_in": "登录", "log_out": "登出", "login_to_your_account": "登录您的帐户", + "login_with_github": "Github 登录", + "login_with_google": "Google 登录", + "login_with_oauth2": "OAuth2.0 登录", + "login_with_wechat": "Wechat 登录", "manage_team": "管理工作空间", "member_list": "成员列表", "memory": "内存", @@ -187,6 +191,7 @@ "remaining_time": "剩余激活时间: ", "remove": "移除", "remove_member_tips": "确认要移除该成员?", + "rename": "重命名", "scan_with_wechat": "微信扫码支付", "sealos_copilot": "Sealos 小助理", "search_apps": "搜索应用", @@ -226,6 +231,5 @@ "you_can_use_the_kubectl_command_directly_from_the_terminal": "您可通过终端直接使用 kubectl 命令", "you_can_view_fees_through_the_fee_center": "您可通过费用中心查看费用", "you_have_not_purchased_the_license": "您还没有购买 License", - "yuan": "元", - "rename": "重命名" + "yuan": "元" } diff --git a/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx index 53ba96f03dd..4d0d21927ad 100644 --- a/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx @@ -1,12 +1,12 @@ 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 { Center, Image, Text } 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'; +import { ConfigItem } from './ConfigItem'; export function AuthModifyList({ isOnlyOne, @@ -34,17 +34,19 @@ export function AuthModifyList({ ({ url, provider, - clientId + clientId, + proxyAddress }: { url: string; provider: OauthProvider; clientId: string; + proxyAddress?: string; }) => (action: T) => { const state = generateState(action); setProvider(provider); - if (conf.proxyAddress) { - const target = new URL(conf.proxyAddress); + if (proxyAddress) { + const target = new URL(proxyAddress); const callback = new URL(conf.callbackURL); target.searchParams.append( 'oauthProxyState', @@ -69,6 +71,7 @@ export function AuthModifyList({ return actionCbGen({ provider: 'GITHUB', clientId: githubConf.clientID, + proxyAddress: githubConf?.proxyAddress, url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user` })(action); } @@ -83,6 +86,7 @@ export function AuthModifyList({ return actionCbGen({ provider: 'WECHAT', clientId: wechatConf.clientID, + proxyAddress: wechatConf?.proxyAddress, url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&scope=snsapi_login&#wechat_redirect` })(action); } @@ -99,6 +103,7 @@ export function AuthModifyList({ return actionCbGen({ provider: 'GOOGLE', clientId: googleConf.clientID, + proxyAddress: googleConf?.proxyAddress, 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); } diff --git a/frontend/desktop/src/components/account/AccountCenter/index.tsx b/frontend/desktop/src/components/account/AccountCenter/index.tsx index fcc86cd3233..81ad850881c 100644 --- a/frontend/desktop/src/components/account/AccountCenter/index.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/index.tsx @@ -1,39 +1,39 @@ +import { UserInfo } from '@/api/auth'; +import PasswordModify from '@/components/account/AccountCenter/PasswordModify'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; +import { ValueOf } from '@/types'; import { + Badge, + Center, Flex, - Text, + HStack, + IconButton, + IconButtonProps, + Image, Modal, ModalBody, ModalCloseButton, ModalContent, - ModalOverlay, - useDisclosure, - IconButton, - IconButtonProps, ModalHeader, + ModalOverlay, Spinner, - Image, - HStack, - VStack, - Center, - Badge + Text, + useDisclosure, + VStack } from '@chakra-ui/react'; -import { useMemo, useState } from 'react'; +import { CloseIcon, LeftArrowIcon, SettingIcon } from '@sealos/ui'; import { useQuery } from '@tanstack/react-query'; -import useSessionStore from '@/stores/session'; import { useTranslation } from 'next-i18next'; -import { SettingIcon, LeftArrowIcon, CloseIcon } from '@sealos/ui'; -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 { PhoneChange, EmailChange } from './SmsModify/SmsChange'; -import { BindingModifyButton, BINDING_STATE_MODIFY_BEHAVIOR } from './BindingModifyButton'; -import { ConfigItem } from './ConfigItem'; +import { useMemo, useState } from 'react'; +import { RealNameAuthForm } from '../RealNameModal'; import { AuthModifyList } from './AuthModifyList'; +import { BINDING_STATE_MODIFY_BEHAVIOR, BindingModifyButton } from './BindingModifyButton'; +import { ConfigItem } from './ConfigItem'; import DeleteAccount from './DeleteAccountModal'; -import { ValueOf } from '@/types'; -import { RealNameAuthForm } from '../RealNameModal'; -import { useConfigStore } from '@/stores/config'; +import { EmailBind, PhoneBind } from './SmsModify/SmsBind'; +import { EmailChange, PhoneChange } from './SmsModify/SmsChange'; +import { EmailUnBind, PhoneUnBind } from './SmsModify/SmsUnbind'; enum _PageState { INDEX = 0 // WECHAT_BIND, @@ -68,6 +68,7 @@ const PageState = Object.assign( export default function Index(props: Omit) { const { commonConfig } = useConfigStore(); const { session } = useSessionStore((s) => s); + const conf = useConfigStore(); const { t } = useTranslation(); const logo = '/images/default-user.svg'; const { isOpen, onOpen, onClose } = useDisclosure(); @@ -236,7 +237,7 @@ export default function Index(props: Omit) { } /> )} - {providerState.PASSWORD.isBinding && ( + {conf.authConfig?.idp.password.enabled && providerState.PASSWORD.isBinding && ( {t('common:password')}} RightElement={ @@ -252,40 +253,42 @@ export default function Index(props: Omit) { } /> )} - {t('common:phone')}} - RightElement={ - <> - - {providerState.PHONE.isBinding - ? providerState.PHONE.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') - : t('common:unbound')} - - - { - providerState.PHONE.isBinding - ? setPageState(PageState.PHONE_CHANGE_BIND) - : setPageState(PageState.PHONE_BIND); - }} - /> - {providerState.PHONE.isBinding && providerState.total > 1 && ( + {conf.authConfig?.idp.sms.enabled && ( + {t('common:phone')}} + RightElement={ + <> + + {providerState.PHONE.isBinding + ? providerState.PHONE.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') + : t('common:unbound')} + + { - setPageState(PageState.PHONE_UNBIND); + providerState.PHONE.isBinding + ? setPageState(PageState.PHONE_CHANGE_BIND) + : setPageState(PageState.PHONE_BIND); }} /> - )} - - - } - /> + {providerState.PHONE.isBinding && providerState.total > 1 && ( + { + setPageState(PageState.PHONE_UNBIND); + }} + /> + )} + + + } + /> + )} {t('common:email')}} RightElement={ diff --git a/frontend/desktop/src/components/signin/auth/AuthList.tsx b/frontend/desktop/src/components/signin/auth/AuthList.tsx index 6f9b7edb746..d353af996da 100644 --- a/frontend/desktop/src/components/signin/auth/AuthList.tsx +++ b/frontend/desktop/src/components/signin/auth/AuthList.tsx @@ -1,152 +1,204 @@ import { useConfigStore } from '@/stores/config'; import useSessionStore from '@/stores/session'; import { OauthProvider } from '@/types/user'; -import { Button, Image, Flex, Icon, Center } from '@chakra-ui/react'; +import { Button, Center, Flex, FlexProps, Icon, Image, Text } from '@chakra-ui/react'; import { GithubIcon, GoogleIcon, WechatIcon } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { MouseEventHandler, useMemo } from 'react'; -const AuthList = () => { +const AuthList = ({ + zeroTab = false, + isAgreeCb, + ...props +}: { zeroTab?: boolean; isAgreeCb: () => boolean } & FlexProps) => { const conf = useConfigStore().authConfig; + const { t } = useTranslation(['common']); const router = useRouter(); const logo = useConfigStore().layoutConfig?.logo; const { generateState, setProvider } = useSessionStore(); - const authList: { icon: typeof Icon; cb: MouseEventHandler; need: boolean }[] = useMemo(() => { - if (!conf) return []; - const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => { - setProvider(provider); - window.location.href = url; - }; - const oauthProxyLogin = async ({ - state, - provider, - id - }: { - state: string; - provider: OauthProvider; - id: string; - }) => { - // if(!conf) return - setProvider(provider); - const target = new URL(conf.proxyAddress); - const callback = new URL(conf.callbackURL); - target.searchParams.append( - 'oauthProxyState', - encodeURIComponent(callback.toString()) + '_' + state - ); - target.searchParams.append('oauthProxyClientID', id); - target.searchParams.append('oauthProxyProvider', provider); - router.replace(target.toString()); - }; - return [ - { - icon: GithubIcon, - cb: (e) => { - e.preventDefault(); - const state = generateState(); - const githubConf = conf?.idp.github; - if (conf?.proxyAddress) - oauthProxyLogin({ - provider: 'GITHUB', - state, - id: githubConf?.clientID as string - }); - else - oauthLogin({ - provider: 'GITHUB', - url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user&state=${state}` - }); + const authList: { icon: typeof Icon; cb: MouseEventHandler; need: boolean; text: string }[] = + useMemo(() => { + if (!conf) return []; + const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => { + setProvider(provider); + window.location.href = url; + }; + const oauthProxyLogin = async ({ + state, + provider, + proxyAddress, + id + }: { + state: string; + proxyAddress: string; + provider: OauthProvider; + id: string; + }) => { + // if(!conf) return + setProvider(provider); + const target = new URL(proxyAddress); + const callback = new URL(conf.callbackURL); + target.searchParams.append( + 'oauthProxyState', + encodeURIComponent(callback.toString()) + '_' + state + ); + target.searchParams.append('oauthProxyClientID', id); + target.searchParams.append('oauthProxyProvider', provider); + router.replace(target.toString()); + }; + return [ + { + icon: GithubIcon, + cb: (e) => { + e.preventDefault(); + if (!isAgreeCb()) return; + const state = generateState(); + const githubConf = conf?.idp.github; + if (githubConf.proxyAddress) + oauthProxyLogin({ + provider: 'GITHUB', + state, + proxyAddress: githubConf.proxyAddress, + id: githubConf?.clientID as string + }); + else + oauthLogin({ + provider: 'GITHUB', + url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user&state=${state}` + }); + }, + text: t('login_with_github'), + need: conf.idp.github.enabled }, - need: conf.idp.github.enabled - }, - { - icon: WechatIcon, - cb: (e) => { - const wechatConf = conf?.idp.wechat; - e.preventDefault(); - const state = generateState(); - if (conf.proxyAddress) - oauthProxyLogin({ - provider: 'WECHAT', - state, - id: conf.idp.wechat?.clientID - }); - else - oauthLogin({ - 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` - }); + { + icon: WechatIcon, + cb: (e) => { + if (!isAgreeCb()) return; + const wechatConf = conf?.idp.wechat; + e.preventDefault(); + const state = generateState(); + if (wechatConf.proxyAddress) + oauthProxyLogin({ + provider: 'WECHAT', + state, + proxyAddress: wechatConf.proxyAddress, + id: conf.idp.wechat?.clientID + }); + else + oauthLogin({ + 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` + }); + }, + text: t('login_with_wechat'), + need: conf?.idp.wechat?.enabled as boolean }, - need: conf?.idp.wechat?.enabled as boolean - }, - { - icon: GoogleIcon, - cb: (e) => { - e.preventDefault(); - const state = generateState(); - const googleConf = conf?.idp.google; - const scope = encodeURIComponent( - `https://www.googleapis.com/auth/userinfo.profile openid` - ); - if (conf.proxyAddress) - oauthProxyLogin({ - state, - provider: 'GOOGLE', - id: googleConf.clientID - }); - else - oauthLogin({ - 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` - }); + { + icon: GoogleIcon, + cb: (e) => { + e.preventDefault(); + if (!isAgreeCb()) return; + const state = generateState(); + const googleConf = conf?.idp.google; + const scope = encodeURIComponent( + `https://www.googleapis.com/auth/userinfo.profile openid` + ); + if (googleConf.proxyAddress) + oauthProxyLogin({ + state, + provider: 'GOOGLE', + proxyAddress: googleConf.proxyAddress, + id: googleConf.clientID + }); + else + oauthLogin({ + 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` + }); + }, + text: t('login_with_google'), + need: conf.idp.google.enabled as boolean }, - need: conf.idp.google.enabled as boolean - }, - { - icon: () => ( -
- logo -
- ), - cb: (e) => { - e.preventDefault(); - const state = generateState(); - const oauth2Conf = conf?.idp.oauth2; - if (conf.proxyAddress) - oauthProxyLogin({ - provider: 'OAUTH2', - state, - id: oauth2Conf.clientID as string - }); - else - oauthLogin({ - 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, logo, router]); + { + icon: () => ( +
+ logo +
+ ), + cb: (e) => { + e.preventDefault(); + const state = generateState(); + const oauth2Conf = conf?.idp.oauth2; + if (oauth2Conf.proxyAddress) + oauthProxyLogin({ + provider: 'OAUTH2', + state, + proxyAddress: oauth2Conf.proxyAddress, + id: oauth2Conf.clientID as string + }); + else + oauthLogin({ + provider: 'OAUTH2', + url: `${oauth2Conf?.authURL}?client_id=${oauth2Conf.clientID}&redirect_uri=${oauth2Conf.callbackURL}&response_type=code&state=${state}` + }); + }, + text: t('login_with_oauth2'), + need: conf.idp.oauth2?.enabled as boolean + } + ]; + }, [conf, logo, router, isAgreeCb]); return ( - + {authList .filter((item) => item.need) .map((item, index) => ( ))} diff --git a/frontend/desktop/src/components/signin/auth/useProtocol.tsx b/frontend/desktop/src/components/signin/auth/useProtocol.tsx index c381ee8cfdd..e8ba66c3e5b 100644 --- a/frontend/desktop/src/components/signin/auth/useProtocol.tsx +++ b/frontend/desktop/src/components/signin/auth/useProtocol.tsx @@ -1,4 +1,4 @@ -import { Checkbox, Flex, Link, Text, TextProps } from '@chakra-ui/react'; +import { Checkbox, Flex, Link, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; @@ -12,7 +12,6 @@ const useProtocol = ({ const { t, i18n } = useTranslation(); const [isAgree, setIsAgree] = useState(false); const [isInvalid, setIsInvalid] = useState(false); - const Protocol = () => ( (LoginType.NONE); @@ -119,15 +121,19 @@ export default function SigninComponent() { [loginConfig, tabIndex] ); - const handleLogin = debounce(() => { - const selectedConfig = loginConfig[tabIndex]; - if (isAgree && selectedConfig) { - const { login } = selectedConfig; - login(); + const isAgreeCb = () => { + if (isAgree) { + return true; } else { setIsInvalid(true); showError(t('common:read_and_agree')); + return false; } + }; + const handleLogin = debounce(() => { + const selectedConfig = loginConfig[tabIndex]; + if (!isAgreeCb() || !selectedConfig) return; + selectedConfig.login(); }, 500); return ( {conf.layoutConfig?.meta.title || ''} + - - - {conf.layoutConfig?.title} - - + {needTabsCount > 0 && ( + + + {conf.layoutConfig?.title} + + + )} - {pageState === 0 && needTabs && ( + {pageState === 0 && needTabsCount > 1 && ( { @@ -205,45 +214,60 @@ export default function SigninComponent() { )} {LoginComponent} - - {tabIndex !== LoginType.WeChat && ( - <> - - {!!conf.commonConfig?.cfSiteKey && ( - 0 ? ( + <> + + {!!conf.commonConfig?.cfSiteKey && ( + + )} + - - - )} + width="266px" + minH="42px" + mb="14px" + borderRadius="4px" + p="10px" + onClick={handleLogin} + > + {isLoading + ? (t('common:loading') || 'Loading') + '...' + : t('common:log_in') || 'Log In'} + + + + ) : ( + <> + + {conf.layoutConfig?.title} + + + + + ))} diff --git a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts index 9c1f568d09c..5c10e97502f 100644 --- a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts @@ -1,13 +1,13 @@ -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'; +import { bindEmailGuard } from '@/services/backend/middleware/oauth'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { bindEmailSvc } from '@/services/backend/svc/bindProvider'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts index faf1647b9e6..0413e4c4e73 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts @@ -1,17 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { - verifyCodeUidGuard, - filterEmailParams, - sendEmailCodeGuard, filterCodeUid, + filterEmailParams, sendNewEmailCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts index 08958e261ee..d9c3dc24ddf 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts @@ -1,13 +1,12 @@ -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'; +import { filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts index b9e6c7d7356..b39eeba9cc5 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts @@ -1,18 +1,18 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindEmailGuard, unbindEmailGuard } from '@/services/backend/middleware/oauth'; import { - verifyCodeUidGuard, - verifyEmailCodeGuard, + filterCodeUid, filterEmailVerifyParams, - filterCodeUid + verifyCodeUidGuard, + verifyEmailCodeGuard } 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'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts index cb3adc68db8..0a27b76d207 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts @@ -1,13 +1,13 @@ -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'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { jsonRes } from '@/services/backend/response'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts index 7c04a5f4bcd..dd7650de720 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts @@ -1,13 +1,13 @@ -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'; +import { filterCf, filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts index aef6f20822a..3ded4697279 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts @@ -1,12 +1,12 @@ -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'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { unbindEmailSvc } from '@/services/backend/svc/bindProvider'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/info.ts b/frontend/desktop/src/pages/api/auth/info.ts index 634af869c80..afbb84b5d82 100644 --- a/frontend/desktop/src/pages/api/auth/info.ts +++ b/frontend/desktop/src/pages/api/auth/info.ts @@ -1,8 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; +import { verifyAccessToken } from '@/services/backend/auth'; import { globalPrisma, prisma } from '@/services/backend/db/init'; +import { jsonRes } from '@/services/backend/response'; +import { + enableEmailSms, + enableGithub, + enableGoogle, + enablePassword, + enablePhoneSms +} from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; import { ProviderType } from 'prisma/global/generated/client'; -import { verifyAccessToken } from '@/services/backend/auth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -62,14 +69,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) userRestrictedLevel?: number; } = { ...globalData, - oauthProvider: globalData.oauthProvider.map((o) => ({ - providerType: o.providerType, - providerId: ( - [ProviderType.PHONE, ProviderType.PASSWORD, ProviderType.EMAIL] as ProviderType[] - ).includes(o.providerType) - ? o.providerId - : '' - })) + oauthProvider: globalData.oauthProvider + .filter((o) => { + if (o.providerType === ProviderType.GOOGLE) { + return enableGoogle(); + } else if (o.providerType === ProviderType.GITHUB) { + return enableGithub(); + } else if (o.providerType === ProviderType.PHONE) { + return enablePhoneSms(); + } else if (o.providerType === ProviderType.EMAIL) { + return enableEmailSms(); + } else if (o.providerType === ProviderType.PASSWORD) { + return enablePassword(); + } + return true; + }) + .map((o) => ({ + providerType: o.providerType, + providerId: ( + [ProviderType.PHONE, ProviderType.PASSWORD, ProviderType.EMAIL] as ProviderType[] + ).includes(o.providerType) + ? o.providerId + : '' + })) }; if (realNameInfo && realNameInfo.isVerified) { diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts index 04b4dfe2f33..d632cc130ac 100644 --- a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts @@ -1,12 +1,12 @@ -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 { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterCf(req, res, async () => { diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts b/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts index 56c208cc893..f6f11f2c7d5 100644 --- a/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts @@ -1,14 +1,13 @@ -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'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { bindPhoneSvc } from '@/services/backend/svc/bindProvider'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts index 5b8e7b4525f..c7de8319f5e 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts @@ -1,15 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { - filterPhoneParams, filterCodeUid, + filterPhoneParams, sendNewPhoneCodeGuard } from '@/services/backend/middleware/sms'; -import { ErrorHandler } from '@/services/backend/middleware/error'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts index 30bd0065e35..745a3b75cb4 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts @@ -1,12 +1,12 @@ -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'; +import { filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts index 822acaa4428..f0bd3240b9c 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts @@ -1,18 +1,18 @@ -import next, { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindPhoneGuard, unbindPhoneGuard } from '@/services/backend/middleware/oauth'; import { - verifyCodeUidGuard, filterCodeUid, filterPhoneVerifyParams, + verifyCodeUidGuard, 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'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts index 0cde43b58b1..db8812a4e53 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts @@ -1,13 +1,13 @@ -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'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { jsonRes } from '@/services/backend/response'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/sms.ts b/frontend/desktop/src/pages/api/auth/phone/sms.ts index c1456aebfad..bbc14a574c5 100644 --- a/frontend/desktop/src/pages/api/auth/phone/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/sms.ts @@ -1,11 +1,11 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterCf(req, res, async () => { diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts index b5aa6c8c757..a3e2bf9b5c3 100644 --- a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts @@ -1,13 +1,13 @@ -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'; +import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, async ({ userUid }) => { diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts index 10edab96ce8..fd18a1ea7a4 100644 --- a/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts @@ -1,12 +1,12 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { unbindPhoneSvc } from '@/services/backend/svc/bindProvider'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/verify.ts b/frontend/desktop/src/pages/api/auth/phone/verify.ts index 8eee1666e82..cde791744ba 100644 --- a/frontend/desktop/src/pages/api/auth/phone/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/verify.ts @@ -1,11 +1,11 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { getGlobalTokenByPhoneSvc } from '@/services/backend/svc/access'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterPhoneVerifyParams( diff --git a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts index b375b4b9c8c..82f094fa865 100644 --- a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts @@ -29,19 +29,22 @@ function genResAuthClientConfig(conf: AuthConfigType) { enabled: !!conf.idp.password?.enabled }, sms: { - enabled: !!conf.idp.sms?.enabled + enabled: !!conf.idp.sms?.ali?.enabled }, github: { enabled: !!conf.idp.github?.enabled, + proxyAddress: conf.idp.github?.proxyAddress || '', clientID: conf.idp.github?.clientID || '' }, wechat: { enabled: !!conf.idp.wechat?.enabled, - clientID: conf.idp.wechat?.clientID || '' + clientID: conf.idp.wechat?.clientID || '', + proxyAddress: conf.idp.wechat?.proxyAddress || '' }, google: { enabled: !!conf.idp.google?.enabled, - clientID: conf.idp.google?.clientID || '' + clientID: conf.idp.google?.clientID || '', + proxyAddress: conf.idp.google?.proxyAddress || '' }, oauth2: { enabled: !!conf.idp.oauth2?.enabled, @@ -49,10 +52,10 @@ function genResAuthClientConfig(conf: AuthConfigType) { clientID: conf.idp.oauth2?.clientID || '', authURL: conf.idp.oauth2?.authURL || '', tokenURL: conf.idp.oauth2?.tokenURL || '', - userInfoURL: conf.idp.oauth2?.userInfoURL || '' + userInfoURL: conf.idp.oauth2?.userInfoURL || '', + proxyAddress: conf.idp.oauth2?.proxyAddress || '' } }, - proxyAddress: conf.proxyAddress || '', hasBaiduToken: !!conf.baiduToken, billingToken: '' }; diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index 9b25892a39d..5f4caeb3a90 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -2,7 +2,8 @@ export const enableRealNameAuth = () => global.AppConfig.common.realNameAuthEnabled || false; 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 enablePhoneSms = () => global.AppConfig.desktop.auth.idp.sms?.ali?.enabled || false; +export const enableSms = () => global.AppConfig.desktop.auth.idp.sms?.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; diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index 5433c5109a5..598221e416b 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -71,7 +71,6 @@ export type LayoutConfigType = { export type AuthConfigType = { billingToken?: string; - proxyAddress?: string; callbackURL: string; signUpEnabled?: boolean; baiduToken?: string; @@ -92,19 +91,32 @@ export type AuthConfigType = { }; github?: { enabled: boolean; + proxyAddress?: string; clientID: string; clientSecret?: string; }; wechat?: { enabled: boolean; + proxyAddress?: string; clientID: string; clientSecret?: string; }; google?: { enabled: boolean; + proxyAddress?: string; clientID: string; clientSecret?: string; }; + oauth2?: { + enabled: boolean; + callbackURL: string; + clientID: string; + proxyAddress?: string; + clientSecret?: string; + authURL: string; + tokenURL: string; + userInfoURL: string; + }; sms?: { enabled: boolean; ali?: { @@ -123,15 +135,6 @@ export type AuthConfigType = { password: string; }; }; - oauth2?: { - enabled: boolean; - callbackURL: string; - clientID: string; - clientSecret?: string; - authURL: string; - tokenURL: string; - userInfoURL: string; - }; }; }; @@ -239,15 +242,18 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { }, github: { enabled: false, - clientID: '' + clientID: '', + proxyAddress: '' }, wechat: { enabled: false, - clientID: '' + clientID: '', + proxyAddress: '' }, google: { enabled: false, - clientID: '' + clientID: '', + proxyAddress: '' }, sms: { enabled: false @@ -258,10 +264,10 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { clientID: '', authURL: '', tokenURL: '', - userInfoURL: '' + userInfoURL: '', + proxyAddress: '' } }, - proxyAddress: '', billingToken: '' }; From 6489ce6a6d7aaf2aa24ee3514bacab924b0f31a9 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Tue, 8 Oct 2024 10:22:37 +0800 Subject: [PATCH 14/63] fix(desktop):fix merge user (#5101) --- .../mergeUser/NeedToMergeModal.tsx | 42 ++++++++++--------- .../services/backend/cronjob/mergeUserCr.ts | 37 ++++++++++------ 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx index 703e7e177de..0bb7c6c6bf6 100644 --- a/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx @@ -1,27 +1,27 @@ +import { mergeUserRequest } from '@/api/auth'; import { useCustomToast } from '@/hooks/useCustomToast'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ValueOf } from '@/types'; +import { I18nErrorKey } from '@/types/i18next'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; import { - Text, + BoxProps, Button, + HStack, Modal, - ModalOverlay, - ModalContent, + ModalBody, ModalCloseButton, + ModalContent, ModalHeader, + ModalOverlay, Spinner, - ModalBody, - BoxProps, - VStack, - HStack + Text, + VStack } from '@chakra-ui/react'; import { WarnTriangeIcon } from '@sealos/ui'; -import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } 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'; -import { I18nErrorKey } from '@/types/i18next'; function NeedToMerge({ ...props }: BoxProps & {}) { const { mergeUserStatus, mergeUserData, setMergeUserStatus, setMergeUserData } = @@ -63,20 +63,22 @@ function NeedToMerge({ ...props }: BoxProps & {}) { - + {t('common:merge_account_title')} @@ -84,7 +86,7 @@ function NeedToMerge({ ...props }: BoxProps & {}) { {mutation.isLoading ? ( ) : ( - + {mergeUserStatus === MergeUserStatus.CONFLICT diff --git a/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts b/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts index 82832f8643b..51a1e99707b 100644 --- a/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts +++ b/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts @@ -1,13 +1,13 @@ -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 { getBillingUrl, getCvmUrl, getRegionUid, getWorkorderUrl } from '@/services/enable'; import { MergeUserEvent } from '@/types/db/event'; +import axios from 'axios'; +import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { generateBillingToken, generateCronJobToken } from '../auth'; +import { globalPrisma, prisma } from '../db/init'; /** * | | user is exist | user is not exist | @@ -190,13 +190,24 @@ export class MergeUserCrJob implements CronJobStatus { 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( + billingUrl, + { + userid: mergeUser.id, + toUser: user.id, + transferAll: true + }, + { + headers: { + Authorization: + 'Bearer ' + + generateBillingToken({ + userUid: mergeUser.uid, + userId: mergeUser.id + }) + } + } + ), axios.post(workorderUrl, { token: generateCronJobToken({ userUid: user.id, From 9817f6fb81b163f42c7d0b31f7e6350101b0f569 Mon Sep 17 00:00:00 2001 From: yy <56745951+lingdie@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:24:25 +0800 Subject: [PATCH 15/63] devbox cache improve. (#5122) --- controllers/devbox/cmd/main.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/controllers/devbox/cmd/main.go b/controllers/devbox/cmd/main.go index 8dd3f3d7f0d..75e046aa587 100644 --- a/controllers/devbox/cmd/main.go +++ b/controllers/devbox/cmd/main.go @@ -24,11 +24,16 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" @@ -133,6 +138,11 @@ func main() { metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } + cacheObjLabelSelector := labels.SelectorFromSet(map[string]string{ + "app.kubernetes.io/managed-by": "sealos", + "app.kubernetes.io/part-of": "devbox", + }) + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, @@ -151,6 +161,15 @@ func main() { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, + + NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) { + opts.ByObject = map[client.Object]cache.ByObject{ + &corev1.Service{}: {Label: cacheObjLabelSelector}, + &corev1.Pod{}: {Label: cacheObjLabelSelector}, + &corev1.Secret{}: {Label: cacheObjLabelSelector}, + } + return cache.New(config, opts) + }, }) if err != nil { setupLog.Error(err, "unable to start manager") From d9b34cd60044590b6a5807d1725398017a735e19 Mon Sep 17 00:00:00 2001 From: yy <56745951+lingdie@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:05:30 +0800 Subject: [PATCH 16/63] fix build offline scripts and ci. (#5125) --- .github/workflows/cloud-release.yml | 12 ++++++++++-- scripts/cloud/build-offline-tar.sh | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cloud-release.yml b/.github/workflows/cloud-release.yml index fae28f00e44..7c520b15c7c 100644 --- a/.github/workflows/cloud-release.yml +++ b/.github/workflows/cloud-release.yml @@ -67,7 +67,11 @@ jobs: sudo mv /tmp/sealos /usr/bin/sealos sudo sealos version - name: Build - run: export CLOUD_VERSION=${{ github.event.release.tag_name }} && export ARCH=amd64 && bash ./scripts/cloud/build-offline-tar.sh + run: | + export CLOUD_VERSION=${{ github.event.release.tag_name }} + export VERSION=${{ github.event.release.tag_name }} + export ARCH=amd64 + bash ./scripts/cloud/build-offline-tar.sh - name: Setup ossutil uses: manyuanrong/setup-ossutil@v2.0 with: @@ -99,7 +103,11 @@ jobs: sudo mv /tmp/sealos /usr/bin/sealos sudo sealos version - name: Build - run: export CLOUD_VERSION=${{ github.event.release.tag_name }} && VERSION=${{ github.event.release.tag_name }} && export ARCH=arm64 && bash ./scripts/cloud/build-offline-tar.sh + run: | + export CLOUD_VERSION=${{ github.event.release.tag_name }} + export VERSION=${{ github.event.release.tag_name }} + export ARCH=arm64 + bash ./scripts/cloud/build-offline-tar.sh - name: Setup ossutil uses: manyuanrong/setup-ossutil@v2.0 with: diff --git a/scripts/cloud/build-offline-tar.sh b/scripts/cloud/build-offline-tar.sh index 5b5ad2b24ee..650def6aaa3 100644 --- a/scripts/cloud/build-offline-tar.sh +++ b/scripts/cloud/build-offline-tar.sh @@ -8,7 +8,6 @@ CLOUD_VERSION=${CLOUD_VERSION:-"latest"} mkdir -p output/tars images=( - docker.io/labring/sealos-cloud:$CLOUD_VERSION docker.io/labring/kubernetes:v1.28.11 docker.io/labring/helm:v3.14.1 docker.io/labring/cilium:v1.15.8 @@ -34,11 +33,14 @@ for image in "${images[@]}"; do fi done +sealos pull --platform "linux/$ARCH" ghcr.io/labring/sealos-cloud:$CLOUD_VERSION +sealos tag ghcr.io/labring/sealos-cloud:$CLOUD_VERSION docker.io/labring/sealos-cloud:$CLOUD_VERSION +sealos save -o output/tars/sealos-cloud.tar docker.io/labring/sealos-cloud:$CLOUD_VERSION # get and save cli mkdir -p output/cli -VERSION="v5.0.1-beta2" +VERSION="v5.0.1" wget https://github.com/labring/sealos/releases/download/${VERSION}/sealos_${VERSION#v}_linux_${ARCH}.tar.gz \ && tar zxvf sealos_${VERSION#v}_linux_${ARCH}.tar.gz sealos && chmod +x sealos && mv sealos output/cli From 351e7616134950859fe559c57499b6fdff67fa0d Mon Sep 17 00:00:00 2001 From: Jiahui <4543bxy@gmail.com> Date: Tue, 8 Oct 2024 15:02:23 +0800 Subject: [PATCH 17/63] Feat/active task (#5121) * feat active task & first recharge discount * sort discount step & add lock for payment --- .../account/controllers/account_controller.go | 30 ++- .../controllers/account_controller_test.go | 85 ++++++-- .../account/controllers/payment_controller.go | 67 +++--- .../pkg/database/cockroach/accountv2.go | 206 +++++++++++++++++- controllers/pkg/database/interface.go | 4 + controllers/pkg/types/activity.go | 6 + controllers/pkg/types/config.go | 36 +++ controllers/pkg/types/global.go | 15 +- controllers/pkg/types/task.go | 80 +++++++ service/account/api/api.go | 33 ++- service/account/dao/interface.go | 18 ++ service/account/helper/common.go | 1 + service/account/helper/jwt.go | 16 +- service/account/helper/request.go | 16 +- service/account/router/router.go | 27 ++- 15 files changed, 563 insertions(+), 77 deletions(-) create mode 100644 controllers/pkg/types/config.go create mode 100644 controllers/pkg/types/task.go diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index b20dbbaf664..43a9ef301ae 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -23,6 +23,7 @@ import ( "fmt" "math" "os" + "sort" "strconv" "strings" "time" @@ -276,14 +277,12 @@ const BaseUnit = 1_000_000 // return getAmountWithDiscount(amount, *discount), nil //} -func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int64 { - if discount.SpecialDiscount != nil && discount.SpecialDiscount[amount/BaseUnit] != 0 { - return amount + discount.SpecialDiscount[amount/BaseUnit]*BaseUnit - } +func getAmountWithDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) int64 { var r float64 - for i, s := range discount.DiscountSteps { - if amount >= s*BaseUnit { - r = discount.DiscountRates[i] + for _, step := range sortSteps(discount.DefaultSteps) { + ratio := discount.DefaultSteps[step] + if amount >= step*BaseUnit { + r = ratio } else { break } @@ -291,6 +290,23 @@ func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int return int64(math.Ceil(float64(amount) * r / 100)) } +func sortSteps(steps map[int64]float64) (keys []int64) { + for k := range steps { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + return +} + +func getFirstRechargeDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) (bool, int64) { + if discount.FirstRechargeSteps != nil && discount.FirstRechargeSteps[amount/BaseUnit] != 0 { + return true, int64(math.Ceil(float64(amount) * discount.FirstRechargeSteps[amount/BaseUnit] / 100)) + } + return false, getAmountWithDiscount(amount, discount) +} + func (r *AccountReconciler) BillingCVM() error { cvmMap, err := r.CVMDBClient.GetPendingStateInstance(os.Getenv("LOCAL_REGION")) if err != nil { diff --git a/controllers/account/controllers/account_controller_test.go b/controllers/account/controllers/account_controller_test.go index d23bd5bfb29..48696aed680 100644 --- a/controllers/account/controllers/account_controller_test.go +++ b/controllers/account/controllers/account_controller_test.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "encoding/json" "os" "testing" @@ -26,8 +27,6 @@ import ( "github.com/labring/sealos/controllers/pkg/database" "github.com/labring/sealos/controllers/pkg/database/cockroach" "github.com/labring/sealos/controllers/pkg/database/mongo" - - "github.com/labring/sealos/controllers/pkg/types" ) //func Test_giveGift(t *testing.T) { @@ -70,26 +69,26 @@ import ( // } //} -func Test_getAmountWithDiscount(t *testing.T) { - type args struct { - amount int64 - discount types.RechargeDiscount - } - tests := []struct { - name string - args args - want int64 - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getAmountWithDiscount(tt.args.amount, tt.args.discount); got != tt.want { - t.Errorf("getAmountWithDiscount() = %v, want %v", got, tt.want) - } - }) - } -} +//func Test_getAmountWithDiscount(t *testing.T) { +// type args struct { +// amount int64 +// discount types.RechargeDiscount +// } +// tests := []struct { +// name string +// args args +// want int64 +// }{ +// // TODO: Add test cases. +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// if got := getAmountWithDiscount(tt.args.amount, tt.args.discount); got != tt.want { +// t.Errorf("getAmountWithDiscount() = %v, want %v", got, tt.want) +// } +// }) +// } +//} func TestAccountReconciler_BillingCVM(t *testing.T) { dbCtx := context.Background() @@ -136,3 +135,45 @@ func TestAccountReconciler_BillingCVM(t *testing.T) { t.Errorf("AccountReconciler.BillingCVM() error = %v", err) } } + +func TestAccountV2_GetAccountConfig(t *testing.T) { + os.Setenv("LOCAL_REGION", "") + v2Account, err := cockroach.NewCockRoach("", "") + if err != nil { + t.Fatalf("unable to connect to cockroach: %v", err) + } + defer func() { + err := v2Account.Close() + if err != nil { + t.Errorf("unable to disconnect from cockroach: %v", err) + } + }() + err = v2Account.InitTables() + if err != nil { + t.Fatalf("unable to init tables: %v", err) + } + + //if err = v2Account.InsertAccountConfig(&types.AccountConfig{ + // TaskProcessRegion: "192.160.0.55.nip.io", + // FirstRechargeDiscountSteps: map[int64]float64{ + // 8: 100, 32: 100, 128: 100, 256: 100, 512: 100, 1024: 100, + // }, + // DefaultDiscountSteps: map[int64]float64{ + // //128,256,512,1024,2048,4096; 10,15,20,25,30,35 + // 128: 10, 256: 15, 512: 20, 1024: 25, 2048: 30, 4096: 35, + // }, + //}); err != nil { + // t.Fatalf("unable to insert account config: %v", err) + //} + + aa, err := v2Account.GetAccountConfig() + if err != nil { + t.Fatalf("failed to get account config: %v", err) + } + + data, err := json.MarshalIndent(aa, "", " ") + if err != nil { + t.Fatalf("failed to marshal account config: %v", err) + } + t.Logf("success get account config:\n%s", string(data)) +} diff --git a/controllers/account/controllers/payment_controller.go b/controllers/account/controllers/payment_controller.go index 5f0ab3223b9..85e6eac7717 100644 --- a/controllers/account/controllers/payment_controller.go +++ b/controllers/account/controllers/payment_controller.go @@ -23,6 +23,8 @@ import ( "sync" "time" + "github.com/google/uuid" + "sigs.k8s.io/controller-runtime/pkg/manager" pkgtypes "github.com/labring/sealos/controllers/pkg/types" @@ -47,6 +49,8 @@ type PaymentReconciler struct { Logger logr.Logger reconcileDuration time.Duration createDuration time.Duration + accountConfig pkgtypes.AccountConfig + userLock map[uuid.UUID]*sync.Mutex domain string } @@ -69,13 +73,14 @@ const ( //+kubebuilder:rbac:groups=account.sealos.io,resources=payments/finalizers,verbs=update // SetupWithManager sets up the controller with the Manager. -func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) (err error) { const controllerName = "payment_controller" r.Logger = ctrl.Log.WithName(controllerName) r.Logger.V(1).Info("init reconcile controller payment") r.domain = os.Getenv("DOMAIN") r.reconcileDuration = defaultReconcileDuration r.createDuration = defaultCreateDuration + r.userLock = make(map[uuid.UUID]*sync.Mutex) if duration := os.Getenv(EnvPaymentReconcileDuration); duration != "" { reconcileDuration, err := time.ParseDuration(duration) if err == nil { @@ -88,6 +93,14 @@ func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error { r.createDuration = createDuration } } + r.accountConfig, err = r.Account.AccountV2.GetAccountConfig() + if err != nil { + return fmt.Errorf("get account config failed: %w", err) + } + if len(r.accountConfig.DefaultDiscountSteps) == 0 { + return fmt.Errorf("default discount steps is empty") + } + r.Logger.V(1).Info("account config", "config", r.accountConfig) r.Logger.V(1).Info("reconcile duration", "reconcileDuration", r.reconcileDuration, "createDuration", r.createDuration) if err := mgr.Add(r); err != nil { return fmt.Errorf("add payment controller failed: %w", err) @@ -142,22 +155,6 @@ func (r *PaymentReconciler) reconcilePayments(_ context.Context) (errs []error) } func (r *PaymentReconciler) reconcileCreatePayments(ctx context.Context) (errs []error) { - //paymentList := &accountv1.PaymentList{} - //listOpts := &client.ListOptions{ - // FieldSelector: fields.OneTermEqualSelector("status.tradeNO", ""), - //} - //// handler old payment - //err := r.Client.List(context.Background(), paymentList, listOpts) - //if err != nil { - // errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) - // return - //} - //for _, payment := range paymentList.Items { - // if err := r.reconcileNewPayment(&payment); err != nil { - // errs = append(errs, fmt.Errorf("reconcile payment failed: payment: %s, user: %s, err: %w", payment.Name, payment.Spec.UserID, err)) - // } - //} - // watch new payment watcher, err := r.WatchClient.Watch(context.Background(), &accountv1.PaymentList{}, &client.ListOptions{}) if err != nil { errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) @@ -211,20 +208,34 @@ func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error { if err != nil { return fmt.Errorf("get user failed: %w", err) } + if r.userLock[user.UID] == nil { + r.userLock[user.UID] = &sync.Mutex{} + } + r.userLock[user.UID].Lock() + defer r.userLock[user.UID].Unlock() + userDiscount, err := r.Account.AccountV2.GetUserRechargeDiscount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}) + if err != nil { + return fmt.Errorf("get user discount failed: %w", err) + } //1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount payAmount := orderAmount * 10000 - gift := getAmountWithDiscount(payAmount, r.Account.DefaultDiscount) + isFirstRecharge, gift := getFirstRechargeDiscount(payAmount, userDiscount) + paymentRaw := pkgtypes.PaymentRaw{ + UserUID: user.UID, + Amount: payAmount, + Gift: gift, + CreatedAt: payment.CreationTimestamp.Time, + RegionUserOwner: getUsername(payment.Namespace), + Method: payment.Spec.PaymentMethod, + TradeNO: payment.Status.TradeNO, + CodeURL: payment.Status.CodeURL, + } + if isFirstRecharge { + paymentRaw.ActivityType = pkgtypes.ActivityTypeFirstRecharge + } + if err = r.Account.AccountV2.Payment(&pkgtypes.Payment{ - PaymentRaw: pkgtypes.PaymentRaw{ - UserUID: user.UID, - Amount: payAmount, - Gift: gift, - CreatedAt: payment.CreationTimestamp.Time, - RegionUserOwner: getUsername(payment.Namespace), - Method: payment.Spec.PaymentMethod, - TradeNO: payment.Status.TradeNO, - CodeURL: payment.Status.CodeURL, - }, + PaymentRaw: paymentRaw, }); err != nil { return fmt.Errorf("payment failed: %w", err) } diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go index 09001941633..8b13cf5131a 100644 --- a/controllers/pkg/database/cockroach/accountv2.go +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -15,6 +15,7 @@ package cockroach import ( + "encoding/json" "errors" "fmt" "log" @@ -46,6 +47,8 @@ type Cockroach struct { activities types.Activities //TODO need init defaultRechargeDiscount types.RechargeDiscount + accountConfig *types.AccountConfig + tasks map[uuid.UUID]types.Task } const ( @@ -120,6 +123,174 @@ func (c *Cockroach) GetUser(ops *types.UserQueryOpts) (*types.User, error) { return &user, nil } +func cloneMap(m map[int64]float64) map[int64]float64 { + newMap := make(map[int64]float64, len(m)) + for k, v := range m { + newMap[k] = v + } + return newMap +} + +func (c *Cockroach) GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error) { + if ops.UID == uuid.Nil { + user, err := c.GetUser(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get user cr: %v", err) + } + ops.UID = user.UID + } + cfg, err := c.GetAccountConfig() + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get account config: %v", err) + } + isFirstRecharge, err := c.IsNullRecharge(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to check is null recharge: %v", err) + } + defaultSteps, firstRechargeSteps := cfg.DefaultDiscountSteps, cloneMap(cfg.FirstRechargeDiscountSteps) + if !isFirstRecharge && firstRechargeSteps != nil { + payments, err := c.getFirstRechargePayments(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get first recharge payments: %v", err) + } + if len(payments) == 0 { + firstRechargeSteps = map[int64]float64{} + } else { + for i := range payments { + delete(firstRechargeSteps, payments[i].Amount/BaseUnit) + } + } + } + return types.UserRechargeDiscount{ + DefaultSteps: defaultSteps, + FirstRechargeSteps: firstRechargeSteps, + }, nil +} + +func (c *Cockroach) GetAccountConfig() (types.AccountConfig, error) { + if c.accountConfig == nil { + config := &types.Configs{} + if err := c.DB.Where(&types.Configs{}).First(config).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return types.AccountConfig{}, nil + } + return types.AccountConfig{}, fmt.Errorf("failed to get account config: %v", err) + } + var accountConfig types.AccountConfig + if err := json.Unmarshal([]byte(config.Data), &accountConfig); err != nil { + return types.AccountConfig{}, fmt.Errorf("failed to unmarshal account config: %v", err) + } + c.accountConfig = &accountConfig + } + return *c.accountConfig, nil +} + +func (c *Cockroach) InsertAccountConfig(config *types.AccountConfig) error { + data, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal account config: %v", err) + } + return c.DB.Model(&types.Configs{}).Create(&types.Configs{Type: types.AccountConfigType, Data: string(data)}).Error +} + +func (c *Cockroach) IsNullRecharge(ops *types.UserQueryOpts) (bool, error) { + if ops.UID == uuid.Nil { + user, err := c.GetUser(ops) + if err != nil { + return false, fmt.Errorf("failed to get user: %v", err) + } + ops.UID = user.UID + } + var count int64 + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}). + Count(&count).Error; err != nil { + return false, fmt.Errorf("failed to get payment count: %v", err) + } + return count == 0, nil +} + +func (c *Cockroach) getFirstRechargePayments(ops *types.UserQueryOpts) ([]types.Payment, error) { + if ops.UID == uuid.Nil { + user, err := c.GetUser(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user: %v", err) + } + ops.UID = user.UID + } + var payments []types.Payment + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Where(`"activityType" = ?`, types.ActivityTypeFirstRecharge). + Find(&payments).Error; err != nil { + return nil, fmt.Errorf("failed to get payment count: %v", err) + } + return payments, nil +} + +func (c *Cockroach) ProcessPendingTaskRewards() error { + userTasks, err := c.getPendingRewardUserTask() + if err != nil { + return fmt.Errorf("failed to get pending reward user task: %w", err) + } + tasks, err := c.getTask() + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + for i := range userTasks { + err = c.DB.Transaction(func(tx *gorm.DB) error { + task := tasks[userTasks[i].TaskID] + if task.Reward == 0 { + fmt.Printf("usertask %v reward is 0, skip\n", userTasks[i]) + return nil + } + if err = c.updateBalanceRaw(tx, &types.UserQueryOpts{UID: userTasks[i].UserUID}, task.Reward, false, true, true); err != nil { + return fmt.Errorf("failed to update balance: %w", err) + } + msg := fmt.Sprintf("task %s reward", task.Title) + transaction := types.AccountTransaction{ + Balance: task.Reward, + Type: string(task.TaskType) + "_Reward", + UserUID: userTasks[i].UserUID, + ID: uuid.New(), + Message: &msg, + BillingID: userTasks[i].ID, + } + if err = tx.Save(&transaction).Error; err != nil { + return fmt.Errorf("failed to save transaction: %w", err) + } + return c.completeRewardUserTask(tx, &userTasks[i]) + }) + if err != nil { + return fmt.Errorf("failed to process reward pending user task %v rewards: %w", userTasks[i], err) + } + } + return nil +} + +func (c *Cockroach) getTask() (map[uuid.UUID]types.Task, error) { + if len(c.tasks) != 0 { + return c.tasks, nil + } + c.tasks = make(map[uuid.UUID]types.Task) + var tasks []types.Task + if err := c.DB.Model(&types.Task{IsActive: true, IsNewUserTask: true}).Find(&tasks).Error; err != nil { + return nil, fmt.Errorf("failed to get tasks: %v", err) + } + for i := range tasks { + c.tasks[tasks[i].ID] = tasks[i] + } + return c.tasks, nil +} + +func (c *Cockroach) getPendingRewardUserTask() ([]types.UserTask, error) { + var userTasks []types.UserTask + return userTasks, c.DB.Where(&types.UserTask{Status: types.TaskStatusCompleted, RewardStatus: types.TaskStatusNotCompleted}). + Find(&userTasks).Error +} + +func (c *Cockroach) completeRewardUserTask(tx *gorm.DB, userTask *types.UserTask) error { + userTask.RewardStatus = types.TaskStatusCompleted + return tx.Model(userTask).Update("rewardStatus", types.TaskStatusCompleted).Error +} + func (c *Cockroach) GetUserCr(ops *types.UserQueryOpts) (*types.RegionUserCr, error) { if ops.UID == uuid.Nil && ops.Owner == "" { if ops.ID == "" { @@ -314,6 +485,10 @@ func (c *Cockroach) GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.Oaut } func (c *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool) error { + return c.updateBalanceRaw(tx, ops, amount, isDeduction, add, false) +} + +func (c *Cockroach) updateBalanceRaw(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool, isActive bool) error { if ops.UID == uuid.Nil { user, err := c.GetUserCr(ops) if err != nil { @@ -334,6 +509,9 @@ func (c *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount if err := c.updateWithAccount(isDeduction, add, account, amount); err != nil { return err } + if isActive { + account.ActivityBonus = account.ActivityBonus + amount + } if err := tx.Save(account).Error; err != nil { return fmt.Errorf("failed to update account balance: %w", err) } @@ -359,6 +537,12 @@ func (c *Cockroach) AddBalance(ops *types.UserQueryOpts, amount int64) error { }) } +func (c *Cockroach) AddRewardBalance(ops *types.UserQueryOpts, amount int64, db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, false, true) + }) +} + func (c *Cockroach) ReduceBalance(ops *types.UserQueryOpts, amount int64) error { return c.DB.Transaction(func(tx *gorm.DB) error { return c.updateBalance(tx, ops, amount, false, false) @@ -856,6 +1040,7 @@ func (c *Cockroach) NewAccount(ops *types.UserQueryOpts) (*types.Account, error) return account, nil } +// //TODO: remove this method func (c *Cockroach) GetUserAccountRechargeDiscount(ops *types.UserQueryOpts) (*types.RechargeDiscount, error) { userID := ops.UID if userID == uuid.Nil { @@ -958,7 +1143,7 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64, return fmt.Errorf("insufficient balance in sender account, sender is %v, transfer amount %d, the transferable amount is: %d", sender, amount, sender.Balance-sender.DeductionBalance-MinBalance-sender.ActivityBonus) } } else { - amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance + amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance - sender.ActivityBonus if amount <= 0 { return ErrInsufficientBalance } @@ -987,7 +1172,24 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64, } func (c *Cockroach) InitTables() error { - return CreateTableIfNotExist(c.DB, types.Account{}, types.ErrorAccountCreate{}, types.ErrorPaymentCreate{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{}, types.InvoicePayment{}) + err := CreateTableIfNotExist(c.DB, types.Account{}, types.ErrorAccountCreate{}, types.ErrorPaymentCreate{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{}, types.InvoicePayment{}, types.Configs{}) + if err != nil { + return fmt.Errorf("failed to create table: %v", err) + } + + // TODO: remove this after migration + if !c.DB.Migrator().HasColumn(&types.Payment{}, `activityType`) { + //if err := c.DB.Migrator().AddColumn(&types.Payment{PaymentRaw: types.PaymentRaw{}}, `PaymentRaw."activityType"`); err != nil { + // return fmt.Errorf("failed to add column activityType: %v", err) + //} + fmt.Println("add column activityType") + tableName := types.Payment{}.TableName() + err := c.DB.Exec(`ALTER TABLE "?" ADD COLUMN "activityType" TEXT;`, gorm.Expr(tableName)).Error + if err != nil { + return fmt.Errorf("failed to add column activityType: %v", err) + } + } + return nil } func NewCockRoach(globalURI, localURI string) (*Cockroach, error) { diff --git a/controllers/pkg/database/interface.go b/controllers/pkg/database/interface.go index 642269f532f..b8d2f152418 100644 --- a/controllers/pkg/database/interface.go +++ b/controllers/pkg/database/interface.go @@ -95,10 +95,14 @@ type AccountV2 interface { GetUserCr(user *types.UserQueryOpts) (*types.RegionUserCr, error) GetUser(ops *types.UserQueryOpts) (*types.User, error) GetAccount(user *types.UserQueryOpts) (*types.Account, error) + GetAccountConfig() (types.AccountConfig, error) + InsertAccountConfig(config *types.AccountConfig) error GetRegions() ([]types.Region, error) GetLocalRegion() types.Region GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.OauthProvider, error) GetWorkspace(namespaces ...string) ([]types.Workspace, error) + GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error) + //TODO will be removed this method GetUserAccountRechargeDiscount(user *types.UserQueryOpts) (*types.RechargeDiscount, error) SetAccountCreateLocalRegion(account *types.Account, region string) error CreateUser(oAuth *types.OauthProvider, regionUserCr *types.RegionUserCr, user *types.User, workspace *types.Workspace, userWorkspace *types.UserWorkspace) error diff --git a/controllers/pkg/types/activity.go b/controllers/pkg/types/activity.go index ddf78ab5896..50b98410d30 100644 --- a/controllers/pkg/types/activity.go +++ b/controllers/pkg/types/activity.go @@ -23,6 +23,12 @@ import ( "gorm.io/gorm" ) +type UserRechargeDiscount struct { + DefaultSteps map[int64]float64 `json:"defaultSteps,omitempty" bson:"defaultSteps,omitempty"` + FirstRechargeSteps map[int64]float64 `json:"firstRechargeDiscount,omitempty" bson:"firstRechargeDiscount,omitempty"` +} + +// TODO the following structures will be deleted type Activity struct { gorm.Model ActivityType string `gorm:"uniqueIndex"` diff --git a/controllers/pkg/types/config.go b/controllers/pkg/types/config.go new file mode 100644 index 00000000000..a09983014de --- /dev/null +++ b/controllers/pkg/types/config.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +type Configs struct { + Type ConfigType `json:"type" gorm:"type:varchar(255);not null,primaryKey"` + Data string `json:"data" gorm:"type:jsonb"` +} + +type ConfigType string + +const AccountConfigType ConfigType = "account" + +type AccountConfig struct { + TaskProcessRegion string `json:"taskProcessRegion"` + FirstRechargeDiscountSteps map[int64]float64 `json:"firstRechargeDiscountSteps"` + DefaultDiscountSteps map[int64]float64 `json:"defaultDiscountSteps"` +} + +func (c Configs) TableName() string { + return "Configs" +} diff --git a/controllers/pkg/types/global.go b/controllers/pkg/types/global.go index 38070d4d995..38a7ea92b8b 100644 --- a/controllers/pkg/types/global.go +++ b/controllers/pkg/types/global.go @@ -237,12 +237,19 @@ type PaymentRaw struct { Gift int64 `gorm:"type:bigint"` TradeNO string `gorm:"type:text;unique;not null"` // CodeURL is the codeURL of wechatpay - CodeURL string `gorm:"type:text"` - InvoicedAt bool `gorm:"type:boolean;default:false"` - Remark string `gorm:"type:text"` - Message string `gorm:"type:text;not null"` + CodeURL string `gorm:"type:text"` + InvoicedAt bool `gorm:"type:boolean;default:false"` + Remark string `gorm:"type:text"` + ActivityType ActivityType `gorm:"type:text;column:activityType"` + Message string `gorm:"type:text;not null"` } +type ActivityType string + +const ( + ActivityTypeFirstRecharge ActivityType = "FIRST_RECHARGE" +) + func (ErrorPaymentCreate) TableName() string { return "ErrorPaymentCreate" } diff --git a/controllers/pkg/types/task.go b/controllers/pkg/types/task.go new file mode 100644 index 00000000000..3522281dd85 --- /dev/null +++ b/controllers/pkg/types/task.go @@ -0,0 +1,80 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "time" + + "github.com/google/uuid" +) + +// Task represents the Task model in Go with GORM annotations. +type Task struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"` + Title string `gorm:"column:title;type:text;not null" json:"title"` + Description string `gorm:"column:description;type:text;not null" json:"description"` + Reward int64 `gorm:"column:reward;type:bigint;not null" json:"reward"` + Order int `gorm:"column:order;type:integer;not null" json:"order"` + IsActive bool `gorm:"column:isActive;type:boolean;default:true;not null" json:"isActive"` + IsNewUserTask bool `gorm:"column:isNewUserTask;type:boolean;default:false;not null" json:"isNewUserTask"` + TaskType TaskType `gorm:"column:taskType;type:TaskType;not null" json:"taskType"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp();not null" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"` +} + +// UserTask represents the UserTask model in Go with GORM annotations. +type UserTask struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null" json:"userUid"` + TaskID uuid.UUID `gorm:"column:taskId;type:uuid;not null" json:"taskId"` + Status TaskStatus `gorm:"column:status;type:TaskStatus;not null" json:"status"` + RewardStatus TaskStatus `gorm:"column:rewardStatus;type:TaskStatus;not null" json:"rewardStatus"` + CompletedAt time.Time `gorm:"column:completedAt;type:timestamp(3);not null" json:"completedAt"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp();not null" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"` + + //User User `gorm:"foreignKey:UserUid;references:UID" json:"user"` + //Task Task `gorm:"foreignKey:TaskId;references:ID" json:"task"` +} + +// TableName specifies the table name for GORM +func (Task) TableName() string { + return "Task" +} + +// TableName specifies the table name for GORM +func (UserTask) TableName() string { + return "UserTask" +} + +// TaskType represents the TaskType enum in Go. +type TaskType string + +//const ( +// TaskTypeLaunchpad TaskType = "LAUNCHPAD" +// TaskTypeCostcenter TaskType = "COSTCENTER" +// TaskTypeDatabase TaskType = "DATABASE" +// TaskTypeDesktop TaskType = "DESKTOP" +//) + +// TaskStatus represents the TaskStatus enum in Go. +type TaskStatus string + +const ( + TaskStatusNotCompleted TaskStatus = "NOT_COMPLETED" + TaskStatusCompleted TaskStatus = "COMPLETED" +) diff --git a/service/account/api/api.go b/service/account/api/api.go index d45b76bbe2c..51155b26c1b 100644 --- a/service/account/api/api.go +++ b/service/account/api/api.go @@ -726,8 +726,9 @@ func ParseAuthTokenUser(c *gin.Context) (auth *helper.Auth, err error) { return nil, fmt.Errorf("invalid user: %v", user) } auth = &helper.Auth{ - Owner: user.UserCrName, - UserID: user.UserID, + Owner: user.UserCrName, + UserID: user.UserID, + UserUID: user.UserUID, } // if the user is not in the local region, get the user cr name from db if dao.DBClient.GetLocalRegion().UID.String() != user.RegionUID { @@ -977,6 +978,34 @@ func UserUsage(c *gin.Context) { }) } +// GetRechargeDiscount +// @Summary Get recharge discount +// @Description Get recharge discount +// @Tags RechargeDiscount +// @Accept json +// @Produce json +// @Param request body helper.GetRechargeDiscountReq true "Get recharge discount request" +// @Success 200 {object} map[string]interface{} "successfully get recharge discount" +// @Failure 400 {object} map[string]interface{} "failed to parse get recharge discount request" +// @Failure 401 {object} map[string]interface{} "authenticate error" +// @Failure 500 {object} map[string]interface{} "failed to get recharge discount" +// @Router /account/v1alpha1/recharge/discount [post] +func GetRechargeDiscount(c *gin.Context) { + req := &helper.AuthBase{} + if err := authenticateRequest(c, req); err != nil { + c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)}) + return + } + discount, err := dao.DBClient.GetRechargeDiscount(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get recharge discount : %v", err)}) + return + } + c.JSON(http.StatusOK, gin.H{ + "discount": discount, + }) +} + // GetUserRealNameInfo // @Summary Get user real name information // @Description Retrieve the real name information for a user diff --git a/service/account/dao/interface.go b/service/account/dao/interface.go index 1fc3aa6705b..4aa2af419bd 100644 --- a/service/account/dao/interface.go +++ b/service/account/dao/interface.go @@ -57,6 +57,8 @@ type Interface interface { GetRegions() ([]types.Region, error) GetLocalRegion() types.Region UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, error) + GetRechargeDiscount(req helper.AuthReq) (helper.RechargeDiscountResp, error) + ProcessPendingTaskRewards() error GetUserRealNameInfo(req *helper.GetRealNameInfoReq) (*types.UserRealNameInfo, error) } @@ -1446,6 +1448,22 @@ func (m *Account) UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, erro return giftCode, nil } +func (m *Account) GetRechargeDiscount(req helper.AuthReq) (helper.RechargeDiscountResp, error) { + userQuery := &types.UserQueryOpts{UID: req.GetAuth().UserUID} + userDiscount, err := m.ck.GetUserRechargeDiscount(userQuery) + if err != nil { + return helper.RechargeDiscountResp{}, fmt.Errorf("failed to get user recharge discount: %v", err) + } + return helper.RechargeDiscountResp{ + DefaultSteps: userDiscount.DefaultSteps, + FirstRechargeSteps: userDiscount.FirstRechargeSteps, + }, nil +} + +func (m *Account) ProcessPendingTaskRewards() error { + return m.ck.ProcessPendingTaskRewards() +} + func (m *Account) GetUserRealNameInfo(req *helper.GetRealNameInfoReq) (*types.UserRealNameInfo, error) { // get user info userRealNameInfo, err := m.ck.GetUserRealNameInfoByUserID(req.UserID) diff --git a/service/account/helper/common.go b/service/account/helper/common.go index 7120ea26265..ba88c53a52c 100644 --- a/service/account/helper/common.go +++ b/service/account/helper/common.go @@ -28,6 +28,7 @@ const ( GetInvoicePayment = "/invoice/get-payment" UseGiftCode = "/gift-code/use" UserUsage = "/user-usage" + GetRechargeDiscount = "/recharge-discount" GetUserRealNameInfo = "/real-name-info" ) diff --git a/service/account/helper/jwt.go b/service/account/helper/jwt.go index 002fee6d20a..dfef164d3e9 100644 --- a/service/account/helper/jwt.go +++ b/service/account/helper/jwt.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/google/uuid" + "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" @@ -21,13 +23,13 @@ type UserClaims struct { } type JwtUser struct { - UserUID string `json:"userUid,omitempty"` - UserCrUID string `json:"userCrUid,omitempty"` - UserCrName string `json:"userCrName,omitempty"` - RegionUID string `json:"regionUid,omitempty"` - UserID string `json:"userId,omitempty"` - WorkspaceID string `json:"workspaceId,omitempty"` - WorkspaceUID string `json:"workspaceUid,omitempty"` + UserUID uuid.UUID `json:"userUid,omitempty"` + UserCrUID string `json:"userCrUid,omitempty"` + UserCrName string `json:"userCrName,omitempty"` + RegionUID string `json:"regionUid,omitempty"` + UserID string `json:"userId,omitempty"` + WorkspaceID string `json:"workspaceId,omitempty"` + WorkspaceUID string `json:"workspaceUid,omitempty"` } func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager { diff --git a/service/account/helper/request.go b/service/account/helper/request.go index 0c264ff8b5f..4d2994de617 100644 --- a/service/account/helper/request.go +++ b/service/account/helper/request.go @@ -4,6 +4,8 @@ import ( "fmt" "time" + "github.com/google/uuid" + "github.com/labring/sealos/service/account/common" "github.com/dustin/go-humanize" @@ -292,6 +294,11 @@ func (a *AuthBase) SetAuth(auth *Auth) { a.Auth = auth } +type RechargeDiscountResp struct { + DefaultSteps map[int64]float64 `json:"defaultSteps,omitempty" bson:"defaultSteps,omitempty"` + FirstRechargeSteps map[int64]float64 `json:"firstRechargeDiscount,omitempty" bson:"firstRechargeDiscount,omitempty"` +} + type NamespaceBillingHistoryResp struct { Data NamespaceBillingHistoryRespData `json:"data,omitempty" bson:"data,omitempty"` Message string `json:"message,omitempty" bson:"message" example:"successfully retrieved namespace list"` @@ -320,10 +327,11 @@ type TimeRange struct { } type Auth struct { - Owner string `json:"owner" bson:"owner" example:"admin"` - UserID string `json:"userID" bson:"userID" example:"admin"` - KubeConfig string `json:"kubeConfig" bson:"kubeConfig"` - Token string `json:"token" bson:"token" example:"token"` + Owner string `json:"owner" bson:"owner" example:"admin"` + UserUID uuid.UUID `json:"userUID" bson:"userUID" example:"user-123"` + UserID string `json:"userID" bson:"userID" example:"admin"` + KubeConfig string `json:"kubeConfig" bson:"kubeConfig"` + Token string `json:"token" bson:"token" example:"token"` } func ParseNamespaceBillingHistoryReq(c *gin.Context) (*NamespaceBillingHistoryReq, error) { diff --git a/service/account/router/router.go b/service/account/router/router.go index cd42df58836..4c875f5447a 100644 --- a/service/account/router/router.go +++ b/service/account/router/router.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "syscall" + "time" "github.com/labring/sealos/controllers/pkg/utils/env" @@ -30,8 +31,9 @@ func RegisterPayRouter() { if err := dao.InitDB(); err != nil { log.Fatalf("Error initializing database: %v", err) } + ctx := context.Background() defer func() { - if err := dao.DBClient.Disconnect(context.Background()); err != nil { + if err := dao.DBClient.Disconnect(ctx); err != nil { log.Fatalf("Error disconnecting database: %v", err) } }() @@ -63,6 +65,7 @@ func RegisterPayRouter() { POST(helper.GetInvoicePayment, api.GetInvoicePayment). POST(helper.UseGiftCode, api.UseGiftCode). POST(helper.UserUsage, api.UserUsage). + POST(helper.GetRechargeDiscount, api.GetRechargeDiscount). POST(helper.GetUserRealNameInfo, api.GetUserRealNameInfo) docs.SwaggerInfo.Host = env.GetEnvWithDefault("SWAGGER_HOST", "localhost:2333") router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) @@ -80,9 +83,31 @@ func RegisterPayRouter() { } }() + // process task + if os.Getenv("REWARD_PROCESSING") == "true" { + fmt.Println("Start reward processing timer") + go startRewardProcessingTimer(ctx) + } + // Wait for interrupt signal. <-interrupt // Terminate procedure. os.Exit(0) } + +func startRewardProcessingTimer(ctx context.Context) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := dao.DBClient.ProcessPendingTaskRewards(); err != nil { + log.Printf("Error processing pending task rewards: %v", err) + } + case <-ctx.Done(): + log.Println("Reward processing timer stopped") + return + } + } +} From 2b74a1281cdd72ec5f02a0cc9edf042639a1e054 Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:52:21 +0800 Subject: [PATCH 18/63] feat: Implement app guide module (#5115) * add user task table * update desktop guide * desktop guide done * delete Deprecated API * support verifyAppToken * update desktop prisma * launchpad guide * update db * add desktop api * db guide done * update launchpad api * appstore done * update * fix desktop autolaunch * fix desktop refresh * update * update desktop * fix password login * fix fontweight * desktop add check * update db * update desktop * checkDeductionBalanceAndCreateTasks * update deploy * update check * update task title --- .../migration.sql | 41 ++ frontend/desktop/prisma/global/schema.prisma | 45 ++ .../desktop/public/locales/en/common.json | 21 +- .../desktop/public/locales/zh/common.json | 21 +- frontend/desktop/src/api/platform.ts | 14 +- .../src/components/desktop_content/apps.tsx | 3 +- .../src/components/desktop_content/index.tsx | 93 +++- .../desktop/src/components/icons/index.tsx | 177 +++++++ .../components/signin/auth/usePassword.tsx | 2 +- .../src/components/task/floatButton.tsx | 44 ++ .../desktop/src/components/task/taskModal.tsx | 157 ++++++ .../desktop/src/components/task/useDriver.tsx | 349 ++++++++++++++ frontend/desktop/src/constants/account.ts | 2 + frontend/desktop/src/hooks/useDriver.tsx | 346 ------------- .../src/pages/api/account/checkTask.ts | 103 ++++ .../src/pages/api/account/getAccount.ts | 26 - .../desktop/src/pages/api/account/getTasks.ts | 49 ++ .../src/pages/api/account/updateGuide.ts | 35 -- .../src/pages/api/account/updateTask.ts | 52 ++ .../pages/api/v1alpha/account/getAccount.ts | 2 +- .../pages/api/v1alpha/account/updateGuide.ts | 2 +- frontend/desktop/src/pages/index.tsx | 5 + frontend/desktop/src/services/backend/auth.ts | 15 + .../src/services/backend/globalAuth.ts | 75 ++- frontend/desktop/src/stores/desktopConfig.ts | 18 + frontend/desktop/src/types/crd.ts | 9 + frontend/desktop/src/types/task.ts | 12 + frontend/packages/driver/src/overlay.ts | 41 +- frontend/pnpm-lock.yaml | 6 + .../public/locales/en/common.json | 20 +- .../public/locales/zh/common.json | 23 +- .../applaunchpad/src/api/platform.ts | 30 +- .../src/components/Icon/icons/gift.svg | 10 + .../src/components/Icon/index.tsx | 1 + .../src/hooks/useDetailDriver.tsx | 454 ++++++++---------- .../applaunchpad/src/hooks/useDriver.tsx | 146 +++--- .../api/guide/{getAccount.ts => checkTask.ts} | 16 +- .../src/pages/api/guide/getBonus.ts | 35 +- .../src/pages/api/guide/getTasks.ts | 55 +++ .../src/pages/api/guide/updateGuide.ts | 57 --- .../app/detail/components/AppBaseInfo.tsx | 15 - .../app/detail/components/AppMainInfo.tsx | 5 +- .../src/pages/app/detail/components/Pods.tsx | 2 +- .../src/pages/app/edit/components/Form.tsx | 10 +- .../applaunchpad/src/pages/app/edit/index.tsx | 17 +- .../applaunchpad/src/services/backend/auth.ts | 12 + .../applaunchpad/src/services/request.ts | 5 +- .../providers/applaunchpad/src/store/guide.ts | 27 ++ .../applaunchpad/src/styles/global.scss | 1 + .../applaunchpad/src/types/user.d.ts | 15 + .../providers/applaunchpad/src/utils/user.ts | 15 +- frontend/providers/dbprovider/deploy/Kubefile | 2 + .../deploy/manifests/deploy.yaml.tmpl | 4 + frontend/providers/dbprovider/package.json | 1 + .../dbprovider/public/locales/en/common.json | 23 +- .../dbprovider/public/locales/zh/common.json | 23 +- .../providers/dbprovider/src/api/platform.ts | 24 +- .../src/components/Icon/icons/gift.svg | 10 + .../dbprovider/src/components/Icon/index.tsx | 1 + .../providers/dbprovider/src/constants/db.ts | 2 +- .../dbprovider/src/hooks/useDetailDriver.tsx | 254 ++++++++++ .../dbprovider/src/hooks/useDriver.tsx | 132 +++++ .../providers/dbprovider/src/pages/_app.tsx | 1 + .../src/pages/api/guide/checkTask.ts | 41 ++ .../src/pages/api/guide/getBonus.ts | 53 ++ .../src/pages/api/guide/getTasks.ts | 57 +++ .../db/detail/components/AppBaseInfo.tsx | 1 + .../dbprovider/src/pages/db/detail/index.tsx | 2 + .../src/pages/db/edit/components/Header.tsx | 8 +- .../dbprovider/src/pages/db/edit/index.tsx | 4 +- .../dbprovider/src/services/backend/auth.ts | 13 + .../dbprovider/src/services/request.ts | 4 +- .../providers/dbprovider/src/store/guide.ts | 27 ++ .../dbprovider/src/styles/reset.scss | 6 + .../providers/dbprovider/src/types/user.d.ts | 15 + .../providers/dbprovider/src/utils/tools.ts | 4 + .../providers/dbprovider/src/utils/user.ts | 13 + frontend/providers/template/deploy/Kubefile | 2 + .../deploy/manifests/deploy.yaml.tmpl | 6 + frontend/providers/template/next.config.js | 18 +- frontend/providers/template/package.json | 1 + .../template/public/locales/en/common.json | 9 +- .../template/public/locales/zh/common.json | 13 +- .../providers/template/src/api/platform.ts | 25 +- .../src/components/Icon/icons/gift.svg | 10 + .../template/src/components/Icon/index.tsx | 1 + .../src/components/layout/appmenu.tsx | 97 ++-- .../template/src/hooks/useDetailDriver.tsx | 232 +++++++++ .../providers/template/src/pages/_app.tsx | 2 + .../template/src/pages/api/guide/checkTask.ts | 41 ++ .../template/src/pages/api/guide/getBonus.ts | 53 ++ .../template/src/pages/api/guide/getTasks.ts | 57 +++ .../template/src/pages/instance/index.tsx | 3 + .../template/src/services/backend/auth.ts | 12 + .../template/src/services/request.ts | 4 +- .../providers/template/src/store/guide.ts | 27 ++ .../providers/template/src/styles/reset.scss | 6 + frontend/providers/template/src/types/user.ts | 15 + .../providers/template/src/utils/tools.ts | 4 + frontend/providers/template/src/utils/user.ts | 13 + 100 files changed, 3111 insertions(+), 1001 deletions(-) create mode 100644 frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql create mode 100644 frontend/desktop/src/components/task/floatButton.tsx create mode 100644 frontend/desktop/src/components/task/taskModal.tsx create mode 100644 frontend/desktop/src/components/task/useDriver.tsx delete mode 100644 frontend/desktop/src/hooks/useDriver.tsx create mode 100644 frontend/desktop/src/pages/api/account/checkTask.ts delete mode 100644 frontend/desktop/src/pages/api/account/getAccount.ts create mode 100644 frontend/desktop/src/pages/api/account/getTasks.ts delete mode 100644 frontend/desktop/src/pages/api/account/updateGuide.ts create mode 100644 frontend/desktop/src/pages/api/account/updateTask.ts create mode 100644 frontend/desktop/src/types/task.ts create mode 100644 frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg rename frontend/providers/applaunchpad/src/pages/api/guide/{getAccount.ts => checkTask.ts} (66%) create mode 100644 frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts delete mode 100644 frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts create mode 100644 frontend/providers/applaunchpad/src/store/guide.ts create mode 100644 frontend/providers/dbprovider/src/components/Icon/icons/gift.svg create mode 100644 frontend/providers/dbprovider/src/hooks/useDetailDriver.tsx create mode 100644 frontend/providers/dbprovider/src/hooks/useDriver.tsx create mode 100644 frontend/providers/dbprovider/src/pages/api/guide/checkTask.ts create mode 100644 frontend/providers/dbprovider/src/pages/api/guide/getBonus.ts create mode 100644 frontend/providers/dbprovider/src/pages/api/guide/getTasks.ts create mode 100644 frontend/providers/dbprovider/src/store/guide.ts create mode 100644 frontend/providers/template/src/components/Icon/icons/gift.svg create mode 100644 frontend/providers/template/src/hooks/useDetailDriver.tsx create mode 100644 frontend/providers/template/src/pages/api/guide/checkTask.ts create mode 100644 frontend/providers/template/src/pages/api/guide/getBonus.ts create mode 100644 frontend/providers/template/src/pages/api/guide/getTasks.ts create mode 100644 frontend/providers/template/src/store/guide.ts diff --git a/frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql b/frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql new file mode 100644 index 00000000000..41b6425f77a --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240926064719_add_user_task_table/migration.sql @@ -0,0 +1,41 @@ +-- CreateEnum +CREATE TYPE "TaskType" AS ENUM ('LAUNCHPAD', 'COSTCENTER', 'DATABASE', 'DESKTOP', 'APPSTORE'); + +-- CreateEnum +CREATE TYPE "TaskStatus" AS ENUM ('NOT_COMPLETED', 'COMPLETED'); + +-- CreateTable +CREATE TABLE "Task" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "title" STRING NOT NULL, + "description" STRING NOT NULL, + "reward" INT8 NOT NULL, + "order" INT4 NOT NULL, + "isActive" BOOL NOT NULL DEFAULT true, + "isNewUserTask" BOOL NOT NULL DEFAULT false, + "taskType" "TaskType" NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "Task_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserTask" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "userUid" UUID NOT NULL, + "taskId" UUID NOT NULL, + "status" "TaskStatus" NOT NULL, + "rewardStatus" "TaskStatus" NOT NULL, + "completedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "UserTask_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "UserTask_taskId_idx" ON "UserTask"("taskId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserTask_userUid_taskId_key" ON "UserTask"("userUid", "taskId"); diff --git a/frontend/desktop/prisma/global/schema.prisma b/frontend/desktop/prisma/global/schema.prisma index 40b28cc33c8..1cc93be5eb4 100644 --- a/frontend/desktop/prisma/global/schema.prisma +++ b/frontend/desktop/prisma/global/schema.prisma @@ -102,6 +102,7 @@ model User { newMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("newUser") DeleteUserTransactionInfo DeleteUserTransactionInfo? deleteUserLog DeleteUserLog? + userTasks UserTask[] } model Transfer { @@ -323,3 +324,47 @@ enum UserStatus { LOCK_USER DELETE_USER } + +model Task { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + title String + description String + reward BigInt + order Int + isActive Boolean @default(true) + isNewUserTask Boolean @default(false) + taskType TaskType + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + userTasks UserTask[] +} + +model UserTask { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + userUid String @db.Uuid + taskId String @db.Uuid + status TaskStatus + rewardStatus TaskStatus + completedAt DateTime + createdAt DateTime @default(now()) @db.Timestamptz(3) + updatedAt DateTime @updatedAt @db.Timestamptz(3) + + user User @relation(fields: [userUid], references: [uid]) + task Task @relation(fields: [taskId], references: [id]) + + @@unique([userUid, taskId]) + @@index([taskId]) +} + +enum TaskType { + LAUNCHPAD + COSTCENTER + DATABASE + DESKTOP + APPSTORE +} + +enum TaskStatus { + NOT_COMPLETED + COMPLETED +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 7b075e654e6..be27e8ac0c1 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -14,6 +14,8 @@ "and": "and", "app_info": "App Info", "app_launchpad": "App Launchpad", + "application_desktop": "Application desktop", + "application_desktop_tips": "Installed application portal", "avatar": "Avatar", "balance": "Balance", "bind": "Bind", @@ -29,6 +31,7 @@ "charge": "Charge", "click_anywhere_to_continue": "Click on any blank space to continue", "click_on_any_shadow_to_skip": "Click on any shadow to skip", + "completed": "Finish", "completed_the_deployment_of_an_nginx_for_the_first_time": "Completed the deployment of an nginx for the first time", "confirm": "Confirm", "confirm_again": "confirm again", @@ -74,6 +77,9 @@ "gift_amount": "Reward {{amount}} balance.", "github": "Github", "google": "Google", + "guide_applaunchpad": "Quickly deploy applications without cumbersome configuration", + "guide_dbprovider": "Create multiple databases in seconds to meet different application needs", + "guide_objectstorage": "Massive storage space, almost bare metal speed experience", "handle": "Handle", "have_read": "Have read", "healthy_pod": "Healthy Pod: {{count}}", @@ -128,6 +134,7 @@ "new_email": "New email", "new_phone": "New mobile number", "newpassword": "New Password", + "newuser_benefit": "Benefits", "next": "Next", "next_time": "Next", "nickname": "Nickname", @@ -199,11 +206,13 @@ "scan_with_wechat": "Scan with WeChat", "sealos_copilot": "Sealos Copilot", "sealos_document": "Sealos Document", + "sealos_newcomer_benefits": "Sealos Newbie Benefits", "search_apps": "Search Apps", "select_amount": "Select Amount", "service_agreement": "Service Agreement", "spend": "spend", "start_immediately": "Start", + "start_now": "Go Now", "start_your_sealos_journey": "Start your Sealos journey", "status": "Status", "storage": "Storage", @@ -224,19 +233,29 @@ "user_name": "User Name", "username": "Username", "username_tips": "Username must be 3-16 characters, including letters, numbers", + "usertask": { + "task_appstore_desc": "Provides prefabricated multiple types of application and tool templates, click to install, and automatically deploy", + "task_appstore_title": "App Store", + "task_database_desc": "Distributed storage, compatible with multi-language ecology, no need to build complex multi-node architecture by yourself", + "task_database_title": "Create DataBase", + "task_launchpad_desc": "Create a container cluster with one click, automatically deploy container applications, and provide intranet/extranet access addresses", + "task_launchpad_title": "Deploy App" + }, "verification_code_login": "with Phone", "verifyCode_invalid": "Verification code format is incorrect", "verify_code_tips": "6-digit Verification Code", "verify_password": "Verify password", "verifycode": "verification", "view_discount_rules": "View recharge discount rules.", + "view_later": "Talk to You later", "waiting": "Waiting", "warning": "Warning", "wechat": "Wechat", "work_order": "Work Order", "year": "Year", + "you_can_complete_the_following_operations": "You can do the following", "you_can_use_the_kubectl_command_directly_from_the_terminal": "You can use the kubectl command directly from the terminal", "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", "you_have_not_purchased_the_license": "You have not purchased the License", "yuan": "Yuan" -} +} \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index a99e42166be..8f444358963 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -13,6 +13,8 @@ "amount_forecast": "根据近一天消耗金额进行预测", "and": "和", "app_info": "应用信息", + "application_desktop": "应用桌面", + "application_desktop_tips": "已安装应用入口", "avatar": "头像", "balance": "余额", "bind": "绑定", @@ -28,6 +30,7 @@ "charge": "充值", "click_anywhere_to_continue": "点击任意空白继续", "click_on_any_shadow_to_skip": "点击任意阴影跳过", + "completed": "完成", "completed_the_deployment_of_an_nginx_for_the_first_time": "部署一个 nginx ,首次完成 将", "confirm": "确认", "confirm_again": "再次确认", @@ -71,6 +74,9 @@ "gift_amount": "赠送 {{amount}} 余额.", "github": "Github", "google": "Google", + "guide_applaunchpad": "快速部署应用,无需繁琐配置", + "guide_dbprovider": "多种数据库秒级创建,满足不同应用需求", + "guide_objectstorage": "海量存储空间,近乎裸机的速度体验", "handle": "操作", "have_read": "已读", "healthy_pod": "健康 Pod: {{count}}", @@ -124,6 +130,7 @@ "new_email": "新电子邮箱", "new_phone": "新手机号", "newpassword": "新密码", + "newuser_benefit": "新手福利", "next": "下一步", "next_time": "下次吧", "nickname": "昵称", @@ -194,11 +201,13 @@ "rename": "重命名", "scan_with_wechat": "微信扫码支付", "sealos_copilot": "Sealos 小助理", + "sealos_newcomer_benefits": "Sealos 新手福利", "search_apps": "搜索应用", "select_amount": "选择金额", "service_agreement": "服务协议", "spend": "花", "start_immediately": "立即开始", + "start_now": "立即前往", "start_your_sealos_journey": "开始您的 Sealos 之旅", "status": "状态", "storage": "存储", @@ -217,19 +226,29 @@ "user_name": "用户名", "username": "用户名", "username_tips": "用户名为3-16位的英文或数字的字符", + "usertask": { + "task_launchpad_desc": "一键创建容器集群,自动化 部署容器应用,并提供内 网/外网访问地址", + "task_launchpad_title": "部署应用", + "task_database_desc": "分布式存储,兼容多语言生态,无须自行构建复杂的多节点架构", + "task_database_title": "创建数据库", + "task_appstore_desc": "提供预制的多类型应用、工具模板,点击安装,自动部署", + "task_appstore_title": "应用商店" + }, "verification_code_login": "手机号登录", "verifyCode_invalid": "验证码格式不对", "verify_code_tips": "6位验证码", "verify_password": "确认密码", "verifycode": "验证码", "view_discount_rules": "查看优惠规则", + "view_later": "稍后再说", "waiting": "等待中", "warning": "警告", "wechat": "微信", "work_order": "工单", "year": "年", + "you_can_complete_the_following_operations": "您可以完成以下操作", "you_can_use_the_kubectl_command_directly_from_the_terminal": "您可通过终端直接使用 kubectl 命令", "you_can_view_fees_through_the_fee_center": "您可通过费用中心查看费用", "you_have_not_purchased_the_license": "您还没有购买 License", "yuan": "元" -} +} \ No newline at end of file diff --git a/frontend/desktop/src/api/platform.ts b/frontend/desktop/src/api/platform.ts index 723f8b48a9d..5549c306adb 100644 --- a/frontend/desktop/src/api/platform.ts +++ b/frontend/desktop/src/api/platform.ts @@ -8,7 +8,7 @@ import { CommonClientConfigType, TNotification } from '@/types'; -import { AccountCRD } from '@/types/user'; +import { UserTask } from '@/types/task'; // handle baidu export const uploadConvertData = ({ newType, bdVid }: { newType: number[]; bdVid?: string }) => { @@ -24,12 +24,16 @@ export const uploadConvertData = ({ newType, bdVid }: { newType: number[]; bdVid }); }; -export const updateDesktopGuide = () => { - return request.post('/api/account/updateGuide'); +export const getUserTasks = () => { + return request.get('/api/account/getTasks'); }; -export const getUserAccount = () => { - return request.get('/api/account/getAccount'); +export const checkUserTask = () => { + return request.get('/api/account/checkTask'); +}; + +export const updateTask = (taskId: string) => { + return request.post('/api/account/updateTask', { taskId }); }; export const getAppConfig = () => { diff --git a/frontend/desktop/src/components/desktop_content/apps.tsx b/frontend/desktop/src/components/desktop_content/apps.tsx index 8e6db4630af..084c5ec4cee 100644 --- a/frontend/desktop/src/components/desktop_content/apps.tsx +++ b/frontend/desktop/src/components/desktop_content/apps.tsx @@ -109,6 +109,7 @@ export default function Apps() { gap={`${gridSpacing}px`} templateColumns={`repeat(auto-fill, minmax(${appWidth}px, 1fr))`} templateRows={`repeat(auto-fit, ${appHeight}px)`} + className="apps-container" > {paginatedApps && paginatedApps.map((item: TApp, index) => ( @@ -121,9 +122,9 @@ export default function Apps() { userSelect="none" cursor={'pointer'} onClick={(e) => handleDoubleClick(e, item)} + className={item.key} > import('../AppDock'), { ssr: false }); const FloatButton = dynamic(() => import('@/components/floating_button'), { ssr: false }); @@ -51,6 +52,11 @@ export default function Desktop(props: any) { const { session } = useSessionStore(); const { commonConfig } = useConfigStore(); const realNameAuthNotificationIdRef = useRef(); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); const infoData = useQuery({ queryFn: UserInfo, @@ -105,6 +111,9 @@ export default function Desktop(props: any) { [apps, openApp, runningInfo, setToHighestLayerById] ); + const { taskComponentState, setTaskComponentState } = useDesktopConfigStore(); + const { UserGuide, tasks, desktopGuide, handleCloseTaskModal } = useDriver(); + useEffect(() => { const cleanup = createMasterAPP(); return cleanup; @@ -234,22 +243,70 @@ export default function Desktop(props: any) { - {/* {showGuide ? ( - <> - - - - ) : ( - <> - )} */} + {isClient && ( + + {desktopGuide && ( + <> + + + + )} + {taskComponentState === 'modal' && tasks?.length > 0 && ( + { + switch (task.taskType) { + case 'LAUNCHPAD': + openDesktopApp({ + appKey: 'system-applaunchpad', + pathname: '/app/edit', + messageData: { + type: 'InternalAppCall' + } + }); + break; + case 'DATABASE': + openDesktopApp({ + appKey: 'system-dbprovider', + pathname: '/db/edit', + messageData: { type: 'InternalAppCall' } + }); + break; + case 'APPSTORE': + openDesktopApp({ + appKey: 'system-template', + pathname: '/', + messageData: { + type: 'InternalAppCall' + } + }); + break; + default: + console.log(task.taskType); + } + setTaskComponentState('button'); + }} + /> + )} + {taskComponentState === 'button' && ( + { + setTaskComponentState('modal'); + }} + /> + )} + + )} {isAppBar ? : } diff --git a/frontend/desktop/src/components/icons/index.tsx b/frontend/desktop/src/components/icons/index.tsx index 96e183625c8..c4ba436bcdb 100644 --- a/frontend/desktop/src/components/icons/index.tsx +++ b/frontend/desktop/src/components/icons/index.tsx @@ -383,3 +383,180 @@ export function HelpIcon(props: IconProps) { ); } + +export function LaunchpadIcon(props: IconProps) { + return ( + + + + + + + ); +} + +export function DBproviderIcon(props: IconProps) { + return ( + + + + + + ); +} + +export function AppStoreIcon(props: IconProps) { + return ( + + + + ); +} + +export function DriverStarIcon() { + return ( + + + + + + + + + + + ); +} + +export function IdeaIcon(props: IconProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export function RightArrowIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/frontend/desktop/src/components/signin/auth/usePassword.tsx b/frontend/desktop/src/components/signin/auth/usePassword.tsx index 1a33b6e718b..7c5324989cc 100644 --- a/frontend/desktop/src/components/signin/auth/usePassword.tsx +++ b/frontend/desktop/src/components/signin/auth/usePassword.tsx @@ -84,7 +84,7 @@ export default function usePassword({ const infoData = await UserInfo(); const payload = jwtDecode(regionResult.data.token); setSession({ - token: regionResult.data.token, + token: regionResult.data.appToken, // fix cannot get appToken after login user: { k8s_username: payload.userCrName, name: infoData.data?.info.nickname || '', diff --git a/frontend/desktop/src/components/task/floatButton.tsx b/frontend/desktop/src/components/task/floatButton.tsx new file mode 100644 index 00000000000..4a979ee2922 --- /dev/null +++ b/frontend/desktop/src/components/task/floatButton.tsx @@ -0,0 +1,44 @@ +import { Flex, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import React from 'react'; +import { IdeaIcon } from '../icons'; + +interface FloatingTaskButtonProps { + onClick: () => void; +} + +const FloatingTaskButton: React.FC = ({ onClick }) => { + const { t } = useTranslation(); + + return ( + + + + {t('common:newuser_benefit')} + + + ); +}; + +export default FloatingTaskButton; diff --git a/frontend/desktop/src/components/task/taskModal.tsx b/frontend/desktop/src/components/task/taskModal.tsx new file mode 100644 index 00000000000..b78c95413c4 --- /dev/null +++ b/frontend/desktop/src/components/task/taskModal.tsx @@ -0,0 +1,157 @@ +import { IdeaIcon, RightArrowIcon } from '@/components/icons'; +import { I18nCommonKey } from '@/types/i18next'; +import { UserTask } from '@/types/task'; +import { formatMoney } from '@/utils/format'; +import { + Box, + Button, + Flex, + HStack, + Image, + Modal, + ModalContent, + ModalOverlay, + Text, + VStack +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import React from 'react'; + +interface TaskModalProps { + isOpen: boolean; + onClose: () => void; + tasks: UserTask[]; + onTaskClick: (task: UserTask) => void; +} + +const TaskModal: React.FC = ({ isOpen, onClose, tasks, onTaskClick }) => { + const { t, i18n } = useTranslation(); + + const boxStyles = { + border: '1px solid rgba(60, 101, 172, 0.08)', + borderRadius: '6px', + padding: '16px 20px', + backgroundColor: '#FFFFFF', + width: '100%', + cursor: 'pointer' + }; + + return ( + + + + background + + + + {t('common:sealos_newcomer_benefits')} + + + {tasks.map((task) => ( + onTaskClick(task)}> + + + + + {task?.title?.[i18n.language]} + + + + + {t('common:balance')} +{formatMoney(Number(task.reward) || 0)} + + + {task.isCompleted ? ( + + {t('common:completed')} + + ) : ( + + + {t('common:start_now')} + + + + )} + + + ))} + + + + + + + + + + + ); +}; + +export default TaskModal; diff --git a/frontend/desktop/src/components/task/useDriver.tsx b/frontend/desktop/src/components/task/useDriver.tsx new file mode 100644 index 00000000000..dcdbe41d02c --- /dev/null +++ b/frontend/desktop/src/components/task/useDriver.tsx @@ -0,0 +1,349 @@ +import { checkUserTask, getUserTasks, updateTask } from '@/api/platform'; +import { AppStoreIcon, DBproviderIcon, DriverStarIcon, LaunchpadIcon } from '@/components/icons'; +import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; +import { UserTask } from '@/types/task'; +import { Box, Button, Flex, FlexProps, Icon, Image, Text, useMediaQuery } from '@chakra-ui/react'; +import { driver } from '@sealos/driver'; +import { useTranslation } from 'next-i18next'; +import { useEffect, useState } from 'react'; + +export default function useDriver() { + const { t } = useTranslation(); + const [desktopGuide, setDesktopGuide] = useState(false); + const { layoutConfig } = useConfigStore(); + const [tasks, setTasks] = useState([]); + const [isPC] = useMediaQuery('(min-width: 768px)', { + ssr: true, + fallback: false // return false on the server, and re-evaluate on the client side + }); + const conf = useConfigStore().commonConfig; + const { taskComponentState, setTaskComponentState } = useDesktopConfigStore(); + const { canShowGuide } = useDesktopConfigStore(); + + useEffect(() => { + const fetchUserTasks = async () => { + await checkUserTask(); + const data = await getUserTasks(); + setTasks(data.data); + }; + fetchUserTasks(); + }, [taskComponentState]); + + useEffect(() => { + const handleUserGuide = async () => { + const data = await getUserTasks(); + setTasks(data.data); + const desktopTask = data.data.find((task) => task.taskType === 'DESKTOP'); + const allTasksCompleted = data.data.every((task) => task.isCompleted); + + if (!desktopTask?.isCompleted && desktopTask?.id) { + setTaskComponentState('none'); + setDesktopGuide(true); + } else if (allTasksCompleted) { + setTaskComponentState('none'); + } else { + setTaskComponentState(taskComponentState !== 'none' ? taskComponentState : 'button'); + } + }; + + if (isPC && conf?.guideEnabled && canShowGuide) { + handleUserGuide(); + } else { + setDesktopGuide(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [conf?.guideEnabled, isPC, canShowGuide]); + + const completeGuide = async () => { + try { + if (!tasks.length) return; + setDesktopGuide(false); + const desktopTask = tasks.find((task) => task.taskType === 'DESKTOP'); + if (desktopTask) { + await updateTask(desktopTask.id); + setTaskComponentState('modal'); + } + } catch (error) {} + }; + + const handleCloseTaskModal = () => { + setTaskComponentState('button'); + }; + + const checkAllTasksCompleted = () => { + const allCompleted = tasks.every((task) => task.isCompleted); + if (allCompleted) { + setTaskComponentState('none'); + } + return allCompleted; + }; + + const PopoverBodyInfo = (props: FlexProps) => ( + + + {t('common:click_on_any_shadow_to_skip')} + + + + + + ); + + const driverObj = driver({ + showProgress: false, + allowClose: false, + allowClickMaskNextStep: true, + // allowPreviousStep: true, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: true, + steps: [ + { + element: '.apps-container', + popover: { + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', + PopoverBody: ( + + + + + {t('common:application_desktop')} + + + {t('common:application_desktop_tips')} + + + + + ) + } + }, + { + element: '.system-applaunchpad', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_applaunchpad')} + + + + ) + } + }, + { + element: '.system-dbprovider', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_dbprovider')} + + + + ) + } + }, + { + element: '.system-objectstorage', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_objectstorage')} + + + + ) + } + }, + { + element: '.system-template', + popover: { + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', + PopoverBody: ( + + + + {t('common:launch_various_third-party_applications_with_one_click')} + + + + ) + } + } + ], + onDestroyed: () => { + completeGuide(); + } + }); + + const startGuide = () => { + setDesktopGuide(false); + driverObj.drive(); + }; + + const boxStyles: FlexProps = { + border: '1px solid #69AEFF', + borderRadius: '8px', + padding: '24px', + backgroundColor: '#FFFFFF', + boxShadow: '0px 4px 40px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)', + flexDirection: 'column', + maxW: '188px' + }; + + const UserGuide = () => ( + + driver + + + + {t('common:hello_welcome')} + + {layoutConfig?.meta.title} + + 👏 + + + {t('common:you_can_complete_the_following_operations')} + + + + + + + {t('common:usertask.task_launchpad_title')} + + + + {t('common:usertask.task_launchpad_desc')} + + + + + + + {t('common:usertask.task_database_title')} + + + + {t('common:usertask.task_database_desc')} + + + + + + + {t('common:usertask.task_appstore_title')} + + + + {t('common:usertask.task_appstore_desc')} + + + + + + + + + ); + + return { + UserGuide, + desktopGuide, + tasks, + handleCloseTaskModal, + checkAllTasksCompleted, + setTaskComponentState + }; +} diff --git a/frontend/desktop/src/constants/account.ts b/frontend/desktop/src/constants/account.ts index 900ed14c22e..47b10618396 100644 --- a/frontend/desktop/src/constants/account.ts +++ b/frontend/desktop/src/constants/account.ts @@ -1,2 +1,4 @@ export const GUIDE_DESKTOP_INDEX_KEY = 'frontend.guide.desktop.index'; export const LicenseFrontendKey = 'cloud.sealos.io/license-frontend'; + +export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos'; diff --git a/frontend/desktop/src/hooks/useDriver.tsx b/frontend/desktop/src/hooks/useDriver.tsx deleted file mode 100644 index de363b85fb5..00000000000 --- a/frontend/desktop/src/hooks/useDriver.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import { getPriceBonus, getUserAccount, updateDesktopGuide } from '@/api/platform'; -import { GUIDE_DESKTOP_INDEX_KEY } from '@/constants/account'; -import { formatMoney } from '@/utils/format'; -import { Box, Button, Flex, FlexProps, Icon, Image, Text } from '@chakra-ui/react'; -import { driver } from '@sealos/driver'; -import { useTranslation } from 'next-i18next'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { useConfigStore } from '@/stores/config'; - -export function DriverStarIcon() { - return ( - - - - - - - - - - - ); -} - -export default function useDriver({ openDesktopApp }: { openDesktopApp: any }) { - const { t, i18n } = useTranslation(); - const [showGuide, setShowGuide] = useState(false); - const [giftAmount, setGiftAmount] = useState(8); - const router = useRouter(); - - const conf = useConfigStore().commonConfig; - const handleSkipGuide = () => { - setShowGuide(false); - updateDesktopGuide().catch((err) => { - console.log(err); - }); - }; - - useEffect(() => { - const handleUserGuide = async () => { - try { - if (!conf?.guideEnabled) return; - const { data } = await getUserAccount(); - const bonus = await getPriceBonus(); - if (bonus.data?.activities) { - const strategy = JSON.parse(bonus.data?.activities); - const rewardBalance = formatMoney( - strategy?.['beginner-guide']?.phases?.launchpad?.giveAmount || 8000000 - ); - setGiftAmount(rewardBalance); - } - - if (data?.metadata?.annotations && !router.query?.openapp) { - const isGuidedDesktop = !!data.metadata.annotations?.[GUIDE_DESKTOP_INDEX_KEY]; - !isGuidedDesktop ? setShowGuide(true) : ''; - } - } catch (error) {} - }; - conf?.guideEnabled && handleUserGuide(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [conf]); - - const PopoverBodyInfo = (props: FlexProps) => ( - - - {t('common:click_on_any_shadow_to_skip')} - - - - - - ); - - const driverObj = driver({ - showProgress: false, - allowClose: false, - allowClickMaskNextStep: true, - allowPreviousStep: true, - isShowButtons: false, - allowKeyboardControl: false, - disableActiveInteraction: true, - steps: [ - { - element: '.floatButtonNav', - popover: { - side: 'left', - align: 'center', - borderRadius: '12px 12px 0px 12px', - PopoverBody: ( - - - - {t('common:quick_application_switching_floating_ball')} - - - - ) - } - }, - { - element: '.system-terminal', - popover: { - side: 'bottom', - align: 'start', - borderRadius: '0px 12px 12px 12px', - PopoverBody: ( - - - - {t('common:you_can_use_the_kubectl_command_directly_from_the_terminal')} - - - - ) - } - }, - { - element: '.system-dbprovider', - popover: { - side: 'bottom', - align: 'start', - borderRadius: '0px 12px 12px 12px', - PopoverBody: ( - - - - {t('common:help_you_enable_high_availability_database')} - - - - ) - } - }, - { - element: '.system-template', - popover: { - side: 'left', - align: 'center', - borderRadius: '12px 12px 0px 12px', - PopoverBody: ( - - - - {t('common:launch_various_third-party_applications_with_one_click')} - - - - ) - } - }, - { - element: '.system-costcenter', - popover: { - side: 'top', - align: 'start', - borderRadius: '12px 12px 12px 0px', - PopoverBody: ( - - - - {t('common:you_can_view_fees_through_the_fee_center')} - - - - ) - } - }, - { - element: '.system-applaunchpad', - popover: { - side: 'left', - align: 'start', - borderRadius: '12px 0px 12px 12px', - onPopoverRender: () => { - const svg = driverObj.getState('__overlaySvg'); - if (svg) { - const pathElement = svg.querySelector('path'); - if (pathElement) { - pathElement.style.pointerEvents = 'none'; - } - } - }, - PopoverBody: ( - - - - - {t('common:deploy_an_application')} - - - - {t('common:spend')} - - 30s - - {t('common:completed_the_deployment_of_an_nginx_for_the_first_time')} - - {t('common:gift_amount', { amount: giftAmount })} - - - - - - - - ) - } - } - ], - onDestroyed: () => { - console.log('onDestroyed'); - handleSkipGuide(); - } - }); - - const startGuide = () => { - setShowGuide(false); - driverObj.drive(); - }; - - const UserGuide = () => ( - - driver - - - {t('common:hello_welcome')} - - Sealos - - 👏 - - - - - ); - - return { UserGuide, showGuide, startGuide }; -} diff --git a/frontend/desktop/src/pages/api/account/checkTask.ts b/frontend/desktop/src/pages/api/account/checkTask.ts new file mode 100644 index 00000000000..fc854ce5026 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/checkTask.ts @@ -0,0 +1,103 @@ +import { verifyAccessToken, verifyAppToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; +import { K8sApi } from '@/services/backend/kubernetes/user'; +import { jsonRes } from '@/services/backend/response'; +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { TaskStatus, TaskType } from 'prisma/global/generated/client'; +import * as k8s from '@kubernetes/client-node'; +import { templateDeployKey } from '@/constants/account'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = (await verifyAccessToken(req.headers)) || (await verifyAppToken(req.headers)); + if (!payload) return jsonRes(res, { code: 401, message: 'Failed to get info' }); + const namespace = payload.workspaceId; + const _kc = await getUserKubeconfigNotPatch(payload.userCrName); + if (!_kc) return jsonRes(res, { code: 404, message: 'User is not found' }); + const realKc = switchKubeconfigNamespace(_kc, namespace); + const kc = K8sApi(realKc); + if (!kc) return jsonRes(res, { code: 404, message: 'The kubeconfig is not found' }); + + const k8sApp = kc.makeApiClient(k8s.AppsV1Api); + const k8sCustomObjects = kc.makeApiClient(k8s.CustomObjectsApi); + + const userTasks = await globalPrisma.userTask.findMany({ + where: { + userUid: payload.userUid, + status: { not: TaskStatus.COMPLETED } + }, + include: { task: true } + }); + + const [deployments, statefulsets, instances, clusters] = await Promise.all([ + k8sApp.listNamespacedDeployment( + namespace, + undefined, + undefined, + undefined, + undefined, + `!${templateDeployKey}` + ), + k8sApp.listNamespacedStatefulSet( + namespace, + undefined, + undefined, + undefined, + undefined, + `!${templateDeployKey}` + ), + k8sCustomObjects.listNamespacedCustomObject( + 'app.sealos.io', + 'v1', + namespace, + 'instances' + ) as any, + k8sCustomObjects.listNamespacedCustomObject( + 'apps.kubeblocks.io', + 'v1alpha1', + namespace, + 'clusters', + undefined, + undefined, + undefined, + undefined, + `!${templateDeployKey}` + ) as any + ]); + + const tasksToUpdate = userTasks.filter((userTask) => { + switch (userTask.task.taskType) { + case TaskType.LAUNCHPAD: + return deployments.body.items.length > 0 || statefulsets.body.items.length > 0; + case TaskType.APPSTORE: + return instances.body.items.length > 0; + case TaskType.DATABASE: + return clusters.body.items.length > 0; + default: + return false; + } + }); + + if (tasksToUpdate.length > 0) { + await globalPrisma.userTask.updateMany({ + where: { + OR: tasksToUpdate.map((task) => ({ + userUid: task.userUid, + taskId: task.taskId + })) + }, + data: { + status: TaskStatus.COMPLETED, + completedAt: new Date() + } + }); + } + + jsonRes(res, { code: 200, data: `${tasksToUpdate.length} tasks updated successfully` }); + } catch (error) { + console.error('Error processing request:', error); + jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} diff --git a/frontend/desktop/src/pages/api/account/getAccount.ts b/frontend/desktop/src/pages/api/account/getAccount.ts deleted file mode 100644 index 7db135f8258..00000000000 --- a/frontend/desktop/src/pages/api/account/getAccount.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GetCRD, K8sApi } from '@/services/backend/kubernetes/user'; -import { jsonRes } from '@/services/backend/response'; -import { CRDMeta } from '@/types'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { getUserKubeconfigNotPatch, K8sApiDefault } from '@/services/backend/kubernetes/admin'; -import { verifyAccessToken } from '@/services/backend/auth'; -export const AccountMeta: CRDMeta = { - group: 'account.sealos.io', - version: 'v1', - namespace: 'sealos-system', - plural: 'accounts' -}; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const payload = await verifyAccessToken(req.headers); - if (!payload) return jsonRes(res, { code: 401, message: 'token is invaild' }); - const kc = await getUserKubeconfigNotPatch(payload.userCrName); - if (!kc) return jsonRes(res, { code: 404, message: ' kubeconfig is not found' }); - const result = await GetCRD(K8sApiDefault(), AccountMeta, payload.userCrName); - jsonRes(res, { data: result?.body }); - } catch (error) { - console.log(error); - jsonRes(res, { code: 500, data: error }); - } -} diff --git a/frontend/desktop/src/pages/api/account/getTasks.ts b/frontend/desktop/src/pages/api/account/getTasks.ts new file mode 100644 index 00000000000..0dab32e57cb --- /dev/null +++ b/frontend/desktop/src/pages/api/account/getTasks.ts @@ -0,0 +1,49 @@ +import { verifyAccessToken, verifyAppToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { jsonRes } from '@/services/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = (await verifyAccessToken(req.headers)) || (await verifyAppToken(req.headers)); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invaild' }); + + const userTasks = await globalPrisma.userTask.findMany({ + where: { + userUid: payload.userUid, + task: { + isActive: true + } + }, + include: { + task: true + }, + orderBy: { + task: { + order: 'asc' + } + } + }); + + const tasks = userTasks.map((ut) => ({ + id: ut.task.id, + title: JSON.parse(ut.task.title), + description: ut.task.description, + reward: ut.task.reward.toString(), + order: ut.task.order, + taskType: ut.task.taskType, + isCompleted: ut.status === 'COMPLETED', + completedAt: ut.completedAt + })); + + const allTasksCompleted = tasks.every((task) => task.isCompleted); + + jsonRes(res, { + code: 200, + data: allTasksCompleted ? [] : tasks, + message: allTasksCompleted ? 'All tasks completed' : 'Tasks fetched' + }); + } catch (error) { + return jsonRes(res, { code: 500, message: 'error' }); + } +} diff --git a/frontend/desktop/src/pages/api/account/updateGuide.ts b/frontend/desktop/src/pages/api/account/updateGuide.ts deleted file mode 100644 index 8716e1afd02..00000000000 --- a/frontend/desktop/src/pages/api/account/updateGuide.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { K8sApiDefault } from '@/services/backend/kubernetes/admin'; -import { UpdateCRD } from '@/services/backend/kubernetes/user'; -import { jsonRes } from '@/services/backend/response'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { AccountMeta } from './getAccount'; -import { GUIDE_DESKTOP_INDEX_KEY } from '@/constants/account'; -import { verifyAccessToken } from '@/services/backend/auth'; - -// req header is kubeconfig -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const payload = await verifyAccessToken(req.headers); - if (!payload) return jsonRes(res, { code: 401, message: 'token is vaild' }); - - const defaultKc = K8sApiDefault(); - - if (!defaultKc) return jsonRes(res, { code: 401, message: 'No cluster permissions' }); - - const endTime = new Date().toISOString(); - - const jsonPatch = [ - { - op: 'add', - path: `/metadata/annotations/${GUIDE_DESKTOP_INDEX_KEY}`, - value: endTime - } - ]; - - const reuslt = await UpdateCRD(defaultKc, AccountMeta, payload.userCrName, jsonPatch); - - jsonRes(res, { data: reuslt?.body }); - } catch (error) { - jsonRes(res, { code: 500, data: error }); - } -} diff --git a/frontend/desktop/src/pages/api/account/updateTask.ts b/frontend/desktop/src/pages/api/account/updateTask.ts new file mode 100644 index 00000000000..e010aad8e50 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/updateTask.ts @@ -0,0 +1,52 @@ +import { verifyAccessToken, verifyAppToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { jsonRes } from '@/services/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { TaskStatus } from 'prisma/global/generated/client'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = (await verifyAccessToken(req.headers)) || (await verifyAppToken(req.headers)); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); + + if (req.method !== 'POST') { + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } + + const { taskId } = req.body as { taskId: string }; + if (!taskId) { + return jsonRes(res, { code: 400, message: 'Task ID is required' }); + } + + const task = await globalPrisma.userTask.findUnique({ + where: { + userUid_taskId: { + userUid: payload.userUid, + taskId: taskId + } + } + }); + + if (!task) { + return jsonRes(res, { code: 404, message: 'Task not found' }); + } + + if (task.status === TaskStatus.COMPLETED) { + return jsonRes(res, { code: 200, message: 'Task is already completed' }); + } + + const updatedTask = await globalPrisma.userTask.update({ + where: { + userUid_taskId: { + userUid: payload.userUid, + taskId: taskId + } + }, + data: { status: TaskStatus.COMPLETED, completedAt: new Date() } + }); + + jsonRes(res, { code: 200, data: 'success' }); + } catch (error) { + jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} diff --git a/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts b/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts index 8949311f7aa..d1fe1de0d68 100644 --- a/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts +++ b/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts @@ -1,5 +1,5 @@ -import { AccountMeta } from '@/pages/api/account/getAccount'; import { jsonRes } from '@/services/backend/response'; +import { AccountMeta } from '@/types'; import type { NextApiRequest, NextApiResponse } from 'next'; import { initK8s } from 'sealos-desktop-sdk/service'; diff --git a/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts b/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts index 7f0e7b83ae6..117da5086d2 100644 --- a/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts +++ b/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts @@ -1,9 +1,9 @@ import { K8sApiDefault } from '@/services/backend/kubernetes/admin'; import { UpdateCRD } from '@/services/backend/kubernetes/user'; import { jsonRes } from '@/services/backend/response'; +import { AccountMeta } from '@/types'; import type { NextApiRequest, NextApiResponse } from 'next'; import { initK8s } from 'sealos-desktop-sdk/service'; -import { AccountMeta } from '@/pages/api/account/getAccount'; export type UpdateUserGuideParams = { activityType: 'beginner-guide'; diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index cb125e75d70..e90c17333ea 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -3,6 +3,7 @@ import DesktopContent from '@/components/desktop_content'; import useAppStore from '@/stores/app'; import useCallbackStore from '@/stores/callback'; import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; import useSessionStore from '@/stores/session'; import { SemData } from '@/types/sem'; import { NSType } from '@/types/team'; @@ -39,6 +40,8 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str const { session } = useSessionStore(); const { layoutConfig } = useConfigStore(); const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore(); + const { setCanShowGuide } = useDesktopConfigStore(); + useEffect(() => { colorMode === 'dark' ? toggleColorMode() : null; }, [colorMode, toggleColorMode]); @@ -127,6 +130,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str let appkey = ''; let appRoute = ''; if (!state.autolaunch) { + setCanShowGuide(true); const result = parseOpenappQuery((query?.openapp as string) || ''); appQuery = result.appQuery; appkey = result.appkey; @@ -141,6 +145,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str } const app = state.installedApps.find((item) => item.key === appkey); if (!app) return; + setCanShowGuide(false); state.openApp(app, { raw: appQuery, pathname: appRoute }).then(() => { state.cancelAutoLaunch(); }); diff --git a/frontend/desktop/src/services/backend/auth.ts b/frontend/desktop/src/services/backend/auth.ts index 1c010924ac2..db660550487 100644 --- a/frontend/desktop/src/services/backend/auth.ts +++ b/frontend/desktop/src/services/backend/auth.ts @@ -25,6 +25,7 @@ const verifyToken = async (header: IncomingHttpHeaders) => { return null; } }; + export const verifyAccessToken = async (header: IncomingHttpHeaders) => verifyToken(header).then( (payload) => { @@ -36,6 +37,7 @@ export const verifyAccessToken = async (header: IncomingHttpHeaders) => }, (err) => null ); + export const verifyAuthenticationToken = async (header: IncomingHttpHeaders) => { try { if (!header?.authorization) { @@ -63,6 +65,19 @@ export const verifyJWT = (token?: string, secret? }); }); +export const verifyAppToken = async (header: IncomingHttpHeaders) => { + try { + if (!header?.authorization) { + throw new Error('缺少凭证'); + } + const token = decodeURIComponent(header.authorization); + const payload = await verifyJWT(token, internalJwtSecret()); + return payload; + } catch (err) { + return null; + } +}; + export const generateBillingToken = (props: BillingTokenPayload) => sign(props, internalJwtSecret(), { expiresIn: '3600000' }); export const generateAccessToken = (props: AccessTokenPayload) => diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index 6878b0616c9..d746e259823 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -5,9 +5,20 @@ import { AuthConfigType } from '@/types'; import { SemData } from '@/types/sem'; import { hashPassword } from '@/utils/crypto'; import { nanoid } from 'nanoid'; -import { ProviderType, User, UserStatus } from 'prisma/global/generated/client'; +import { + PrismaClient, + ProviderType, + TaskStatus, + User, + UserStatus +} from 'prisma/global/generated/client'; import { enableSignUp } from '../enable'; +type TransactionClient = Omit< + PrismaClient, + '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' +>; + async function signIn({ provider, id }: { provider: ProviderType; id: string }) { const userProvider = await globalPrisma.oauthProvider.findUnique({ where: { @@ -21,6 +32,9 @@ async function signIn({ provider, id }: { provider: ProviderType; id: string }) } }); if (!userProvider) return null; + + await checkDeductionBalanceAndCreateTasks(userProvider.user.uid); + return { user: userProvider.user }; @@ -82,11 +96,65 @@ export async function signInByPassword({ id, password }: { id: string; password: } }); if (!userProvider) return null; + + await checkDeductionBalanceAndCreateTasks(userProvider.user.uid); + return { user: userProvider.user }; } +/** + * Checks the deduction balance of a user and creates new tasks if the balance is zero. + * + * @param {string} userUid - The unique identifier of the user. + */ +async function checkDeductionBalanceAndCreateTasks(userUid: string) { + const account = await globalPrisma.account.findUnique({ + where: { userUid } + }); + + // Check if the account exists, the deduction balance is not null, and the balance is zero. + if ( + account && + account.deduction_balance !== null && + account.deduction_balance.toString() === '0' + ) { + const userTasks = await globalPrisma.userTask.findFirst({ + where: { userUid } + }); + + // If no user tasks are found, create new tasks for the user. + if (!userTasks) { + await globalPrisma.$transaction(async (tx) => { + await createNewUserTasks(tx, userUid); + }); + } + } +} + +// Assign tasks to newly registered users +async function createNewUserTasks(tx: TransactionClient, userUid: string) { + const newUserTasks = await tx.task.findMany({ + where: { + isNewUserTask: true, + isActive: true + } + }); + + for (const task of newUserTasks) { + await tx.userTask.create({ + data: { + userUid, + taskId: task.id, + status: TaskStatus.NOT_COMPLETED, + rewardStatus: task.taskType === 'DESKTOP' ? TaskStatus.COMPLETED : TaskStatus.NOT_COMPLETED, + completedAt: new Date(0) + } + }); + } +} + async function signUp({ provider, id, @@ -128,6 +196,8 @@ async function signUp({ }); } + await createNewUserTasks(tx, user.uid); + return { user }; }); @@ -181,6 +251,8 @@ export async function signUpByPassword({ }); } + await createNewUserTasks(tx, user.uid); + return { user }; }); @@ -244,6 +316,7 @@ export const getGlobalToken = async ({ } } }); + if (provider === ProviderType.PASSWORD) { if (!password) { return null; diff --git a/frontend/desktop/src/stores/desktopConfig.ts b/frontend/desktop/src/stores/desktopConfig.ts index 46d86518265..4c934ed1511 100644 --- a/frontend/desktop/src/stores/desktopConfig.ts +++ b/frontend/desktop/src/stores/desktopConfig.ts @@ -2,7 +2,11 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; +type TaskComponentState = 'none' | 'modal' | 'button'; + type State = { + canShowGuide: boolean; + setCanShowGuide: (value: boolean) => void; isAppBar: boolean; isNavbarVisible: boolean; isAnimationEnabled: boolean; @@ -10,6 +14,8 @@ type State = { toggleNavbarVisibility: (forceState?: boolean) => void; temporarilyDisableAnimation: () => void; getTransitionValue: () => string; + taskComponentState: TaskComponentState; + setTaskComponentState: (state: TaskComponentState) => void; }; export const useDesktopConfigStore = create()( @@ -18,6 +24,13 @@ export const useDesktopConfigStore = create()( isAppBar: true, isNavbarVisible: true, isAnimationEnabled: true, + taskComponentState: 'none', + canShowGuide: false, + setCanShowGuide(value) { + set((state) => { + state.canShowGuide = value; + }); + }, toggleShape() { set((state) => { state.isAppBar = !state.isAppBar; @@ -42,6 +55,11 @@ export const useDesktopConfigStore = create()( return get().isAnimationEnabled ? 'transform 200ms ease-in-out, opacity 200ms ease-in-out' : 'none'; + }, + setTaskComponentState(s) { + set((state) => { + state.taskComponentState = s; + }); } })), { diff --git a/frontend/desktop/src/types/crd.ts b/frontend/desktop/src/types/crd.ts index 3d1b57626bf..3a4f56f2a78 100644 --- a/frontend/desktop/src/types/crd.ts +++ b/frontend/desktop/src/types/crd.ts @@ -13,6 +13,7 @@ export const userCRD = { Version: 'v1', Resource: 'users' }; + export type UserCR = { apiVersion: 'user.sealos.io/v1'; kind: 'User'; @@ -62,6 +63,7 @@ export type StatusCR = { }; code: 404; }; + export type TAppCR = { apiVersion: 'app.sealos.io/v1'; kind: 'App'; @@ -137,3 +139,10 @@ export type TNotification = { }; }; }; + +export const AccountMeta: CRDMeta = { + group: 'account.sealos.io', + version: 'v1', + namespace: 'sealos-system', + plural: 'accounts' +}; diff --git a/frontend/desktop/src/types/task.ts b/frontend/desktop/src/types/task.ts new file mode 100644 index 00000000000..b0d1b04f02d --- /dev/null +++ b/frontend/desktop/src/types/task.ts @@ -0,0 +1,12 @@ +import { TaskType } from 'prisma/global/generated/client'; + +export type UserTask = { + id: string; + title: Record; + description: string; + reward: string; + order: number; + taskType: TaskType; + isCompleted: boolean; + completedAt: string; +}; diff --git a/frontend/packages/driver/src/overlay.ts b/frontend/packages/driver/src/overlay.ts index 682153782ea..1f8b8d65b1c 100644 --- a/frontend/packages/driver/src/overlay.ts +++ b/frontend/packages/driver/src/overlay.ts @@ -111,47 +111,8 @@ function mountOverlay(stagePosition: StageDefinition) { document.body.appendChild(skipButton); - let countdown = 5; - let timeoutId: any; - let isButtonDisabled = true; - - const updateButtonText = () => { - skipButton.innerText = `${getConfig('overlaySkipButton') || ''} (${countdown}s)`; - }; - - const countdownInterval = setInterval(() => { - countdown -= 1; - updateButtonText(); - }, 1000); - - timeoutId = setTimeout(() => { - console.log('Auto skipping after 5 seconds'); - clearInterval(countdownInterval); - enableButton(); - skipButton.innerText = getConfig('overlaySkipButton') || ''; - }, 5000); - - updateButtonText(); - - function disableButton() { - isButtonDisabled = true; - skipButton.style.pointerEvents = 'none'; - skipButton.style.opacity = '0.5'; - } - - function enableButton() { - isButtonDisabled = false; - skipButton.style.pointerEvents = 'auto'; - skipButton.style.opacity = '1'; - } - onDriverClick(skipButton, () => { - if (!isButtonDisabled) { - disableButton(); - clearInterval(countdownInterval); - clearTimeout(timeoutId); - emit('skipButtonClick'); - } + emit('skipButtonClick'); }); setState('__overlaySkipBtn', skipButton); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 00c0dfd2852..b91c087c361 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1191,6 +1191,9 @@ importers: '@next/font': specifier: 13.1.6 version: 13.1.6 + '@sealos/driver': + specifier: workspace:^ + version: link:../../packages/driver '@sealos/ui': specifier: workspace:^ version: link:../../packages/ui @@ -2163,6 +2166,9 @@ importers: '@replit/codemirror-vscode-keymap': specifier: ^6.0.2 version: 6.0.2(@codemirror/autocomplete@6.11.1)(@codemirror/commands@6.3.2)(@codemirror/language@6.9.3)(@codemirror/lint@6.4.2)(@codemirror/search@6.5.5)(@codemirror/state@6.3.2)(@codemirror/view@6.22.1) + '@sealos/driver': + specifier: workspace:^ + version: link:../../packages/driver '@sealos/ui': specifier: workspace:^ version: link:../../packages/ui diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json index d7868afe211..bafd64012a5 100644 --- a/frontend/providers/applaunchpad/public/locales/en/common.json +++ b/frontend/providers/applaunchpad/public/locales/en/common.json @@ -4,7 +4,6 @@ "Add": "Add", "Add Port": "Add Port", "Add volume": "Add Storage", - "Adjust application configuration": "Adjust application configuration", "Advanced Configuration": "Advanced Config", "Age": "Age", "Amount": "Amount", @@ -32,12 +31,11 @@ "Balance": "Balance", "Basic Config": "Basic", "Basic Information": "Basic", - "Can help you deploy any Docker image": "Helps you deploy any Docker image", + "Can help you deploy any Docker image": "Rich image warehouse, supporting any Docker image", "Can not change storage path": "Storage mount path cannot be modified", "Cancel": "Cancel", "capacity": "capacity", "Card": "cards", - "Click here to visit the website": "Click here to open the site", "Click on any shadow to skip": "Click on any shadow to skip", "Click the Deploy Application button": "Click \\\"Deploy Application\\\"", "Cname auth error: customDomain's cname is not equal to publicDomain": "CNAME error. You must configure CNAME to the provided domain before binding.", @@ -99,7 +97,6 @@ "File Value can not empty": "File content is required", "filename": "File Name", "Filename can not empty": "File Name is required", - "First time completion guide benefits": "First-time user bonus", "Fixed instance": "Fixed", "Folder Name": "Folder Name", "gift amount tip": "Top up {{amount}} to get {{gift}} bonus", @@ -188,7 +185,6 @@ "Restarts Num": "Restarts", "Run command": "Command", "Running": "Running", - "Second-level domain name tips": "Public subdomain will be automatically assigned if enabled", "Separated by spaces": "Space separated, e.g.:", "show hidden files": "Show hidden files", "Size": "Size", @@ -266,5 +262,15 @@ "within_1_day": "Within 1 day", "terminated_logs": "Terminated logs", "no_logs_for_now": "No logs for now", - "or": "or" -} \ No newline at end of file + "or": "or", + "guide_deploy_storage": "Massive storage to meet various application needs", + "guide_detail_operate": "Log|Terminal|Details|Restart", + "guide_detail_monitor": "Visual resource monitoring", + "guide_detail_update_button": "Adjust in real time as needed", + "guide_detail_network": "Provide intranet and extranet access addresses and automatically configure SSL certificates", + "first_charge": "First Recharge Discount", + "first_charge_tip": "For some specifications, you can enjoy double the gift amount when you recharge for the first time.", + "gift": "gift", + "balance": "balance", + "guide_deploy_button": "Complete creation and get it now" +} diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json index 8fb7cb20a65..56d12456d25 100644 --- a/frontend/providers/applaunchpad/public/locales/zh/common.json +++ b/frontend/providers/applaunchpad/public/locales/zh/common.json @@ -4,7 +4,6 @@ "Add": "新增", "Add Port": "添加端口", "Add volume": "新增存储卷", - "Adjust application configuration": "调整应用配置", "Advanced Configuration": "高级配置", "Age": "启动时长", "Amount": "数量", @@ -32,12 +31,11 @@ "Balance": "余额", "Basic Config": "基础配置", "Basic Information": "基本信息", - "Can help you deploy any Docker image": "可以帮助您部署任意 Docker 镜像", + "Can help you deploy any Docker image": "丰富的镜像仓库,支持任意 Docker 镜像", "Can not change storage path": "不允许修改挂载路径", "Cancel": "取消", "capacity": "容量", "Card": "张", - "Click here to visit the website": "点击此处访问网站", "Click on any shadow to skip": "点击任意阴影跳过", "Click the Deploy Application button": "点击「部署应用」按钮", "Cname auth error: customDomain's cname is not equal to publicDomain": "CNAME 校验错误,你需要先 CNAME 到指定域名才能绑定", @@ -99,7 +97,6 @@ "File Value can not empty": "文件值不能为空", "filename": "文件名", "Filename can not empty": "文件名不能为空", - "First time completion guide benefits": "首次完成引导福利", "Fixed instance": "固定实例", "Folder Name": "文件夹名", "gift amount tip": "充值 {{amount}} 赠送 {{gift}} ", @@ -179,7 +176,7 @@ "Public Address": "公网地址", "Real-time Monitoring": "实时监控", "Reboot Success": "重启成功", - "receive": "获得", + "receive": "已获得", "rename": "重命名", "Replicas": "实例数", "Restart": "重启", @@ -188,7 +185,6 @@ "Restarts Num": "重启次数", "Run command": "运行命令", "Running": "运行中", - "Second-level domain name tips": "开启后将自动为您分配一个二级公网域名", "Separated by spaces": "以空格分开,如: ", "show hidden files": "显示隐藏文件", "Size": "文件大小", @@ -266,5 +262,16 @@ "within_1_day": "一天内", "terminated_logs": "中断前", "no_logs_for_now": "暂无日志", - "or": "或" -} \ No newline at end of file + "or": "或", + "guide_deploy_command": "丰富的命令集支持,简化服务器管理", + "guide_deploy_storage": "海量存储,满足各种应用需求", + "guide_detail_monitor": "可视化资源监控", + "guide_detail_operate": "日志|终端|详情|重启", + "guide_detail_update_button": "按需实时调整", + "guide_detail_network": "提供内网和外网访问地址,并自动配置 SSL 证书", + "first_charge": "首充优惠", + "first_charge_tip": "部分规格首次充值可享双倍赠送金额", + "gift": "赠", + "balance": "余额", + "guide_deploy_button": "完成创建,立即获得" +} diff --git a/frontend/providers/applaunchpad/src/api/platform.ts b/frontend/providers/applaunchpad/src/api/platform.ts index c64afb13d50..406773b8256 100644 --- a/frontend/providers/applaunchpad/src/api/platform.ts +++ b/frontend/providers/applaunchpad/src/api/platform.ts @@ -1,8 +1,8 @@ import type { Response as InitDataType } from '@/pages/api/platform/getInitData'; import { GET, POST } from '@/services/request'; -import type { AccountCRD, UserQuotaItemType, userPriceType } from '@/types/user'; +import type { UserQuotaItemType, UserTask, userPriceType } from '@/types/user'; +import { getUserSession } from '@/utils/user'; import { AuthCnamePrams } from './params'; -import { UpdateUserGuideParams } from '@/pages/api/guide/updateGuide'; export const getResourcePrice = () => GET('/api/platform/resourcePrice'); @@ -15,12 +15,26 @@ export const getUserQuota = () => export const postAuthCname = (data: AuthCnamePrams) => POST('/api/platform/authCname', data); -export const updateDesktopGuide = (payload: UpdateUserGuideParams) => - POST('/api/guide/updateGuide', payload); - -export const getUserAccount = () => GET('/api/guide/getAccount'); - -export const getPriceBonus = () => GET('/api/guide/getBonus'); +export const getUserTasks = () => + GET<{ needGuide: boolean; task: UserTask }>('/api/guide/getTasks', undefined, { + headers: { + Authorization: getUserSession()?.token + } + }); + +export const checkUserTask = () => + GET('/api/guide/checkTask', undefined, { + headers: { + Authorization: getUserSession()?.token + } + }); + +export const getPriceBonus = () => + GET<{ amount: number; gift: number }[]>('/api/guide/getBonus', undefined, { + headers: { + Authorization: getUserSession()?.token + } + }); export const checkPermission = (payload: { appName: string }) => GET('/api/platform/checkPermission', payload); diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg new file mode 100644 index 00000000000..865d6307dbb --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/index.tsx b/frontend/providers/applaunchpad/src/components/Icon/index.tsx index 6f72e383a5e..bf1f410e152 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/index.tsx +++ b/frontend/providers/applaunchpad/src/components/Icon/index.tsx @@ -29,6 +29,7 @@ const map = { noEvents: require('./icons/noEvents.svg').default, warning: require('./icons/warning.svg').default, analyze: require('./icons/analyze.svg').default, + gift: require('./icons/gift.svg').default, terminal: require('./icons/terminal.svg').default, log: require('./icons/log.svg').default, nvidia: require('./icons/gpu/nvidia.svg').default, diff --git a/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx b/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx index 2353e8ac15c..36ec95d5a70 100644 --- a/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx +++ b/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx @@ -1,22 +1,34 @@ -import { getInitData, getPriceBonus, getUserAccount, updateDesktopGuide } from '@/api/platform'; -import { GUIDE_LAUNCHPAD_DETAIL_KEY, GUIDE_LAUNCHPAD_GIFT_KEY } from '@/constants/account'; +import { checkUserTask, getPriceBonus, getUserTasks } from '@/api/platform'; +import MyIcon from '@/components/Icon'; +import { useGuideStore } from '@/store/guide'; import { formatMoney } from '@/utils/tools'; -import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; +import { Center, Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; import { DriveStep, driver } from '@sealos/driver'; +import { SealosCoin } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; import { useEffect, useMemo, useState } from 'react'; import { sealosApp } from 'sealos-desktop-sdk/app'; import { DriverStarIcon } from './useDriver'; -export default function useDriver() { +export default function useDetailDriver() { const { t, i18n } = useTranslation(); - const [showGiftStep, setShowGiftStep] = useState(false); - const [activity, setActivity] = useState({ - balance: 8, - limitDuration: '1', - amount: '8', - giftAmount: '8' - }); + const [reward, setReward] = useState(5); + const { detailCompleted, setDetailCompleted } = useGuideStore(); + + const [rechargeOptions, setRechargeOptions] = useState([ + { amount: 8, gift: 8 }, + { amount: 32, gift: 32 }, + { amount: 128, gift: 128 } + ]); + + const openCostCenterApp = () => { + sealosApp.runEvents('openDesktopApp', { + appKey: 'system-costcenter', + query: { + openRecharge: 'true' + } + }); + }; const PopoverBodyInfo = (props: FlexProps) => { return ( @@ -51,296 +63,250 @@ export default function useDriver() { const baseSteps: DriveStep[] = [ { - element: '.driver-detail-network-public', + element: '.driver-detail-monitor', popover: { - side: 'left', + side: 'bottom', align: 'start', - borderRadius: '12px 12px 0px 12px', + borderRadius: '0px 12px 12px 12px', PopoverBody: ( - - {t('Click here to visit the website')} + + {t('guide_detail_monitor')} - + ) } }, { - element: '.driver-detail-terminal', + element: '.driver-detail-update-button', popover: { - side: 'left', - align: 'center', + side: 'bottom', + align: 'start', borderRadius: '12px 12px 0px 12px', PopoverBody: ( - - {t('You can enter the container through the terminal')} + + {t('guide_detail_update_button')} - + ) } }, { - element: '.driver-detail-update-button', + element: '.driver-detail-network', popover: { - side: 'bottom', + side: 'left', align: 'start', borderRadius: '12px 12px 0px 12px', PopoverBody: ( - - {t('Adjust application configuration')} + + {t('guide_detail_network')} - + ) } - } - ]; - - const giftStep: DriveStep[] = [ + }, { + element: '.driver-detail-operate', popover: { - borderRadius: '12px 12px 12px 12px', + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', PopoverBody: ( - - - - {t('You have successfully deployed an application')} - - {t('receive')} - - {activity.balance} - - - - {t('Balance')} - - - - - - - - - - - - - - - - - - {t('First time completion guide benefits')} - - - - {t('gift time tip', { time: activity.limitDuration })} - - - {t('gift amount tip', { - amount: activity.amount, - gift: activity.giftAmount - })} - - - { - console.log('充值'); - driverObj.destroy(); - openCostCenterApp(); - }} - > - {t('Go to recharge')} - - { - driverObj.destroy(); - }} - > - {t('let me think again')} + + + + {t('guide_detail_operate')} + - ), - onPopoverRender: () => { - const svg = driverObj.getState('__overlaySvg'); - if (svg) { - const pathElement = svg.querySelector('path'); - if (pathElement) { - pathElement.style.pointerEvents = 'none'; - } - } - } + ) } } ]; - const driverConfig = useMemo(() => { - return { - disableActiveInteraction: true, - showProgress: false, - allowClose: false, - allowClickMaskNextStep: true, - allowPreviousStep: false, - isShowButtons: false, - allowKeyboardControl: false, - overlaySkipButton: t('skip') || 'skip', - steps: showGiftStep ? [...baseSteps, ...giftStep] : baseSteps, - onDestroyed: () => { - console.log('onDestroyed Detail'); - updateGuideStatus(); - }, - interceptSkipButtonClick: () => { - const skipButton = driverObj.getState('__overlaySkipBtn'); - skipButton?.remove(); - if (driverObj.isLastStep()) { - driverObj.destroy(); - } else { - driverObj.drive(3); - } - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showGiftStep]); + const giftStep: DriveStep[] = useMemo( + () => [ + { + popover: { + borderRadius: '12px 12px 12px 12px', + PopoverBody: ( + + + + + {t('You have successfully deployed an application')} + + + {t('receive')} + + + {reward} + + {t('Balance')} + + - const driverObj = driver(driverConfig); + + + + {t('first_charge')} + + - const openCostCenterApp = () => { - sealosApp.runEvents('openDesktopApp', { - appKey: 'system-costcenter', - query: { - openRecharge: 'true' + + {rechargeOptions.map((item, index) => ( +
+ + + {item.amount} + + + {t('gift')} + + {item.gift} + +
+ ))} +
+ + { + driverObj.destroy(); + openCostCenterApp(); + }} + > + {t('Go to recharge')} + + { + driverObj.destroy(); + }} + > + {t('let me think again')} + +
+ ) + } } - }); - }; + ], + [rechargeOptions, reward, t] + ); - const updateGuideStatus = () => { - updateDesktopGuide({ - activityType: 'beginner-guide', - phase: 'launchpad', - phasePage: 'detail', - shouldSendGift: false - }).catch((err) => { - console.log(err); - }); + const driverObj = driver({ + disableActiveInteraction: true, + showProgress: false, + allowClose: false, + allowClickMaskNextStep: true, + allowPreviousStep: false, + isShowButtons: false, + allowKeyboardControl: false, + steps: [...baseSteps, ...giftStep], + onDestroyed: () => { + console.log('onDestroyed Detail'); + setDetailCompleted(true); + checkUserTask().then((err) => { + console.log(err); + }); + }, + interceptSkipButtonClick: () => { + driverObj.destroy(); + } + }); + + const startGuide = () => { + driverObj.drive(); }; useEffect(() => { const handleUserGuide = async () => { try { - const { guideEnabled } = await getInitData(); - const userAccount = await getUserAccount(); - - const bonus = await getPriceBonus(); - if (bonus?.data?.activities) { - const strategy = JSON.parse(bonus.data?.activities); - const activity = { - balance: formatMoney( - strategy?.['beginner-guide']?.phases?.launchpad?.giveAmount || 8000000 - ), - limitDuration: strategy?.[ - 'beginner-guide' - ]?.phases?.launchpad?.RechargeDiscount?.limitDuration?.replace('h', ''), - amount: Object.entries( - strategy?.['beginner-guide']?.phases?.launchpad?.RechargeDiscount?.specialDiscount - )[0][0], - giftAmount: - Object.entries( - strategy?.['beginner-guide']?.phases?.launchpad?.RechargeDiscount?.specialDiscount - )[0][1] + '' - }; - setActivity(activity); - } - - if (guideEnabled && userAccount?.metadata?.annotations) { - const showGiftStep = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_GIFT_KEY]; - const isGuided = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_DETAIL_KEY]; - if (!isGuided) { - setShowGiftStep(showGiftStep); + const [taskData, bonusData] = await Promise.all([getUserTasks(), getPriceBonus()]); + if (taskData.needGuide && !detailCompleted) { + setReward(formatMoney(Number(taskData.task.reward))); + setRechargeOptions(bonusData); + requestAnimationFrame(() => { startGuide(); - } + }); } } catch (error) { console.log(error); } }; handleUserGuide(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const startGuide = () => { - driverObj.drive(); - }; - - const closeGuide = () => { - driverObj.destroy(); - }; - - return { startGuide, closeGuide }; + return { startGuide }; } diff --git a/frontend/providers/applaunchpad/src/hooks/useDriver.tsx b/frontend/providers/applaunchpad/src/hooks/useDriver.tsx index 85b6b1209ba..fcbd40fe68c 100644 --- a/frontend/providers/applaunchpad/src/hooks/useDriver.tsx +++ b/frontend/providers/applaunchpad/src/hooks/useDriver.tsx @@ -1,77 +1,48 @@ -import { getInitData, getUserAccount, updateDesktopGuide } from '@/api/platform'; -import { GUIDE_LAUNCHPAD_CREATE_KEY } from '@/constants/account'; +import { getUserTasks } from '@/api/platform'; +import { useGuideStore } from '@/store/guide'; +import { formatMoney } from '@/utils/tools'; import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; import { driver } from '@sealos/driver'; +import { SealosCoin } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; export function DriverStarIcon() { return ( - + - - - + + + ); } -export default function useDriver() { +export default function useDriver({ + setIsAdvancedOpen +}: { + setIsAdvancedOpen: (val: boolean) => void; +}) { const { t } = useTranslation(); - const [isGuided, setIsGuided] = useState(true); - - useEffect(() => { - const handleUserGuide = async () => { - try { - const { guideEnabled } = await getInitData(); - const userAccount = await getUserAccount(); - if (guideEnabled && userAccount?.metadata?.annotations) { - const isGuided = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_CREATE_KEY]; - if (!isGuided) { - startGuide(); - } - setIsGuided(isGuided); - } - } catch (error) { - console.log(error); - } - }; - handleUserGuide(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const updateGuideStatus = () => { - updateDesktopGuide({ - activityType: 'beginner-guide', - phase: 'launchpad', - phasePage: 'create', - shouldSendGift: false - }).catch((err) => { - console.log(err); - }); - }; + const [isGuided, setIsGuided] = useState(false); + const { createCompleted, setCreateCompleted } = useGuideStore(); + const [reward, setReward] = useState(1); const PopoverBodyInfo = (props: FlexProps) => { return ( @@ -112,6 +83,7 @@ export default function useDriver() { isShowButtons: false, allowKeyboardControl: false, overlaySkipButton: t('skip') || 'skip', + disableActiveInteraction: true, steps: [ { element: '.driver-deploy-image', @@ -122,7 +94,7 @@ export default function useDriver() { PopoverBody: ( - + {t('Can help you deploy any Docker image')} @@ -131,16 +103,16 @@ export default function useDriver() { } }, { - element: '.driver-deploy-instance', + element: '.driver-deploy-command', popover: { - side: 'top', - align: 'start', - borderRadius: '12px 12px 12px 0px', + side: 'right', + align: 'center', + borderRadius: '0px 12px 12px 12px', PopoverBody: ( - - {t('Configurable number of instances or automatic horizontal scaling')} + + {t('guide_deploy_command')} @@ -148,7 +120,7 @@ export default function useDriver() { } }, { - element: '.driver-deploy-network-switch', + element: '.driver-deploy-storage', popover: { side: 'top', align: 'start', @@ -156,8 +128,8 @@ export default function useDriver() { PopoverBody: ( - - {t('Second-level domain name tips')} + + {t('guide_deploy_storage')} @@ -171,37 +143,49 @@ export default function useDriver() { align: 'center', borderRadius: '12px 12px 0px 12px', PopoverBody: ( - + - - {t('Click the Deploy Application button')} - + {t('guide_deploy_button')} + {reward} + + {t('balance')} - ), - onPopoverRender: () => { - const svg = driverObj.getState('__overlaySvg'); - if (svg) { - const pathElement = svg.querySelector('path'); - if (pathElement) { - pathElement.style.pointerEvents = 'none'; - } - } - } + ) } } ], onDestroyed: () => { - updateGuideStatus(); + setCreateCompleted(true); } }); - const startGuide = () => { + const startGuide = useCallback(() => { driverObj.drive(); - }; + }, [driverObj]); const closeGuide = () => { driverObj.destroy(); }; - return { driverObj, startGuide, closeGuide, isGuided }; + useEffect(() => { + const handleUserGuide = async () => { + try { + const data = await getUserTasks(); + if (data.needGuide && !createCompleted) { + setReward(formatMoney(Number(data.task.reward))); + setIsAdvancedOpen(true); + setIsGuided(true); + requestAnimationFrame(() => { + startGuide(); + }); + } + } catch (error) { + setIsGuided(false); + } + }; + handleUserGuide(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { startGuide, closeGuide, isGuided }; } diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts b/frontend/providers/applaunchpad/src/pages/api/guide/checkTask.ts similarity index 66% rename from frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts rename to frontend/providers/applaunchpad/src/pages/api/guide/checkTask.ts index a20be083632..458be833a9b 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/checkTask.ts @@ -1,4 +1,4 @@ -import { authSession } from '@/services/backend/auth'; +import { authAppToken } from '@/services/backend/auth'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; import type { NextApiRequest, NextApiResponse } from 'next'; @@ -6,15 +6,21 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { if (!global.AppConfig.common.guideEnabled) return jsonRes(res, { data: null }); - const kubeconfig = await authSession(req.headers); - const domain = global.AppConfig.cloud.domain; + const token = await authAppToken(req.headers); + if (!token) { + return jsonRes(res, { code: 401, message: 'token is valid' }); + } + + const domain = global.AppConfig.cloud.desktopDomain; - const response = await fetch(`https://${domain}/api/v1alpha/account/getAccount`, { + const response = await fetch(`https://${domain}/api/account/checkTask`, { method: 'GET', headers: { - Authorization: encodeURIComponent(kubeconfig) + 'Content-Type': 'application/json', + Authorization: token } }); + const result: { code: number; data: any; diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts b/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts index 50dc5c8cea4..4ad97b2c826 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts @@ -1,22 +1,43 @@ -import { authSession } from '@/services/backend/auth'; -import { getK8s } from '@/services/backend/kubernetes'; +import { authAppToken } from '@/services/backend/auth'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - if (!global.AppConfig.common.guideEnabled) return jsonRes(res, { data: null }); - const { k8sCore, namespace } = await getK8s({ - kubeconfig: await authSession(req.headers) + const token = await authAppToken(req.headers); + if (!token) { + return jsonRes(res, { code: 401, message: '令牌无效' }); + } + + const url = global.AppConfig.launchpad.components.billing.url; + + const response = await fetch(`${url}/account/v1alpha1/recharge-discount`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}` + } }); - const result = await k8sCore.readNamespacedConfigMap('recharge-gift', 'sealos'); + const result: { + discount: { + firstRechargeDiscount: Record; + }; + } = await response.json(); + + const rechargeOptions = Object.entries(result.discount.firstRechargeDiscount).map( + ([amount, rate]) => ({ + amount: Number(amount), + gift: Math.floor((Number(amount) * Number(rate)) / 100) + }) + ); jsonRes(res, { - data: result.body + code: 200, + data: rechargeOptions }); } catch (err: any) { + console.log(err); jsonRes(res, { code: 500, error: err diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts b/frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts new file mode 100644 index 00000000000..9321febd240 --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts @@ -0,0 +1,55 @@ +import { authAppToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { UserTask } from '@/types/user'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (!global.AppConfig.common.guideEnabled) + return jsonRes(res, { + data: { + needGuide: false + } + }); + + const token = await authAppToken(req.headers); + if (!token) { + return jsonRes(res, { code: 401, message: 'token is valid' }); + } + + const domain = global.AppConfig.cloud.desktopDomain; + const response = await fetch(`https://${domain}/api/account/getTasks`, { + method: 'GET', + headers: { + Authorization: token + } + }); + const result: { + code: number; + data: UserTask[]; + message: string; + } = await response.json(); + + if (result.code !== 200) { + return jsonRes(res, { + code: 500, + message: 'desktop api is err' + }); + } + + const launchpadTask = result.data.find((task) => task.taskType === 'LAUNCHPAD'); + const needGuide = launchpadTask ? !launchpadTask.isCompleted : false; + + jsonRes(res, { + code: 200, + data: { needGuide, task: launchpadTask } + }); + } catch (err: any) { + console.log(err); + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts b/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts deleted file mode 100644 index 7d46c77c43a..00000000000 --- a/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { authSession } from '@/services/backend/auth'; -import { jsonRes } from '@/services/backend/response'; -import { ApiResp } from '@/services/kubernet'; -import type { NextApiRequest, NextApiResponse } from 'next'; - -export type UpdateUserGuideParams = { - activityType: 'beginner-guide'; - phase: 'launchpad' | 'database' | 'template' | 'terminal'; - phasePage: 'create' | 'detail' | 'index'; - shouldSendGift: boolean; -}; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (!global.AppConfig.common.guideEnabled) return jsonRes(res, { data: null }); - const { activityType, phase, phasePage, shouldSendGift } = req.body as UpdateUserGuideParams; - - if (!activityType || !phase || !phasePage) - return jsonRes(res, { code: 400, message: 'Bad Request: Invalid parameters' }); - const kubeconfig = await authSession(req.headers); - const domain = global.AppConfig.cloud.domain; - - const payload = { - activityType, - phase, - phasePage, - shouldSendGift - }; - - const response = await fetch(`https://${domain}/api/v1alpha/account/updateGuide`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: encodeURIComponent(kubeconfig) - }, - body: JSON.stringify(payload) - }); - - const result: { - code: number; - data: any; - message: string; - } = await response.json(); - - if (result.code !== 200) { - return jsonRes(res, { code: result.code, message: 'desktop api is err' }); - } else { - return jsonRes(res, { data: result.data }); - } - } catch (err: any) { - console.log(err); - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx index 89f98aae550..d5ad98246bb 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx @@ -238,21 +238,6 @@ const AppBaseInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { {t(item.label)} - - copyData(item.value)} - cursor={'pointer'} - > - {t(item.value)} - - ))} {/* env */} diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx index 7b050b6afea..53efbbae50b 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx @@ -59,6 +59,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { color={'grayModern.600'} fontWeight={'bold'} position={'relative'} + className="driver-detail-monitor" >