From c39b6a2c4f5ee89a3c899f7327670cd5ad744b68 Mon Sep 17 00:00:00 2001 From: zhujingyang <72259332+zjy365@users.noreply.github.com> Date: Mon, 25 Sep 2023 10:45:09 +0800 Subject: [PATCH] feat: license desktop page (#3969) * feat license desktop page Signed-off-by: jingyang <3161362058@qq.com> * delete log --------- Signed-off-by: jingyang <3161362058@qq.com> --- frontend/desktop/{.env => .env.template} | 6 +- frontend/desktop/public/icons/download.svg | 3 + frontend/desktop/public/icons/empty.svg | 5 + frontend/desktop/public/icons/license.svg | 3 + frontend/desktop/public/icons/pay_wechat.svg | 1 + frontend/desktop/public/icons/shell_coin.svg | 3 + frontend/desktop/public/icons/stripe.svg | 3 + frontend/desktop/public/icons/token.svg | 10 + frontend/desktop/public/icons/wechat.svg | 4 + .../desktop/public/locales/en/common.json | 11 +- .../public/locales/zh-Hans/common.json | 43 --- .../desktop/public/locales/zh/common.json | 11 +- .../src/components/LangSelect/simple.tsx | 43 +++ .../components/signin/auth/useLanguage.tsx | 12 +- .../components/signin/auth/useProtocol.tsx | 2 +- .../src/components/user_menu/index.tsx | 33 ++- frontend/desktop/src/hooks/useBonusBox.tsx | 76 ++++++ .../pages/api/license/createLicenseRecord.ts | 31 +++ .../src/pages/api/license/getLicenseRecord.ts | 29 ++ frontend/desktop/src/pages/api/license/pay.ts | 83 ++++++ .../desktop/src/pages/api/license/result.ts | 32 +++ .../desktop/src/pages/api/platform/getEnv.ts | 13 +- frontend/desktop/src/pages/api/price/bonus.ts | 28 ++ frontend/desktop/src/pages/api/price/index.ts | 52 ++++ .../src/pages/api/system/getSystemConfig.ts | 1 - .../license/components/CurrencySymbol.tsx | 22 ++ .../pages/license/components/OuterLink.tsx | 35 +++ .../pages/license/components/Pagination.tsx | 118 ++++++++ .../src/pages/license/components/Recharge.tsx | 258 ++++++++++++++++++ .../src/pages/license/components/Record.tsx | 123 +++++++++ .../license/components/WechatPayment.tsx | 60 ++++ frontend/desktop/src/pages/license/index.tsx | 75 +++++ .../src/services/backend/db/license.ts | 72 +++++ frontend/desktop/src/services/enable.ts | 8 + frontend/desktop/src/styles/globals.scss | 19 ++ frontend/desktop/src/types/index.ts | 2 + frontend/desktop/src/types/license.ts | 63 +++++ frontend/desktop/src/types/system.ts | 3 + frontend/desktop/src/types/valuation.ts | 18 ++ frontend/desktop/src/utils/format.ts | 16 ++ frontend/providers/license/.env.template | 4 +- .../license/deploy/manifests/appcr.yaml.tmpl | 6 +- .../license/deploy/manifests/deploy.yaml.tmpl | 25 +- .../deploy/manifests/ingress.yaml.tmpl | 4 +- frontend/providers/license/public/favicon.ico | Bin 4286 -> 4286 bytes .../license/public/icons/license-bg.svg | 17 -- .../license/public/icons/license-sealos.svg | 21 ++ .../license/public/locales/en/common.json | 7 +- .../license/public/locales/zh/common.json | 9 +- frontend/providers/license/public/logo.svg | 4 +- .../src/components/FileSelect/index.tsx | 5 +- .../src/components/Pagination/index.tsx | 2 +- .../providers/license/src/constants/theme.ts | 1 - frontend/providers/license/src/pages/_app.tsx | 2 +- .../src/pages/api/license/getLicense.tsx | 8 +- .../license/src/pages/api/platform/getEnv.ts | 5 +- .../providers/license/src/pages/index.tsx | 220 ++++++++++----- .../providers/license/src/utils/crypto.ts | 7 + .../license/src/utils/downloadFIle.ts | 14 + .../providers/license/src/utils/json2Yaml.ts | 14 +- frontend/providers/license/src/utils/user.ts | 3 +- 61 files changed, 1612 insertions(+), 196 deletions(-) rename frontend/desktop/{.env => .env.template} (88%) create mode 100644 frontend/desktop/public/icons/download.svg create mode 100644 frontend/desktop/public/icons/empty.svg create mode 100644 frontend/desktop/public/icons/license.svg create mode 100644 frontend/desktop/public/icons/pay_wechat.svg create mode 100644 frontend/desktop/public/icons/shell_coin.svg create mode 100644 frontend/desktop/public/icons/stripe.svg create mode 100644 frontend/desktop/public/icons/token.svg create mode 100644 frontend/desktop/public/icons/wechat.svg delete mode 100644 frontend/desktop/public/locales/zh-Hans/common.json create mode 100644 frontend/desktop/src/components/LangSelect/simple.tsx create mode 100644 frontend/desktop/src/hooks/useBonusBox.tsx create mode 100644 frontend/desktop/src/pages/api/license/createLicenseRecord.ts create mode 100644 frontend/desktop/src/pages/api/license/getLicenseRecord.ts create mode 100644 frontend/desktop/src/pages/api/license/pay.ts create mode 100644 frontend/desktop/src/pages/api/license/result.ts create mode 100644 frontend/desktop/src/pages/api/price/bonus.ts create mode 100644 frontend/desktop/src/pages/api/price/index.ts create mode 100644 frontend/desktop/src/pages/license/components/CurrencySymbol.tsx create mode 100644 frontend/desktop/src/pages/license/components/OuterLink.tsx create mode 100644 frontend/desktop/src/pages/license/components/Pagination.tsx create mode 100644 frontend/desktop/src/pages/license/components/Recharge.tsx create mode 100644 frontend/desktop/src/pages/license/components/Record.tsx create mode 100644 frontend/desktop/src/pages/license/components/WechatPayment.tsx create mode 100644 frontend/desktop/src/pages/license/index.tsx create mode 100644 frontend/desktop/src/services/backend/db/license.ts create mode 100644 frontend/desktop/src/types/license.ts create mode 100644 frontend/desktop/src/types/valuation.ts create mode 100644 frontend/providers/license/public/icons/license-sealos.svg create mode 100644 frontend/providers/license/src/utils/crypto.ts create mode 100644 frontend/providers/license/src/utils/downloadFIle.ts diff --git a/frontend/desktop/.env b/frontend/desktop/.env.template similarity index 88% rename from frontend/desktop/.env rename to frontend/desktop/.env.template index 3de62b3b9f2..53db89856ce 100644 --- a/frontend/desktop/.env +++ b/frontend/desktop/.env.template @@ -20,4 +20,8 @@ SEALOS_CLOUD_DOMAIN="cloud.sealos.io" # WECHAT_ENABLED= # GITHUB_ENABLED= # PASSWORD_ENABLED= -# SMS_ENABLED= \ No newline at end of file +# SMS_ENABLED= + +# costcenter +STRIPE_ENABLED= +STRIPE_PUB= \ No newline at end of file diff --git a/frontend/desktop/public/icons/download.svg b/frontend/desktop/public/icons/download.svg new file mode 100644 index 00000000000..8aed8eea5b5 --- /dev/null +++ b/frontend/desktop/public/icons/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/empty.svg b/frontend/desktop/public/icons/empty.svg new file mode 100644 index 00000000000..f213a801161 --- /dev/null +++ b/frontend/desktop/public/icons/empty.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/desktop/public/icons/license.svg b/frontend/desktop/public/icons/license.svg new file mode 100644 index 00000000000..9893206ace4 --- /dev/null +++ b/frontend/desktop/public/icons/license.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/pay_wechat.svg b/frontend/desktop/public/icons/pay_wechat.svg new file mode 100644 index 00000000000..ecab7b8e6a8 --- /dev/null +++ b/frontend/desktop/public/icons/pay_wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/desktop/public/icons/shell_coin.svg b/frontend/desktop/public/icons/shell_coin.svg new file mode 100644 index 00000000000..759c769a0a0 --- /dev/null +++ b/frontend/desktop/public/icons/shell_coin.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/stripe.svg b/frontend/desktop/public/icons/stripe.svg new file mode 100644 index 00000000000..219cbed9b96 --- /dev/null +++ b/frontend/desktop/public/icons/stripe.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/desktop/public/icons/token.svg b/frontend/desktop/public/icons/token.svg new file mode 100644 index 00000000000..b0dd76f35a8 --- /dev/null +++ b/frontend/desktop/public/icons/token.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/desktop/public/icons/wechat.svg b/frontend/desktop/public/icons/wechat.svg new file mode 100644 index 00000000000..b713640cbdf --- /dev/null +++ b/frontend/desktop/public/icons/wechat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 87fff97cd87..e4642f2bff4 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -79,5 +79,14 @@ "Dissovle Tips": "Dissolving the team will clear all resources. Are you sure you want to disband", "Enter Confirm.": "Please enter {{value}} to confirm", "Accept Invitation": "Accept Invitation", - "Recive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}" + "Recive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}", + "pay with stripe": "Pay With Stripe", + "pay with wechat": "Pay With Wechat", + "License Buy": "License Buy", + "Purchase History": "Purchase History", + "Purchase License": "Purchase License", + "Remaining Time": "Remaining Time: ", + "Please read and agree to the agreement": "Please read and agree to the agreement", + "Purchase Link Error": "Purchase Link Error", + "You have not purchased the License": "You have not purchased the License" } \ No newline at end of file diff --git a/frontend/desktop/public/locales/zh-Hans/common.json b/frontend/desktop/public/locales/zh-Hans/common.json deleted file mode 100644 index 66aecacbec8..00000000000 --- a/frontend/desktop/public/locales/zh-Hans/common.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "More Apps": "更多应用", - "Message Center": "消息中心", - "Have Read": "已读", - "Unread": "未读", - "Read All": "全部已读", - "Username": "用户名", - "Log In": "登录", - "Loading": "加载中", - "Log Out": "退出账号", - "From": "来自", - "Balance": "余额", - "verify code tips": "6位验证码", - "phone number tips": "手机号码", - "Password Login":"密码登录", - "Password":"密码", - "Verify password": "确认密码", - "Verification Code Login":"手机号登录", - "password tips": "密码为8位以上字符", - "username tips":"用户名为3-16位的英文或数字的字符", - "Invalid username or password": "用户名或密码错误", - "Invalid phone number":"无效的手机号码", - "Invalid verification code": "无效的验证码", - "Get code failed": "获取验证码失败", - "Read and agree": "请阅读并同意下方协议", - "agree policy": "我已阅读并同意", - "and": "和", - "Service Agreement": "服务协议", - "Privacy Policy": "隐私政策", - "Get Code": "获取验证码", - "Bonus": "赠", - "Payment Result": "支付结果", - "Payment Successful": "支付成功", - "In Payment": "支付中 ...", - "Recharge Amount": "充值金额", - "Select Amount": "选择金额", - "View Discount Rules": "查看优惠规则", - "Payment Status": "支付状态", - "Scan with WeChat": "微信扫码支付", - "Charge": "充值", - "Order Number": "订单号", - "Confirm": "确认" -} diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index ffabefbf910..47f45e1c335 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -73,5 +73,14 @@ "Dissovle Tips": "解散团队会清空所有资源,确定要解散吗?", "Enter Confirm.": "请输入 {{value}} 确认", "Accept Invitation": "接受邀请", - "Recive Tips": "{{managerName}} 邀请你到 {{teamName}} 当 {{role}}" + "Recive Tips": "{{managerName}} 邀请你到 {{teamName}} 当 {{role}}", + "pay with wechat": "微信支付", + "pay with stripe": "Stripe 支付", + "License Buy": "License 购买", + "Purchase History": "购买记录", + "Purchase License": "购买 License", + "Remaining Time": "剩余激活时间: ", + "Please read and agree to the agreement": "请阅读并同意协议", + "Purchase Link Error": "购买链接错误", + "You have not purchased the License": "您还没有购买 License" } \ No newline at end of file diff --git a/frontend/desktop/src/components/LangSelect/simple.tsx b/frontend/desktop/src/components/LangSelect/simple.tsx new file mode 100644 index 00000000000..75cd047114a --- /dev/null +++ b/frontend/desktop/src/components/LangSelect/simple.tsx @@ -0,0 +1,43 @@ +import { setCookie } from '@/utils/cookieUtils'; +import { Flex, FlexProps } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { EVENT_NAME } from 'sealos-desktop-sdk'; +import { masterApp } from 'sealos-desktop-sdk/master'; + +export default function LangSelectSimple(props: FlexProps) { + const { t, i18n } = useTranslation(); + + return ( + { + masterApp?.sendMessageToAll({ + apiName: 'event-bus', + eventName: EVENT_NAME.CHANGE_I18N, + data: { + currentLanguage: i18n?.language === 'en' ? 'zh' : 'en' + } + }); + setCookie('NEXT_LOCALE', i18n?.language === 'en' ? 'zh' : 'en', { + expires: 30, + sameSite: 'None', + secure: true + }); + i18n?.changeLanguage(i18n?.language === 'en' ? 'zh' : 'en'); + }} + > + {i18n?.language === 'en' ? 'En' : '中'} + + ); +} diff --git a/frontend/desktop/src/components/signin/auth/useLanguage.tsx b/frontend/desktop/src/components/signin/auth/useLanguage.tsx index 910bcbab7f4..2a953dec0dc 100644 --- a/frontend/desktop/src/components/signin/auth/useLanguage.tsx +++ b/frontend/desktop/src/components/signin/auth/useLanguage.tsx @@ -1,4 +1,5 @@ import LangSelect from '@/components/LangSelect'; +import LangSelectSimple from '@/components/LangSelect/simple'; import { Box, Flex, UseDisclosureReturn } from '@chakra-ui/react'; import { I18n } from 'next-i18next'; @@ -14,19 +15,12 @@ const Language = ({ disclosure, i18n }: LanguageType) => { cursor={'pointer'} gap={'16px'} > - - disclosure.onOpen()}>{i18n?.language === 'en' ? 'en' : '中'} - - + /> ); }; diff --git a/frontend/desktop/src/components/signin/auth/useProtocol.tsx b/frontend/desktop/src/components/signin/auth/useProtocol.tsx index d34f5ca560f..36574dab25c 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 } from '@chakra-ui/react'; +import { Checkbox, Flex, Link, Text, TextProps } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; diff --git a/frontend/desktop/src/components/user_menu/index.tsx b/frontend/desktop/src/components/user_menu/index.tsx index 7ee95d09508..8eac640810d 100644 --- a/frontend/desktop/src/components/user_menu/index.tsx +++ b/frontend/desktop/src/components/user_menu/index.tsx @@ -1,10 +1,9 @@ import Account from '@/components/account'; import Notification from '@/components/notification'; import useSessionStore from '@/stores/session'; -import { Box, Flex, Image, useDisclosure } from '@chakra-ui/react'; -import { i18n } from 'next-i18next'; +import { Box, Flex, FlexProps, Image, useDisclosure } from '@chakra-ui/react'; import { useState } from 'react'; -import LangSelect from '../LangSelect'; +import LangSelectSimple from '../LangSelect/simple'; import Iconfont from '../iconfont'; enum UserMenuKeys { @@ -13,25 +12,31 @@ enum UserMenuKeys { Account } -export default function Index() { +export default function Index(props: { userMenuStyleProps?: FlexProps }) { const [notificationAmount, setNotificationAmount] = useState(0); const accountDisclosure = useDisclosure(); const showDisclosure = useDisclosure(); const switchLangDisclosure = useDisclosure(); const userInfo = useSessionStore((state) => state.getSession()); if (!userInfo) return null; + + const { + userMenuStyleProps = { + alignItems: 'center', + position: 'absolute', + top: '42px', + right: '42px', + cursor: 'pointer', + gap: '16px' + } + } = props; + const buttonList: { click?: () => void; button: JSX.Element; content: JSX.Element; key: UserMenuKeys; }[] = [ - { - key: UserMenuKeys.LangSelect, - button: {i18n?.language === 'en' ? 'en' : '中'}, - click: () => switchLangDisclosure.onOpen(), - content: - }, { key: UserMenuKeys.Notification, button: ( @@ -63,7 +68,13 @@ export default function Index() { } ]; return ( - + + {buttonList.map((item, index) => ( + request.get< + any, + ApiResp<{ + steps: string; + ratios: string; + }> + >('/api/price/bonus'), + {} + ); + + const { ratios, steps } = useMemo(() => { + return { + ratios: (bonuses?.data?.ratios || '').split(',').map((v) => +v), + steps: (bonuses?.data?.steps || '').split(',').map((v) => +v) + }; + }, [bonuses?.data]); + + const BonusBox = useCallback( + () => ( + + {steps.map((amount, index) => ( + { + e.preventDefault(); + setSelectAmountIndex(index); + }} + > + + + + {amount} + + + + ))} + + ), + [selectAmountIndex, steps] + ); + + return { + BonusBox, + selectAmount: steps[selectAmountIndex] + }; +} diff --git a/frontend/desktop/src/pages/api/license/createLicenseRecord.ts b/frontend/desktop/src/pages/api/license/createLicenseRecord.ts new file mode 100644 index 00000000000..86aa73f8a32 --- /dev/null +++ b/frontend/desktop/src/pages/api/license/createLicenseRecord.ts @@ -0,0 +1,31 @@ +import { authSession } from '@/services/backend/auth'; +import { createLicenseRecord } from '@/services/backend/db/license'; +import { jsonRes } from '@/services/backend/response'; +import { LicensePayload } from '@/types'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const payload = await authSession(req.headers); + if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' }); + + const { token, orderID, amount, quota, paymentMethod } = req.body as LicensePayload; + + const record = { + uid: payload.user.nsid, + amount: amount, + token: token, + orderID: orderID, + quota: quota, + paymentMethod: paymentMethod + }; + + const result = await createLicenseRecord(record); + + return jsonRes(resp, { + data: result + }); + } catch (error) { + jsonRes(resp, { code: 500, data: error }); + } +} diff --git a/frontend/desktop/src/pages/api/license/getLicenseRecord.ts b/frontend/desktop/src/pages/api/license/getLicenseRecord.ts new file mode 100644 index 00000000000..cf18b889010 --- /dev/null +++ b/frontend/desktop/src/pages/api/license/getLicenseRecord.ts @@ -0,0 +1,29 @@ +import { authSession } from '@/services/backend/auth'; +import { createLicenseRecord, getLicenseRecordsByUid } from '@/services/backend/db/license'; +import { jsonRes } from '@/services/backend/response'; +import { LicensePayload } from '@/types'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const payload = await authSession(req.headers); + if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' }); + + const { page = 1, pageSize = 10 } = req.body as { + page: number; + pageSize: number; + }; + + const result = await getLicenseRecordsByUid({ + uid: payload.user.nsid, + page: page, + pageSize: pageSize + }); + + return jsonRes(resp, { + data: result + }); + } catch (error) { + jsonRes(resp, { code: 500, data: error }); + } +} diff --git a/frontend/desktop/src/pages/api/license/pay.ts b/frontend/desktop/src/pages/api/license/pay.ts new file mode 100644 index 00000000000..11efd922ca1 --- /dev/null +++ b/frontend/desktop/src/pages/api/license/pay.ts @@ -0,0 +1,83 @@ +import { authSession } from '@/services/backend/auth'; +import { ApplyYaml } from '@/services/backend/kubernetes/user'; +import { jsonRes } from '@/services/backend/response'; +import { LicensePaymentForm } from '@/types'; +import yaml from 'js-yaml'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export const generateLicenseCrd = (form: LicensePaymentForm) => { + const paymentCrd = { + apiVersion: 'infostream.sealos.io/v1', + kind: 'Payment', + metadata: { + name: form.paymentName, + namespace: form.namespace + }, + spec: { + userID: form.userId, + amount: form.amount, // weixin + paymentMethod: form.paymentMethod, + service: { + amt: form.quota, // Actual value + hid: form.hashID + } + } + }; + try { + const result = yaml.dump(paymentCrd); + return result; + } catch (error) { + throw error; + } +}; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const payload = await authSession(req.headers); + if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' }); + + const { amount, paymentMethod, hid, quota } = req.body as { + amount: number; + paymentMethod: 'wechat' | 'stripe'; + hid: string; + quota: number; + }; + console.log(amount, quota, paymentMethod, hid); + if (!hid) { + return jsonRes(resp, { + code: 400, + message: 'Missing hid parameter' + }); + } + if (amount <= 0) { + return jsonRes(resp, { + code: 400, + message: 'Amount cannot be less than 0' + }); + } + + const paymentName = crypto.randomUUID(); + const form: LicensePaymentForm = { + namespace: payload.user.nsid, + paymentName: paymentName, + userId: payload.user.k8s_username, + amount: amount, + quota: quota, + paymentMethod: paymentMethod, + hashID: hid + }; + + const LicenseCrd = generateLicenseCrd(form); + + const res = await ApplyYaml(payload.kc, LicenseCrd); + return jsonRes(resp, { + data: { + paymentName: paymentName, + extra: res[0] + } + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, data: error }); + } +} diff --git a/frontend/desktop/src/pages/api/license/result.ts b/frontend/desktop/src/pages/api/license/result.ts new file mode 100644 index 00000000000..a51198f2d9b --- /dev/null +++ b/frontend/desktop/src/pages/api/license/result.ts @@ -0,0 +1,32 @@ +import { authSession } from '@/services/backend/auth'; +import { GetCRD } from '@/services/backend/kubernetes/user'; +import { jsonRes } from '@/services/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const payload = await authSession(req.headers); + if (!payload) return jsonRes(resp, { code: 401, message: 'token verify error' }); + + const { paymentName } = req.query as { + paymentName: string; + }; + + if (typeof paymentName !== 'string' || paymentName === '') { + return jsonRes(resp, { code: 400, message: 'payment name cannot be empty' }); + } + + const paymentM = { + group: 'infostream.sealos.io', + version: 'v1', + namespace: payload.user.nsid, + plural: 'payments' + }; + const paymentDesc = await GetCRD(payload.kc, paymentM, paymentName); + console.log(paymentDesc?.body?.status, 'payment'); + return jsonRes(resp, { data: paymentDesc?.body?.status }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, data: error }); + } +} diff --git a/frontend/desktop/src/pages/api/platform/getEnv.ts b/frontend/desktop/src/pages/api/platform/getEnv.ts index 572ae64efd6..455c83e8e0f 100644 --- a/frontend/desktop/src/pages/api/platform/getEnv.ts +++ b/frontend/desktop/src/pages/api/platform/getEnv.ts @@ -5,7 +5,10 @@ import { enableWechat, enablePassword, enableSms, - enableGoogle + enableGoogle, + enableStripe, + enableWechatRecharge, + enableLicense } from '@/services/enable'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -20,6 +23,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const needSms = enableSms(); const needGoogle = enableGoogle(); const callback_url = process.env.CALLBACK_URL; + const stripeEnabled = enableStripe(); + const wechatEnabledRecharge = enableWechatRecharge(); + const licenseEnabled = enableLicense(); jsonRes(res, { data: { @@ -34,7 +40,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) needSms, needGithub, needWechat, - needGoogle + needGoogle, + stripeEnabled, + wechatEnabledRecharge, + licenseEnabled } }); } diff --git a/frontend/desktop/src/pages/api/price/bonus.ts b/frontend/desktop/src/pages/api/price/bonus.ts new file mode 100644 index 00000000000..62cadd7c6ae --- /dev/null +++ b/frontend/desktop/src/pages/api/price/bonus.ts @@ -0,0 +1,28 @@ +import { authSession } from '@/services/backend/auth'; +import { GetConfigMap } from '@/services/backend/kubernetes/user'; +import { jsonRes } from '@/services/backend/response'; +import { enableRecharge } from '@/services/enable'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const payload = await authSession(req.headers); + if (!payload) { + return jsonRes(resp, { code: 401, message: 'token verify error' }); + } + const result = await GetConfigMap(payload.kc, 'sealos', 'recharge-gift'); + if (!result.body.data) { + return jsonRes(resp, { code: 404, message: 'not found' }); + } + return jsonRes(resp, { + code: 200, + data: { + ratios: result.body.data.ratios, + steps: result.body.data.steps + } + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get price error' }); + } +} diff --git a/frontend/desktop/src/pages/api/price/index.ts b/frontend/desktop/src/pages/api/price/index.ts new file mode 100644 index 00000000000..5586297b69a --- /dev/null +++ b/frontend/desktop/src/pages/api/price/index.ts @@ -0,0 +1,52 @@ +import { authSession } from '@/services/backend/auth'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import * as yaml from 'js-yaml'; +import { jsonRes } from '@/services/backend/response'; +import { CRDMeta, ValuationBillingRecord, ValuationData } from '@/types'; +import { ApplyYaml, GetCRD } from '@/services/backend/kubernetes/user'; +import { IncomingMessage } from 'http'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const payload = await authSession(req.headers); + if (!payload) { + return jsonRes(resp, { code: 401, message: 'token verify error' }); + } + const namespace = payload.user.nsid; + const name = 'prices'; + 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(payload.kc, yaml.dump(crdSchema)); + await new Promise((resolve) => setTimeout(() => resolve(), 1000)); + } finally { + const crd = (await GetCRD(payload.kc, meta, name)) as { + response: IncomingMessage; + body: ValuationData; + }; + const billingRecords = crd?.body?.status?.billingRecords || []; + 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/desktop/src/pages/api/system/getSystemConfig.ts b/frontend/desktop/src/pages/api/system/getSystemConfig.ts index 2b08d072f4b..87f78b8d1d1 100644 --- a/frontend/desktop/src/pages/api/system/getSystemConfig.ts +++ b/frontend/desktop/src/pages/api/system/getSystemConfig.ts @@ -22,7 +22,6 @@ export async function getSystemConfig(): Promise { const res = JSON.parse(readFileSync(filename, 'utf-8')); return res; } catch (error) { - console.log('get system config error, set default'); return defaultConfig; } } diff --git a/frontend/desktop/src/pages/license/components/CurrencySymbol.tsx b/frontend/desktop/src/pages/license/components/CurrencySymbol.tsx new file mode 100644 index 00000000000..f784def928f --- /dev/null +++ b/frontend/desktop/src/pages/license/components/CurrencySymbol.tsx @@ -0,0 +1,22 @@ +import { Text, Icon } from '@chakra-ui/react'; +export default function currencysymbol({ + type = 'shellCoin', + ...props +}: { + type?: 'shellCoin' | 'cny' | 'usd'; +} & Pick[0], 'w' | 'h' | 'color'>) { + return type === 'shellCoin' ? ( + + + + ) : type === 'cny' ? ( + + ) : ( + $ + ); +} diff --git a/frontend/desktop/src/pages/license/components/OuterLink.tsx b/frontend/desktop/src/pages/license/components/OuterLink.tsx new file mode 100644 index 00000000000..415ac13d707 --- /dev/null +++ b/frontend/desktop/src/pages/license/components/OuterLink.tsx @@ -0,0 +1,35 @@ +import { Flex, Img, Link } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; + +export default function Index({ text, href }: { text: string; href?: string }) { + const { t } = useTranslation(); + return ( + + + + + + {text} + + + ); +} diff --git a/frontend/desktop/src/pages/license/components/Pagination.tsx b/frontend/desktop/src/pages/license/components/Pagination.tsx new file mode 100644 index 00000000000..064132bc699 --- /dev/null +++ b/frontend/desktop/src/pages/license/components/Pagination.tsx @@ -0,0 +1,118 @@ +import { Flex, Text, Image, FlexProps, Icon, Box } from '@chakra-ui/react'; +import { useState } from 'react'; +type PaginationProps = { + totalItems: number; + itemsPerPage: number; + onPageChange: any; +}; + +export default function Pagination({ totalItems, itemsPerPage, onPageChange }: PaginationProps) { + const totalPage = Math.ceil(totalItems / itemsPerPage); + const [currentPage, setCurrentPage] = useState(1); + + const goToPage = (page: number) => { + if (page >= 1 && page <= totalPage) { + setCurrentPage(page); + onPageChange(page); + } + }; + + const handlePrevPage = () => { + goToPage(currentPage - 1); + }; + + const handleNextPage = () => { + goToPage(currentPage + 1); + }; + + const buttonStyle: FlexProps = { + justifyContent: 'center', + alignItems: 'center', + w: '24px', + h: '24px', + borderRadius: '50%' + }; + + return ( + + Total: + {totalItems} + + + + + + + goToPage(1)} + > + + + + + + + {currentPage}/{totalPage} + + + + + + + + goToPage(totalPage)} + > + + + + + + {itemsPerPage} + /Page + + ); +} diff --git a/frontend/desktop/src/pages/license/components/Recharge.tsx b/frontend/desktop/src/pages/license/components/Recharge.tsx new file mode 100644 index 00000000000..8a7832e13c3 --- /dev/null +++ b/frontend/desktop/src/pages/license/components/Recharge.tsx @@ -0,0 +1,258 @@ +import useBonusBox from '@/hooks/useBonusBox'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import request from '@/services/request'; +import { ApiResp, LicensePayStatus, LicensePayload, Payment, SystemEnv } from '@/types'; +import { deFormatMoney } from '@/utils/format'; +import { + Button, + Checkbox, + Flex, + Image, + Link, + Modal, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Text, + useDisclosure +} from '@chakra-ui/react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import WechatPayment from './WechatPayment'; + +export default function RechargeComponent() { + const { t } = useTranslation(); + const [isAgree, setIsAgree] = useState(false); + const [isInvalid, setIsInvalid] = useState(false); + const { BonusBox, selectAmount } = useBonusBox(); + const { isOpen, onOpen, onClose } = useDisclosure(); + // 整个流程跑通需要状态管理, 0 初始态, 1 创建支付单, 2 支付中, 3 支付成功 + const [complete, setComplete] = useState<0 | 1 | 2 | 3>(0); + // 0 是微信,1 是stripe + const [payType, setPayType] = useState<'wechat' | 'stripe'>('wechat'); + const [paymentName, setPaymentName] = useState(''); + const queryClient = useQueryClient(); + const { toast } = useCustomToast(); + const { query } = useRouter(); + const [hid, setHid] = useState(''); // license key + + // handle hid + useEffect(() => { + if (query?.hid && typeof query.hid === 'string') { + const decodedHid = decodeURIComponent(query.hid); + setHid(decodedHid); + } else { + toast({ + status: 'error', + title: 'Purchase Link Error', + isClosable: true, + position: 'top' + }); + } + }, []); + + const onModalClose = () => { + setComplete(0); + onClose(); + }; + + const handleWechatConfirm = () => { + if (isAgree) { + setComplete(1); + createPaymentLicense.mutate(); + onOpen(); + } else { + toast({ + status: 'error', + title: t('Please read and agree to the agreement'), + isClosable: true, + position: 'top' + }); + } + }; + + const { data: platformEnv } = useQuery(['getPlatformEnv'], () => + request>('/api/platform/getEnv') + ); + + const createPaymentLicense = useMutation( + () => + request.post>('/api/license/pay', { + amount: deFormatMoney(selectAmount), //weixin + quota: selectAmount, + paymentMethod: payType, + hid: hid + }), + { + onSuccess(data) { + console.log(data); + setPaymentName((data?.data?.paymentName as string).trim()); + setComplete(2); + }, + onError(err: any) { + toast({ + status: 'error', + title: err?.message || '', + isClosable: true, + position: 'top' + }); + setComplete(0); + } + } + ); + + const createLicenseRecord = useMutation( + (payload: LicensePayload) => + request.post('/api/license/createLicenseRecord', payload), + { + onSuccess(data) { + console.log(data); + queryClient.invalidateQueries(['getLicenseActive']); + }, + onError(err: any) { + console.log(err); + } + } + ); + + const { data } = useQuery( + ['getLicenseResult', paymentName], + () => + request>('/api/license/result', { + params: { + paymentName: paymentName + } + }), + { + refetchInterval: complete === 2 ? 1000 : false, + enabled: complete === 2 && !!paymentName, + cacheTime: 0, + staleTime: 0, + onSuccess(data) { + if (data?.data?.status === 'Completed') { + onModalClose(); + toast({ + status: 'success', + title: t('Payment Successful'), + isClosable: true, + position: 'top' + }); + createLicenseRecord.mutate({ + uid: '', + amount: deFormatMoney(selectAmount), + quota: selectAmount, + token: data.data?.token, + orderID: data?.data?.tradeNO, + paymentMethod: 'wechat' + }); + } + } + } + ); + + return ( + + + {t('Purchase License')} + + + {t('Select Amount')} + + + + { + setIsInvalid(false); + setIsAgree(e.target.checked); + }} + /> + + {t('agree policy')} + + {t('Service Agreement')} + + {t('and')} + + {t('Privacy Policy')} + + + {/* */} + + + {/* {platformEnv?.data?.stripeEnabled && ( + + )} */} + + + + + + + 充值金额 + + + + + + + ); +} diff --git a/frontend/desktop/src/pages/license/components/Record.tsx b/frontend/desktop/src/pages/license/components/Record.tsx new file mode 100644 index 00000000000..a46110992f9 --- /dev/null +++ b/frontend/desktop/src/pages/license/components/Record.tsx @@ -0,0 +1,123 @@ +import request from '@/services/request'; +import { ApiResp, LicenseRecord } from '@/types'; +import download from '@/utils/downloadFIle'; +import { formatMoney, getRemainingTime } from '@/utils/format'; +import { Box, Flex, Image, Text } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import Pagination from './Pagination'; + +export default function History() { + const { t } = useTranslation(); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const { data } = useQuery(['getLicenseActive', page, pageSize], () => + request.post>( + '/api/license/getLicenseRecord', + { + page, + pageSize + } + ) + ); + + const downloadToken = (token: string) => { + const result = Buffer.from(token, 'binary').toString('base64'); + download('token.txt', result); + }; + + return ( + + + {t('Purchase History')} + + {data?.data?.records && data?.data?.records?.length > 0 ? ( + + {data?.data?.records.map((item) => ( + + + + token + + License + + token + + {formatMoney(item.amount)} + + + + {t('Remaining Time')} {getRemainingTime(item.exp)} + + + + + token + + Token + + downloadToken(item.token)} + src={'/icons/download.svg'} + w={'20px'} + h={'20px'} + alt="download" + > + + + ))} + + ) : ( + + empty + + {t('You have not purchased the License')} + + + )} + + + {}} + /> + + + ); +} diff --git a/frontend/desktop/src/pages/license/components/WechatPayment.tsx b/frontend/desktop/src/pages/license/components/WechatPayment.tsx new file mode 100644 index 00000000000..8faacec5885 --- /dev/null +++ b/frontend/desktop/src/pages/license/components/WechatPayment.tsx @@ -0,0 +1,60 @@ +import { Flex, Box, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { QRCodeSVG } from 'qrcode.react'; + +export default function WechatPayment(props: { + complete: number; + codeURL?: string; + tradeNO?: string; +}) { + const { t } = useTranslation(); + return ( + + + + {t('Scan with WeChat')} + + {props.complete === 2 && !!props.codeURL ? ( + + ) : ( + waiting... + )} + + + {t('Order Number')}: {props.tradeNO || ''} + + + {t('Payment Result')}:{props.complete === 3 ? t('Payment Successful') : t('In Payment')} + + + + + ); +} diff --git a/frontend/desktop/src/pages/license/index.tsx b/frontend/desktop/src/pages/license/index.tsx new file mode 100644 index 00000000000..cd46ad14460 --- /dev/null +++ b/frontend/desktop/src/pages/license/index.tsx @@ -0,0 +1,75 @@ +import LangSelectSimple from '@/components/LangSelect/simple'; +import { Flex, Image, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import RechargeComponent from './components/Recharge'; +import LicenseRecord from './components/Record'; +import { useRouter } from 'next/router'; +import request from '@/services/request'; +import { useQuery } from '@tanstack/react-query'; +import { ApiResp, SystemEnv } from '@/types'; + +export default function LicensePage() { + const { t } = useTranslation(); + const router = useRouter(); + const goHome = () => router.replace('/'); + + const { data: platformEnv } = useQuery( + ['getPlatformEnv'], + () => request>('/api/platform/getEnv'), + { + onSuccess(data) { + console.log(data.data?.licenseEnabled, 'licenseEnabled'); + if (!data.data?.licenseEnabled) { + goHome(); + } + } + } + ); + + return ( + + + logo + + Sealos + + + | {t('License Buy')} + + + + + + + + + ); +} + +export async function getServerSideProps({ req, res, locales }: any) { + const local = req?.cookies?.NEXT_LOCALE || 'en'; + + return { + props: { + ...(await serverSideTranslations(local, undefined, null, locales || [])) + } + }; +} diff --git a/frontend/desktop/src/services/backend/db/license.ts b/frontend/desktop/src/services/backend/db/license.ts new file mode 100644 index 00000000000..89a59485170 --- /dev/null +++ b/frontend/desktop/src/services/backend/db/license.ts @@ -0,0 +1,72 @@ +import { LicensePayload, LicenseRecord } from '@/types'; +import { connectToDatabase } from './mongodb'; + +async function connectLicenseRecordCollection() { + const client = await connectToDatabase(); + const collection = client.db().collection('licenseRecord'); + return collection; +} + +export async function createLicenseRecord({ + uid, + amount, + token, + orderID, + quota, + paymentMethod +}: LicensePayload) { + const collection = await connectLicenseRecordCollection(); + + const now = Math.floor(Date.now() / 1000); // Get current timestamp in seconds + const oneDayInSeconds = 24 * 60 * 60; // One day in seconds + + const record: LicenseRecord = { + uid: uid, + token: token, + orderID: orderID, + paymentMethod: paymentMethod, + service: { + quota: quota + }, + iat: now, // Store the current timestamp as iat + exp: now + oneDayInSeconds, // Set expiration to one day from now (in seconds) + amount: amount + }; + + const result = await collection.insertOne(record); + + return result; +} + +export async function getLicenseRecordsByUid({ + uid, + page, + pageSize +}: { + uid: string; + page: number; + pageSize: number; +}) { + const collection = await connectLicenseRecordCollection(); + + const skip = (page - 1) * pageSize; + + const query = { uid: uid }; + const options = { + skip: skip, + limit: pageSize + }; + + // Find records for the specified uid, skip records based on pagination, and limit the result to pageSize + const records = await collection.find(query, options).sort({ iat: -1 }).toArray(); + + // Calculate the total count of records for the given uid + const totalCount = await collection.countDocuments(query); + + const result = { + records: records, + total: totalCount + }; + + return result; +} diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index a97ed5078f5..322f9e35ae4 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -22,3 +22,11 @@ export const enableGoogle = () => export const enableRecharge = () => { return process.env.RECHARGE_ENABLED === 'true'; }; +// costcenter +export const enableStripe = () => + process.env['STRIPE_ENABLED'] === 'true' && !!process.env['STRIPE_PUB']; +export const enableWechatRecharge = () => process.env['WECHAT_ENABLED'] === 'true'; +// license +export const enableLicense = () => { + return process.env.LICENSE_ENABLED === 'true'; +}; diff --git a/frontend/desktop/src/styles/globals.scss b/frontend/desktop/src/styles/globals.scss index 9a24db4d163..c59ed76cd89 100644 --- a/frontend/desktop/src/styles/globals.scss +++ b/frontend/desktop/src/styles/globals.scss @@ -52,3 +52,22 @@ input:-webkit-autofill { background-image: none; color: #000; } + +div { + &::-webkit-scrollbar-thumb, + &::-webkit-scrollbar-thumb { + background: transparent !important; + border-radius: 10px; + transition: 1s; + } + &:hover { + &::-webkit-scrollbar-thumb, + &::-webkit-scrollbar-thumb { + background: rgba(189, 193, 197, 0.5) !important; + } + &::-webkit-scrollbar-thumb:hover, + &::-webkit-scrollbar-thumb:hover { + background: rgba(189, 193, 197, 1) !important; + } + } +} diff --git a/frontend/desktop/src/types/index.ts b/frontend/desktop/src/types/index.ts index 7ac651a2620..d0f327f1e38 100644 --- a/frontend/desktop/src/types/index.ts +++ b/frontend/desktop/src/types/index.ts @@ -6,6 +6,8 @@ export * from './crd'; export * from './payment'; export * from './system'; export * from './login'; +export * from './valuation'; +export * from './license'; declare global { var mongodb: MongoClient | null; diff --git a/frontend/desktop/src/types/license.ts b/frontend/desktop/src/types/license.ts new file mode 100644 index 00000000000..69d699c2f71 --- /dev/null +++ b/frontend/desktop/src/types/license.ts @@ -0,0 +1,63 @@ +export type LicenseCrd = { + apiVersion: 'infostream.sealos.io/v1'; + kind: 'Payment'; + metadata: { + name: string; + namespace: string; + }; + spec: { + userID: string; + amount: number; + paymentMethod: 'wechat' | 'stripe'; + service: { + amt: number; + objType: string; // 'external,internal' + }; + }; + status: { + tradeNO: string; + codeURL: string; + status: 'Created' | 'Completed'; + token: string; + }; +}; + +export type LicensePaymentForm = { + paymentName: string; + namespace: string; + userId: string; + amount: number; + paymentMethod: 'wechat' | 'stripe'; + hashID: string; + quota: number; +}; + +export type LicensePayStatus = { + tradeNO: string; + codeURL: string; + status: 'Created' | 'Completed'; + token: string; +}; + +export type LicenseRecord = { + _id?: string; + uid: string; // user id + token: string; // license token + orderID: string; // order number + paymentMethod: 'wechat' | 'stripe'; + service: { + quota: number; // 额度 + }; + iat: number; // 签发日期 + exp: number; // 有效期 + amount: number; // 消费金额 +}; + +export type LicensePayload = { + uid: string; + amount: number; + token: string; + orderID: string; + quota: number; + paymentMethod: 'wechat' | 'stripe'; +}; diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index 741b872a3e4..3fa09f8bbba 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -23,4 +23,7 @@ export type LoginProps = { export type SystemEnv = { SEALOS_CLOUD_DOMAIN: string; + stripeEnabled: boolean; + wechatEnabledRecharge: boolean; + licenseEnabled: boolean; } & LoginProps; diff --git a/frontend/desktop/src/types/valuation.ts b/frontend/desktop/src/types/valuation.ts new file mode 100644 index 00000000000..da4244ceb53 --- /dev/null +++ b/frontend/desktop/src/types/valuation.ts @@ -0,0 +1,18 @@ +export type ValuationStandard = { + name: string; + unit: string; + price: string; +}; +export type ValuationBillingRecord = { + price: number; + resourceType: string; +}; +export type ValuationData = { + apiVersion: 'account.sealos.io/v1'; + kind: 'PriceQuery'; + metadata: any; + spec: {}; + status: { + billingRecords: ValuationBillingRecord[]; + }; +}; diff --git a/frontend/desktop/src/utils/format.ts b/frontend/desktop/src/utils/format.ts index a44903afac6..5f24f135ef4 100644 --- a/frontend/desktop/src/utils/format.ts +++ b/frontend/desktop/src/utils/format.ts @@ -36,3 +36,19 @@ export const parseOpenappQuery = (openapp: string) => { appQuery }; }; + +export const getRemainingTime = (expirationTime: number) => { + const currentTime = Math.floor(Date.now() / 1000); + + if (currentTime >= expirationTime) { + return 'expired'; + } + + const remainingTimeInSeconds = expirationTime - currentTime; + const hours = Math.floor(remainingTimeInSeconds / 3600); + const minutes = Math.floor((remainingTimeInSeconds % 3600) / 60); + const seconds = remainingTimeInSeconds % 60; + + const formattedTime = `${hours}小时${minutes}分钟`; + return formattedTime; +}; diff --git a/frontend/providers/license/.env.template b/frontend/providers/license/.env.template index b86f5b79196..98609357cd8 100644 --- a/frontend/providers/license/.env.template +++ b/frontend/providers/license/.env.template @@ -1,3 +1,5 @@ NEXT_PUBLIC_MOCK_USER= SEALOS_DOMAIN="cloud.sealos.io" -MONGODB_URI= \ No newline at end of file +MONGODB_URI= +# Same as desktop important +PASSWORD_SALT= \ No newline at end of file diff --git a/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl b/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl index 485790ea584..4b611e0c98b 100644 --- a/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl +++ b/frontend/providers/license/deploy/manifests/appcr.yaml.tmpl @@ -14,8 +14,4 @@ spec: name: license type: iframe displayType: normal - i18n: - zh: - name: 定时任务 - zh-Hans: - name: 定时任务 + diff --git a/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl index 6dabee4b810..739783fbce3 100644 --- a/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/license/deploy/manifests/deploy.yaml.tmpl @@ -1,15 +1,8 @@ apiVersion: v1 -kind: Namespace -metadata: - labels: - app: license-frontend - name: license-frontend ---- -apiVersion: v1 kind: ConfigMap metadata: name: license-frontend-config - namespace: license-frontend + namespace: sealos data: config.yaml: |- addr: :3000 @@ -18,7 +11,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: license-frontend - namespace: license-frontend + namespace: sealos spec: replicas: 1 selector: @@ -39,6 +32,16 @@ spec: env: - name: SEALOS_DOMAIN value: {{ .cloudDomain }} + - name: MONGODB_URI + valueFrom: + secretKeyRef: + key: mongodb_uri + name: license-frontend-secret + - name: PASSWORD_SALT + valueFrom: + secretKeyRef: + key: password_salt + name: license-frontend-secret securityContext: runAsNonRoot: true runAsUser: 1001 @@ -54,7 +57,7 @@ spec: cpu: 10m memory: 128Mi # do not modify this image, it is used for CI/CD - image: ghcr.io/labring/sealos-license-frontend:latest + image: zhujingyang/sealos-license-frontend:1.0.1 imagePullPolicy: Always volumeMounts: - name: license-frontend-volume @@ -71,7 +74,7 @@ metadata: labels: app: license-frontend name: license-frontend - namespace: license-frontend + namespace: sealos spec: ports: - name: http diff --git a/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl b/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl index ecc46f16604..affa394e9d3 100644 --- a/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl +++ b/frontend/providers/license/deploy/manifests/ingress.yaml.tmpl @@ -14,7 +14,7 @@ metadata: add_header Cache-Control "public"; } name: license-frontend - namespace: license-frontend + namespace: sealos spec: rules: - host: license.{{ .cloudDomain }} @@ -30,4 +30,4 @@ spec: tls: - hosts: - license.{{ .cloudDomain }} - secretName: {{ .certSecretName }} + secretName: wildcard-cert diff --git a/frontend/providers/license/public/favicon.ico b/frontend/providers/license/public/favicon.ico index 1519d186a3d48771d05d94204d364d6514159a88..0587dabd2325c5ca594c306b155461b6f37ea15a 100644 GIT binary patch literal 4286 zcmdUyTZmOv7{}LPJPLza$)L{EjN|OJ&szI-&R7(Vr71$G2tiNw){EeiLKl6qn-BVC zL{HUA6f&YwFA+iY5-6zMkU}3sx*!o4QCiu}e*c--vYO7YHQ6(C4!^V3Uf(+F|E+J` z_A;iycX-(7*Noq4%u-{F2a3#kQ2E3M+lPxMnJk$5-`rXwvX!yPV`~Z@)k|Vc;#(#K z=bXuS?ktW?R-ije`qrf;^YquFy^MSwP5AM!S!mjBq^Ircp+mc)scn#=9TR8bo}sPO z+X4J}6n59lPTIe-?QM_r^Efu`A=jX62mAAFe^asTq*%H2ef=^BBAc-%dS$O8Pu} zHT4|&Ri#fq;%E3@Kiob8vaNv=ptg^~3eY@01RGi7vY&xaTlwqIw+Heha^Sm=Oxo-N zwDr+c`)?G^4iqjPWL^)$a`abYJ3D0$eW=n`{4c`!3ckUvaaY?%(7rc!_672a`(E@n zkp7qchrV8KUi$0N)41zDuib>c_McB+91aKQS>yd1O!w;uW$WM-cpdiNguY_>M0<2# zUQrA+VSZ5zHjB<3O~!pYwjUtW*S`5|k;MH)VpqVDB8^v4=HM;Rc!u?>#$Ww=2{yr& zfX(C&!X*gX`;7H+ckWw@NXM{kD}3C_TTw50ohjM}!unPE@>d$)b)h^X!uF2li5;Y_ z*4`KJJ-o;HvX*o09`ffQtY4)c)}5Z0Z<-%vOur<*m2nv%{Q%1PI%oFhF6twF4#F5z z>FdAm5X-&rR2QDc{yxZ$bF8`hb01mq+5<~{`FAk$(QTx=Asn+Redb^MWRWJ#BJ~UY zMdB6^TlsJWr1vUlpWFi%;1~E99)wZ&97?~GR_W`^c$4+B6Lxh2``b{SEojB7=Fv~^Uuu+ol+WGLf9lWS`*lqllR3lu)%ww1skK@stS5U{){1&E7n#1^C1%xVB7L)k>tNmYk4_RbxPzFq#k~Xl zjqAqDNN>N%3*G-}zWYtwEv*?I;SPv?ivFzMKY8bGG3R{C8q2+Ru0L1#%#{z){u=}v B)A9fS literal 4286 zcmbW5Ym8l08HU&K%ycqCnbz74u}){s+4uW7Gt<%*T3Sj=DoRDMR9d--!K4Z05<|EY zEeQz$666O7pe2y-2QMfnKfD`53}^+k788vL6^sN#tcsC~Ql5A1vu93cPD^1=p83|^ zd+qgp-?y$a#?<&}G>m?mhZY($VvLD^M`k_fxxIMz_;5-2p!e@Q2uwxc# zl;f^v_Psk(C-zO21SZ2S564Uqxuz94mfou6(~NoQi63?WlQ# zej2)_R;%@N!|G4_;G2_|oH7%C;??kBG5+P)O#G>ufg-3`>o{rB>7P0p7BMVg|0WA-)D&$^bzAN zb{(_YSyjWAF}}YVo&v>r0PVeSDY;*frvAv}igy3qkM3t&$AgU7G5YCr>U|L&hUefF z_#SM{1Ao|DX~-*c$|u*&*y^vE%8i)FVpGI{nap_~`X3^$9ek%)mET{J%Nww18U;gn z;`JqkUyGZ|Oze%DAUsKJGERd2EDneHb~AOq0qT?gz(e@9hIR*)W6CCHZt8NzM=gVc z1E!sZW@Paqjq~;JZIJ&OSJ%=nPT)4`E#?#PGd6uQwQ|z7(TWnFa)t>l|~ z;csvl)E64Jj`{#=3f2c*3HO%3F_G7-(NBwce--E(ji=kv$d8AHM{4aXGMiVCpQO}p zw==V1`Etfg0KQ2AzmCm_e>dMf4gUq{{~t6aKZ@>rh6mv9@Hm|9 z2C1sPq|c?iBa8!tX(SROL@Tyaw{)n;;(+Lg!m+hq=ECs&8iN&A!*# ztaZh^cQgdRp|caXyv!i`W>7L=$HGW zY!P|$(O6GA_kiZT+R(c91Gt!XOB?87yJJ^b`uBqR_Gr4*bL~-&F`hSInZMH4xuTkb z?g^CTgW}OR{yu0-EQL~+bt6sUvRpoUFZwUCKHdt6)=8mw3VbLn>u2PiKzRe~g6**M zRO;k0zGh96=7~x2_WRY>x^O-$s^X!2XixNB?Cs?}w}?q}R!3m$r1~K-}%s>}|zpJtYW!==yj%Z2L7fZW zgK&iY9LUjlQhUdv{}kWsOhRvT%x~AU zb_StN>1J-s($5@Iv>N90FLPcf8bev^o`=rQ;7{-v?54eQ)6n}kJK5_xO9Fq(%oA$w-8E8)yp(lCBLTBFPucejzJAJUH22kq(4f!3>MLHnfUnAR)i zxZQI#haZNkVJ{p=Blo)r=fN;`2CycIonG=Lj+>SU>m_5OixO<(XsKJ*K{m zpYt7-w%K!717ct26Aw;>(_kGCr?tA*x$DCdt=yh<@yARq;qSpyI3rCqkvnV@7ss%7 zj~`tp8MXCjvzjxkk@kGB56hUg=Tnu%Gw^fx)Rqf(nZ$3{@Q9yyLsQ1h8a5rj{5kfe zGh%gKq+R)-^;Y|o##=A6U+rAGVRKFTpSb7>=1+kp?MQbP$5v2KJX*j3j4zXLSq zweQGQe2wy*uy*YkGx}3gQzqlwI$K-!$8*tMw+}V$b>`4HU^ZHdl%E0gZT=O%^hK}h hbz=q&@h<{#kP3T_0k7WEwTIfYKZnCSv+ObB?*M`h2cG}{ diff --git a/frontend/providers/license/public/icons/license-bg.svg b/frontend/providers/license/public/icons/license-bg.svg index 08977cb1d5b..b6de3cbc6f7 100644 --- a/frontend/providers/license/public/icons/license-bg.svg +++ b/frontend/providers/license/public/icons/license-bg.svg @@ -23,13 +23,6 @@ - - - - - - - @@ -53,16 +46,6 @@ - - - - - - - - - - diff --git a/frontend/providers/license/public/icons/license-sealos.svg b/frontend/providers/license/public/icons/license-sealos.svg new file mode 100644 index 00000000000..c63dd2757cf --- /dev/null +++ b/frontend/providers/license/public/icons/license-sealos.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/license/public/locales/en/common.json b/frontend/providers/license/public/locales/en/common.json index e19bcfe5ce8..4504e44379d 100644 --- a/frontend/providers/license/public/locales/en/common.json +++ b/frontend/providers/license/public/locales/en/common.json @@ -4,6 +4,9 @@ "Activation Record": "Activation Record", "Purchase License": "Purchase License", "Please go to": "Please go to", - "Buy": "Buy", - "Activation Time": "Activation time: " + "Purchase": "purchase a license.", + "Activation Time": "Activation time: ", + "Go to the message center to see the results": "Go to the message center to see the results", + "token does not exist": "token does not exist", + "No Record": "No Record" } \ No newline at end of file diff --git a/frontend/providers/license/public/locales/zh/common.json b/frontend/providers/license/public/locales/zh/common.json index 06fc0cddef1..18ec9e4b9a6 100644 --- a/frontend/providers/license/public/locales/zh/common.json +++ b/frontend/providers/license/public/locales/zh/common.json @@ -3,7 +3,10 @@ "Activate License": "激活 License", "Activation Record": "激活记录", "Purchase License": "购买 License", - "Please go to": "请前往", - "Buy": "购买", - "Activation Time": "激活时间: " + "Please go to": "请到", + "Purchase": "购买 license", + "Activation Time": "激活时间: ", + "Go to the message center to see the results": "前往消息中心查看结果", + "token does not exist": "token 不存在", + "No Record": "暂无记录" } \ No newline at end of file diff --git a/frontend/providers/license/public/logo.svg b/frontend/providers/license/public/logo.svg index d826088c31e..956fd4ac3f9 100644 --- a/frontend/providers/license/public/logo.svg +++ b/frontend/providers/license/public/logo.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/providers/license/src/components/FileSelect/index.tsx b/frontend/providers/license/src/components/FileSelect/index.tsx index 40b25681669..4d7a7cd59e7 100644 --- a/frontend/providers/license/src/components/FileSelect/index.tsx +++ b/frontend/providers/license/src/components/FileSelect/index.tsx @@ -182,8 +182,9 @@ const FileSelect = ({ fileExtension, setFiles, files, ...props }: Props) => { {t('Upload Token File')} + Total: {totalItems} diff --git a/frontend/providers/license/src/constants/theme.ts b/frontend/providers/license/src/constants/theme.ts index 0c14037ac3f..ac73cd40374 100644 --- a/frontend/providers/license/src/constants/theme.ts +++ b/frontend/providers/license/src/constants/theme.ts @@ -233,7 +233,6 @@ export const theme = extendTheme({ color: 'myGray.900', fontSize: 'md', height: '100%', - overflowY: 'hidden', fontWeight: 400 } } diff --git a/frontend/providers/license/src/pages/_app.tsx b/frontend/providers/license/src/pages/_app.tsx index 74464bf5f83..abc63550c10 100644 --- a/frontend/providers/license/src/pages/_app.tsx +++ b/frontend/providers/license/src/pages/_app.tsx @@ -91,7 +91,7 @@ function App({ Component, pageProps }: AppProps) { const lastLang = getLangStore(); const newLang = data.currentLanguage; if (lastLang !== newLang) { - i18n.changeLanguage(newLang); + i18n?.changeLanguage?.(newLang); setLangStore(newLang); setRefresh((state) => !state); } diff --git a/frontend/providers/license/src/pages/api/license/getLicense.tsx b/frontend/providers/license/src/pages/api/license/getLicense.tsx index dbbad0743ce..53f67264e8c 100644 --- a/frontend/providers/license/src/pages/api/license/getLicense.tsx +++ b/frontend/providers/license/src/pages/api/license/getLicense.tsx @@ -3,15 +3,16 @@ import { jsonRes } from '@/services/backend/response'; import { connectToLicenseCollection } from '@/services/db/mongodb'; import { getK8s } from '@/services/backend/kubernetes'; import { authSession } from '@/services/backend/auth'; -import { getUserId } from '@/utils/user'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { page = '1', pageSize = '10' } = req.query as { page: string; pageSize: string }; - const { namespace } = await getK8s({ + const { kube_user } = await getK8s({ kubeconfig: await authSession(req) }); - const userId = getUserId(); + const userId = kube_user.name; + console.log(userId, 'userId'); + const skip = (parseInt(page) - 1) * parseInt(pageSize); const license_collection = await connectToLicenseCollection(); @@ -38,6 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } }); } catch (err) { + console.log(err, 'getlicense----'); jsonRes(res, { code: 500, error: err diff --git a/frontend/providers/license/src/pages/api/platform/getEnv.ts b/frontend/providers/license/src/pages/api/platform/getEnv.ts index 06c85983aae..44397b31e04 100644 --- a/frontend/providers/license/src/pages/api/platform/getEnv.ts +++ b/frontend/providers/license/src/pages/api/platform/getEnv.ts @@ -1,17 +1,18 @@ import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; +import { hashCrypto } from '@/utils/crypto'; import type { NextApiRequest, NextApiResponse } from 'next'; export type Response = { domain?: string; - env_storage_className?: string; + hid: string; // PASSWORD_SALT }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { jsonRes(res, { data: { domain: process.env.SEALOS_DOMAIN || 'cloud.sealos.io', - env_storage_className: process.env.STORAGE_CLASSNAME || '' + hid: hashCrypto(process.env.PASSWORD_SALT || '') } }); } diff --git a/frontend/providers/license/src/pages/index.tsx b/frontend/providers/license/src/pages/index.tsx index 2b902b04163..758939cbe7c 100644 --- a/frontend/providers/license/src/pages/index.tsx +++ b/frontend/providers/license/src/pages/index.tsx @@ -1,14 +1,19 @@ import { applyLicense, getLicenseRecord } from '@/api/license'; +import { getPlatformEnv } from '@/api/platform'; import CurrencySymbol from '@/components/CurrencySymbol'; import FileSelect, { FileItemType } from '@/components/FileSelect'; +import MyIcon from '@/components/Icon'; import Pagination from '@/components/Pagination'; import { useToast } from '@/hooks/useToast'; +import download from '@/utils/downloadFIle'; +import { serviceSideProps } from '@/utils/i18n'; import { json2License } from '@/utils/json2Yaml'; -import { Box, Flex, Icon, Image, Link, Text, Button } from '@chakra-ui/react'; +import { useCopyData } from '@/utils/tools'; +import { Box, Flex, Icon, Image, Text } from '@chakra-ui/react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { debounce } from 'lodash'; -import { useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; export default function LicenseApp() { const { t } = useTranslation(); @@ -16,16 +21,38 @@ export default function LicenseApp() { const { toast } = useToast(); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(5); + const { copyData } = useCopyData(); + const [purchaseLink, setPurchaseLink] = useState(''); + const licenseMutation = useMutation({ mutationFn: (yamlList: string[]) => applyLicense(yamlList, 'create'), onSuccess(data) { - console.log(data); + toast({ + title: t('Go to the message center to see the results'), + status: 'success' + }); }, onError(error) { console.log(error); } }); + useQuery(['getPlatformEnv'], () => getPlatformEnv(), { + onSuccess(data) { + const hid = data?.hid; + if (!hid) { + return toast({ + title: 'env hid error', + status: 'error' + }); + } + const encodedHid = encodeURIComponent(hid); + const main = data?.domain ? data.domain : 'cloud.sealos.io'; + const link = `https:/${main}/license?hid=${encodedHid}`; + setPurchaseLink(link); + } + }); + const { data } = useQuery(['getLicenseActive', page, pageSize], () => getLicenseRecord({ page: page, pageSize: pageSize }) ); @@ -33,7 +60,7 @@ export default function LicenseApp() { const activeLicense = debounce(async () => { if (files.length < 1) { return toast({ - title: 'token 不存在', + title: t('token does not exist'), status: 'error' }); } @@ -41,40 +68,68 @@ export default function LicenseApp() { licenseMutation.mutate(yamlList); }, 500); + const copyLicenseLink = () => { + if (!purchaseLink) { + return toast({ + title: 'env hid error', + status: 'error' + }); + } + copyData(purchaseLink); + }; + + const downloadToken = (token: string) => { + const result = Buffer.from(token, 'binary').toString('base64'); + download('token.txt', result); + }; + return ( - - - + + + - - - - {t('Purchase License')} - - - {t('Please go to')} - - https://cloud.sealos.io/license - - {t('Buy')} + + + {t('Purchase License')} + + + {t('Please go to')} + + {purchaseLink} + + {t('Purchase')} + + + + - {/* rihgt */} - - + {/* right */} + + {t('Activate License')} @@ -99,48 +154,71 @@ export default function LicenseApp() { {t('Activation Record')} - - {data?.items?.map((license, i) => ( - - - - License - - - - {license.payload?.amt} - - - {t('Activation time')} {license.meta.createTime} - + {data?.items && data?.items?.length > 0 ? ( + + {data?.items?.map((license, i) => ( - - Token + + + License + + + + {license.payload?.amt} - - - + + {t('Activation time')} {license.meta.createTime} + + + downloadToken(license?.meta?.token)} + > + Token + + + + + - - ))} - + ))} + + ) : ( + + + + {t('No Record')} + + + )} + ); } + +export async function getServerSideProps(content: any) { + return { + props: { + ...(await serviceSideProps(content)) + } + }; +} diff --git a/frontend/providers/license/src/utils/crypto.ts b/frontend/providers/license/src/utils/crypto.ts new file mode 100644 index 00000000000..7d78b7f3bb1 --- /dev/null +++ b/frontend/providers/license/src/utils/crypto.ts @@ -0,0 +1,7 @@ +import * as crypto from 'crypto'; + +export const hashCrypto = (text: string): string => { + const hash = crypto.createHash('sha256'); + hash.update(text); + return hash.digest('hex'); +}; diff --git a/frontend/providers/license/src/utils/downloadFIle.ts b/frontend/providers/license/src/utils/downloadFIle.ts new file mode 100644 index 00000000000..f0837599ae2 --- /dev/null +++ b/frontend/providers/license/src/utils/downloadFIle.ts @@ -0,0 +1,14 @@ +function download(filename: string, text: string) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + +export default download; diff --git a/frontend/providers/license/src/utils/json2Yaml.ts b/frontend/providers/license/src/utils/json2Yaml.ts index 34e4c90d5f3..fe949d66cf0 100644 --- a/frontend/providers/license/src/utils/json2Yaml.ts +++ b/frontend/providers/license/src/utils/json2Yaml.ts @@ -1,20 +1,24 @@ import yaml from 'js-yaml'; -import { getUserId } from './user'; +import { getUserId, getUserNamespace } from './user'; export const json2License = (token: string) => { + const namespace = getUserNamespace(); const userId = getUserId(); + const license_name = crypto.randomUUID(); + const decodedData = Buffer.from(token, 'base64').toString('binary'); + const template = { apiVersion: 'infostream.sealos.io/v1', kind: 'License', metadata: { - name: 'license', - namespace: 'ns-admin' + name: license_name, + namespace: namespace }, spec: { uid: userId, - token: token + token: decodedData } }; - + console.log(template, 'license'); return yaml.dump(template); }; diff --git a/frontend/providers/license/src/utils/user.ts b/frontend/providers/license/src/utils/user.ts index 799671efe74..892f23ac23f 100644 --- a/frontend/providers/license/src/utils/user.ts +++ b/frontend/providers/license/src/utils/user.ts @@ -1,3 +1,4 @@ +// edge import yaml from 'js-yaml'; type KC = { @@ -47,5 +48,5 @@ export const getUserServiceAccount = () => { export const getUserId = () => { const kubeConfig = getUserKubeConfig(); const json = yaml.load(kubeConfig) as KC; - return json?.contexts[0]?.context?.user || json.users[0].name; + return json?.contexts[0]?.context?.user || json?.users[0]?.name; };