From e91bce93bb139b05800cc8c155233792c18bb840 Mon Sep 17 00:00:00 2001 From: jingyang <72259332+zjy365@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:37:45 +0800 Subject: [PATCH] fix:permission change check for launchpad (#5072) * update desktop billing * add checkPermission api * Change Location * update --- .../src/pages/api/desktop/getBilling.ts | 13 ++- .../applaunchpad/src/api/platform.ts | 4 +- .../src/pages/api/platform/checkPermission.ts | 79 +++++++++++++++++++ .../src/pages/api/platform/getInitData.ts | 5 +- .../src/pages/api/platform/getQuota.ts | 70 +--------------- .../src/pages/api/platform/resourcePrice.ts | 11 ++- .../applaunchpad/src/pages/app/edit/index.tsx | 35 +++++--- .../providers/applaunchpad/src/store/user.ts | 2 - .../applaunchpad/src/types/index.d.ts | 3 + 9 files changed, 128 insertions(+), 94 deletions(-) create mode 100644 frontend/providers/applaunchpad/src/pages/api/platform/checkPermission.ts diff --git a/frontend/desktop/src/pages/api/desktop/getBilling.ts b/frontend/desktop/src/pages/api/desktop/getBilling.ts index 1546bd0468c..1fc5e85c372 100644 --- a/frontend/desktop/src/pages/api/desktop/getBilling.ts +++ b/frontend/desktop/src/pages/api/desktop/getBilling.ts @@ -5,6 +5,11 @@ import { jsonRes } from '@/services/backend/response'; import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; import type { NextApiRequest, NextApiResponse } from 'next'; +type ConsumptionResult = { + allAmount: number; + regionAmount: Record; +}; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const payload = await verifyAccessToken(req.headers); @@ -25,9 +30,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) timeOneMonthAgo.setMonth(currentTime.getMonth() - 1); const base = global.AppConfig.desktop.auth.billingUrl as string; - const consumptionUrl = base + '/account/v1alpha1/costs/consumption'; + const consumptionUrl = base + '/account/v1alpha1/costs/all-region-consumption'; - const results = await Promise.all([ + const results: ConsumptionResult[] = await Promise.all([ ( await fetch(consumptionUrl, { method: 'POST', @@ -56,8 +61,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) jsonRes(res, { data: { - prevMonthTime: results[0].amount || 0, - prevDayTime: results[1].amount || 0 + prevMonthTime: results[0].allAmount || 0, + prevDayTime: results[1].allAmount || 0 } }); } catch (err: any) { diff --git a/frontend/providers/applaunchpad/src/api/platform.ts b/frontend/providers/applaunchpad/src/api/platform.ts index 27c19f55ce1..98f0b42dd72 100644 --- a/frontend/providers/applaunchpad/src/api/platform.ts +++ b/frontend/providers/applaunchpad/src/api/platform.ts @@ -11,7 +11,6 @@ export const getInitData = () => GET('/api/platform/getInitData'); export const getUserQuota = () => GET<{ - balance: string; quota: UserQuotaItemType[]; }>('/api/platform/getQuota'); @@ -26,3 +25,6 @@ export const updateDesktopGuide = (payload: UpdateUserGuideParams) => export const getUserAccount = () => GET('/api/guide/getAccount'); export const getPriceBonus = () => GET('/api/guide/getBonus'); + +export const checkPermission = (payload: { appName: string; resourceType: 'deploy' | 'sts' }) => + GET('/api/platform/checkPermission', payload); diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/checkPermission.ts b/frontend/providers/applaunchpad/src/pages/api/platform/checkPermission.ts new file mode 100644 index 00000000000..7055a10983e --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/api/platform/checkPermission.ts @@ -0,0 +1,79 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { ApiResp } from '@/services/kubernet'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import * as k8s from '@kubernetes/client-node'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { appName, resourceType } = req.query as { + appName: string; + resourceType: 'deploy' | 'sts'; + }; + + if (!appName || !resourceType) { + throw new Error('appName or resourceType is empty'); + } + + const { k8sApp, namespace } = await getK8s({ + kubeconfig: await authSession(req.headers) + }); + + const patchBody = { + metadata: { + annotations: { + 'update-time': new Date().toISOString() + } + } + }; + + const options = { + headers: { 'Content-type': k8s.PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH } + }; + + if (resourceType === 'deploy') { + await k8sApp.patchNamespacedDeployment( + appName, + namespace, + patchBody, + undefined, + undefined, + undefined, + undefined, + undefined, + options + ); + } else if (resourceType === 'sts') { + await k8sApp.patchNamespacedStatefulSet( + appName, + namespace, + patchBody, + undefined, + undefined, + undefined, + undefined, + undefined, + options + ); + } + + jsonRes(res, { + code: 200, + data: 'success' + }); + } catch (err: any) { + if (err?.body?.code === 403 && err?.body?.message.includes('40001')) { + return jsonRes(res, { + code: 200, + data: 'insufficient_funds', + message: err?.body?.message + }); + } + + jsonRes(res, { + code: 500, + error: err?.body + }); + } +} diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts index fd1157a4595..ecacaac51f7 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts @@ -37,6 +37,9 @@ export const defaultAppConfig: AppConfigType = { components: { monitor: { url: 'http://launchpad-monitor.sealos.svc.cluster.local:8428' + }, + billing: { + url: 'http://account-service.account-system.svc:2333' } }, appResourceFormSliderConfig: { @@ -62,7 +65,7 @@ process.on('uncaughtException', (err) => { export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - if (!global.AppConfig || process.env.NODE_ENV !== 'production') { + if (!global.AppConfig) { const filename = process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; const res: any = yaml.load(readFileSync(filename, 'utf-8')); diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/getQuota.ts b/frontend/providers/applaunchpad/src/pages/api/platform/getQuota.ts index 1093cb687ba..ad81507bcb1 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/getQuota.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/getQuota.ts @@ -2,61 +2,8 @@ import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; import { UserQuotaItemType } from '@/types/user'; -import Decimal from 'decimal.js'; import type { NextApiRequest, NextApiResponse } from 'next'; -async function getAmount(req: NextApiRequest): Promise<{ - data?: { - balance: number; - deductionBalance: number; - }; -}> { - const domain = global.AppConfig.cloud.domain; - const base = `https://account-api.${domain}`; - - if (!base) throw Error('not base url'); - const { kube_user, kc } = await getK8s({ - kubeconfig: await authSession(req.headers) - }); - - if (kube_user === null) { - return { data: undefined }; - } - - const body = JSON.stringify({ - kubeConfig: kc.exportConfig() - }); - - const response = await fetch(base + '/account/v1alpha1/account', { - method: 'POST', - body, - headers: { - 'Content-Type': 'application/json' - } - }); - - const data = (await response.json()) as { - account?: { - UserUID: string; - ActivityBonus: number; - EncryptBalance: string; - EncryptDeductionBalance: string; - CreatedAt: Date; - Balance: number; - DeductionBalance: number; - }; - }; - - if (!kc || !data?.account) return { data: undefined }; - - return { - data: { - balance: data.account.Balance, - deductionBalance: data.account.DeductionBalance - } - }; -} - export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { // source price @@ -68,26 +15,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const gpuEnabled = global.AppConfig.common.gpuEnabled; const filteredQuota = gpuEnabled ? quota : quota.filter((item) => item.type !== 'gpu'); - let balance = '0'; - try { - const { data } = await getAmount(req); - if (data) { - balance = new Decimal(data.balance) - .minus(new Decimal(data.deductionBalance)) - .dividedBy(1000000) - .toFixed(2); - } - } catch (error) { - console.log(error, 'getAmount Error'); - } - jsonRes<{ - balance: string; quota: UserQuotaItemType[]; }>(res, { data: { - quota: filteredQuota, - balance + quota: filteredQuota } }); } catch (error) { diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/resourcePrice.ts b/frontend/providers/applaunchpad/src/pages/api/platform/resourcePrice.ts index 3e81c01ee47..8f0f54ec0f9 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/resourcePrice.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/resourcePrice.ts @@ -142,12 +142,11 @@ function countGpuSource(rawData: ResourcePriceType['data']['properties'], gpuNod } const getResourcePrice = async () => { - const res = await fetch( - `https://account-api.${global.AppConfig.cloud.domain}/account/v1alpha1/properties`, - { - method: 'POST' - } - ); + const url = global.AppConfig.launchpad.components.billing.url; + + const res = await fetch(`${url}/account/v1alpha1/properties`, { + method: 'POST' + }); const data: ResourcePriceType = await res.json(); return data.data.properties; diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx index cb4f319fb21..bad738f33ab 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx @@ -1,5 +1,5 @@ import { postDeployApp, putApp } from '@/api/app'; -import { updateDesktopGuide } from '@/api/platform'; +import { checkPermission, updateDesktopGuide } from '@/api/platform'; import { defaultSliderKey } from '@/constants/app'; import { defaultEditVal, editModeMap } from '@/constants/editApp'; import { useConfirm } from '@/hooks/useConfirm'; @@ -98,7 +98,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => const [forceUpdate, setForceUpdate] = useState(false); const { setAppDetail } = useAppStore(); const { screenWidth, formSliderListConfig } = useGlobalStore(); - const { userSourcePrice, loadUserSourcePrice, checkQuotaAllow, balance } = useUserStore(); + const { userSourcePrice, loadUserSourcePrice, checkQuotaAllow } = useUserStore(); const { title, applyBtnText, applyMessage, applySuccess, applyError } = editModeMap(!!appName); const [yamlList, setYamlList] = useState([]); const [errorMessage, setErrorMessage] = useState(''); @@ -320,18 +320,10 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => applyBtnText={applyBtnText} applyCb={() => { closeGuide(); - formHook.handleSubmit((data) => { + formHook.handleSubmit(async (data) => { const parseYamls = formData2Yamls(data); setYamlList(parseYamls); - // balance check - if (balance <= 0) { - return toast({ - status: 'warning', - title: t('user.Insufficient account balance') - }); - } - // gpu inventory check if (data.gpu?.type) { const inventory = countGpuInventory(data.gpu?.type); @@ -362,6 +354,27 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => }); } + // check permission + if (appName) { + try { + const result = await checkPermission({ + appName: data.appName, + resourceType: !!data.storeList?.length ? 'sts' : 'deploy' + }); + if (result === 'insufficient_funds') { + return toast({ + status: 'warning', + title: t('user.Insufficient account balance') + }); + } + } catch (error: any) { + return toast({ + status: 'warning', + title: error + }); + } + } + openConfirm(() => submitSuccess(parseYamls))(); }, submitError)(); }} diff --git a/frontend/providers/applaunchpad/src/store/user.ts b/frontend/providers/applaunchpad/src/store/user.ts index c9eaad14b2e..9936095d56d 100644 --- a/frontend/providers/applaunchpad/src/store/user.ts +++ b/frontend/providers/applaunchpad/src/store/user.ts @@ -7,7 +7,6 @@ import type { userPriceType } from '@/types/user'; import { AppEditType } from '@/types/app'; type State = { - balance: number; userQuota: UserQuotaItemType[]; loadUserQuota: () => Promise; userSourcePrice: userPriceType | undefined; @@ -45,7 +44,6 @@ export const useUserStore = create()( const response = await getUserQuota(); set((state) => { state.userQuota = response.quota; - state.balance = parseFloat(response.balance); }); return null; }, diff --git a/frontend/providers/applaunchpad/src/types/index.d.ts b/frontend/providers/applaunchpad/src/types/index.d.ts index 32b0a20f60a..e886c2fdb40 100644 --- a/frontend/providers/applaunchpad/src/types/index.d.ts +++ b/frontend/providers/applaunchpad/src/types/index.d.ts @@ -43,6 +43,9 @@ export type AppConfigType = { monitor: { url: string; }; + billing: { + url: string; + }; }; appResourceFormSliderConfig: FormSliderListType; fileManger: FileMangerType;