diff --git a/frontend/desktop/prisma/global/migrations/20240904071212_add_user_status/migration.sql b/frontend/desktop/prisma/global/migrations/20240904071212_add_user_status/migration.sql new file mode 100644 index 00000000000..7f8f30917a8 --- /dev/null +++ b/frontend/desktop/prisma/global/migrations/20240904071212_add_user_status/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "UserStatus" AS ENUM ('NORMAL_USER', 'LOCK_USER', 'DELETE_USER'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "status" "UserStatus" NOT NULL DEFAULT 'NORMAL_USER'; diff --git a/frontend/desktop/prisma/global/schema.prisma b/frontend/desktop/prisma/global/schema.prisma index 33c36ae3816..40b28cc33c8 100644 --- a/frontend/desktop/prisma/global/schema.prisma +++ b/frontend/desktop/prisma/global/schema.prisma @@ -97,6 +97,7 @@ model User { id String @unique name String @unique oauthProvider OauthProvider[] + status UserStatus @default(NORMAL_USER) oldMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("oldUser") newMergeUserTransactionInfo MergeUserTransactionInfo[] @relation("newUser") DeleteUserTransactionInfo DeleteUserTransactionInfo? @@ -316,3 +317,9 @@ enum AuditAction { DELETE CREATE } + +enum UserStatus { + NORMAL_USER + LOCK_USER + DELETE_USER +} diff --git a/frontend/desktop/public/locales/en/applist.json b/frontend/desktop/public/locales/en/applist.json new file mode 100644 index 00000000000..ec1eb992438 --- /dev/null +++ b/frontend/desktop/public/locales/en/applist.json @@ -0,0 +1,12 @@ +{ + "terminal": "terminal", + "job": "job", + "other": "other", + "object-storage": "object-storage", + "cloud-vm": "cloud-vm", + "db": "database", + "app": "app", + "app-store": "app-store", + "db-backup": "db-backup", + "app_type": "app type" +} \ 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 e58238ecbe5..694d80f1265 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -31,6 +31,7 @@ "click_on_any_shadow_to_skip": "Click on any shadow to skip", "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", "confirmnewpassword": "Confirm New Password", "core": "Core", "cost_center": "Cost Center", @@ -44,8 +45,10 @@ "delete_account": "Delete account", "delete_account_button": "Delete", "delete_account_caution": "Once a resource is deleted, it cannot be recovered. \nTherefore, before performing the above operations, please be sure to back up your data.", + "delete_account_force_button": "Delete Directly", + "delete_account_remain_resources": "The following resources in your account have not been deleted. Please manually delete all the resources listed below, ensuring nothing is overlooked.", "delete_account_tips": "Permanently delete this account and all its content.", - "delete_account_title": "Logout prompt", + "delete_account_title": "Delete Account", "deleteaccounttitle": "Sealos will permanently delete this account.", "deletemyaccount": "DeleteMyAccount", "deploy_an_application": "Let’s deploy an application~", @@ -62,6 +65,8 @@ "expected_used": "Expected used", "failed_to_generate_invitation_link": "Failed to generate invitation link", "flow": "Traffic", + "force_delete_keywords": "All resources cannot be recovered after account deletion.", + "force_delete_tips": "There are still undeleted resources in your account. Once deleted, all resources will be unrecoverable. Please ensure you have backed up or transferred all important data.", "from": "From", "generate_invitation_link": "Generate invitation link", "get_code": "verification", @@ -79,6 +84,7 @@ "idCard_invalid": "INVAILD ID CARD FORMAT", "in_payment": "In Payment ...", "in_time": "In Time", + "insufficient_balance": "Your account currently has outstanding payments", "insufficient_balance_tips": "There is currently an outstanding balance in your account. In order to successfully complete the account cancellation process, please settle the outstanding balance first.", "invaild_context": "You need switch to other workspace for handling", "invaild_name_of_team": "Invaild Name of Workspace", @@ -97,6 +103,7 @@ "language": "Language", "launch_various_third-party_applications_with_one_click": "Launch various third-party applications with one click", "license_buy": "License Buy", + "link": "link", "loading": "Loading", "log_in": "Log In", "log_out": "Log Out", @@ -147,6 +154,8 @@ "placeholders_name": "Please enter your name", "placeholders_phone": "Please enter your phone number", "placeholders_verifycode": "please enter verification code", + "please_enter": "Please enter", + "please_enter_username": "Please enter your username", "please_read_and_agree_to_the_agreement": "Please read and agree to the agreement", "privacy_policy": "Privacy Policy", "private_team_id_of_user": "User's ID", diff --git a/frontend/desktop/public/locales/zh/applist.json b/frontend/desktop/public/locales/zh/applist.json new file mode 100644 index 00000000000..0f003260134 --- /dev/null +++ b/frontend/desktop/public/locales/zh/applist.json @@ -0,0 +1,12 @@ +{ + "terminal": "终端", + "job": "任务", + "other": "其他", + "object-storage": "对象存储", + "cloud-vm": "云主机", + "db": "数据库", + "app": "应用", + "app-store": "应用商店", + "db-backup": "数据库备份", + "app_type": "应用类型" +} \ 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 6e9d0bf7bea..f1f81e20464 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -30,6 +30,7 @@ "click_on_any_shadow_to_skip": "点击任意阴影跳过", "completed_the_deployment_of_an_nginx_for_the_first_time": "部署一个 nginx ,首次完成 将", "confirm": "确认", + "confirm_again": "再次确认", "confirmnewpassword": "确认新密码", "core": "核", "create_team": "创建工作空间", @@ -41,8 +42,10 @@ "delete_account": "注销账号", "delete_account_button": "注销", "delete_account_caution": "资源一旦删除,将不可恢复。因此,在执行以上操作前,请务必做好数据备份工作", + "delete_account_force_button": "直接注销", + "delete_account_remain_resources": "账号中仍有以下资源未删除,请您手动删除下列所有资源,确保无遗漏。", "delete_account_tips": "将永久删除此账户及其所有内容", - "delete_account_title": "注销提示", + "delete_account_title": "注销账户", "deleteaccounttitle": "Sealos 将永久删除此账户。", "deletemyaccount": "删除我的账号", "deploy_an_application": "来部署一个应用吧~", @@ -59,6 +62,8 @@ "expected_used": "预计还能使用", "failed_to_generate_invitation_link": "生成邀请链接失败", "flow": "流量", + "force_delete_keywords": "注销后所有资源无法恢复", + "force_delete_tips": "您的账号中仍有未删除的资源。一旦注销,所有资源将无法恢复。请确保已经备份或转移了所有重要数据。", "from": "来自", "generate_invitation_link": "生成邀请链接", "get_code": "获取验证码", @@ -76,6 +81,7 @@ "idCard_invalid": "身份证格式不对", "in_payment": "支付中 ...", "in_time": "加入时间", + "insufficient_balance": "您的账户目前存在未结清的款项", "insufficient_balance_tips": "您的账户目前存在未结清的款项,为了顺利完成账户注销流程,请先结清欠款。", "invaild_context": "你需要切换到其他工作空间操作", "invaild_name_of_team": "不合法的工作空间名称", @@ -93,6 +99,7 @@ "language": "语言", "launch_various_third-party_applications_with_one_click": "一键启动各种第三方应用", "license_buy": "License 购买", + "link": "链接", "loading": "加载中", "log_in": "登录", "log_out": "登出", @@ -143,6 +150,8 @@ "placeholders_name": "请输入您的姓名", "placeholders_phone": "请输入您的电话号码", "placeholders_verifycode": "请输入验证码", + "please_enter": "请输入", + "please_enter_username": "请输入您的用户名", "please_read_and_agree_to_the_agreement": "请阅读并同意协议", "privacy_policy": "隐私政策", "private_team_id_of_user": "用户ID", diff --git a/frontend/desktop/src/api/auth.ts b/frontend/desktop/src/api/auth.ts index c37662b1464..0819d115c3e 100644 --- a/frontend/desktop/src/api/auth.ts +++ b/frontend/desktop/src/api/auth.ts @@ -1,16 +1,18 @@ +import { SmsType } from '@/services/backend/db/verifyCode'; +import { RegionResourceType } from '@/services/backend/svc/checkResource'; import request from '@/services/request'; -import { OauthProvider, TUserExist } from '@/types/user'; -import { ApiResp, Region } from '@/types'; -import { AxiosHeaders, AxiosHeaderValue, type AxiosInstance } from 'axios'; import useSessionStore from '@/stores/session'; -import { ProviderType } from 'prisma/global/generated/client'; -import { ValueOf } from '@/types/tools'; -import { SmsType } from '@/services/backend/db/verifyCode'; -import { USER_MERGE_STATUS } from '@/types/response/merge'; +import { ApiResp, Region } from '@/types'; import { BIND_STATUS } from '@/types/response/bind'; -import { UNBIND_STATUS } from '@/types/response/unbind'; import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { DELETE_USER_STATUS } from '@/types/response/deleteUser'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; +import { UNBIND_STATUS } from '@/types/response/unbind'; import { SemData } from '@/types/sem'; +import { ValueOf } from '@/types/tools'; +import { TUserExist } from '@/types/user'; +import { type AxiosInstance } from 'axios'; +import { ProviderType } from 'prisma/global/generated/client'; export const _getRegionToken = (request: AxiosInstance) => () => request.post>( @@ -147,11 +149,22 @@ export const _mergeUser = export const _deleteUser = (request: AxiosInstance) => () => request>('/api/auth/delete'); +export const _checkRemainResource = (request: AxiosInstance) => () => + request< + never, + ApiResp<{ regionResourceList: RegionResourceType[]; code: string }>, + RESOURCE_STATUS + >('/api/auth/delete/checkAllResource'); +export const _forceDeleteUser = (request: AxiosInstance) => (data: { code: string }) => + request.post>('/api/auth/delete/force', data); export const _realNameAuthRequest = (request: AxiosInstance) => (data: { name: string; phone?: string; idCard: string }) => request.post>('/api/account/realNameAuth', data); +export const _getAmount = (request: AxiosInstance) => () => + request>('/api/account/getAmount'); + export const passwordExistRequest = _passwordExistRequest(request); export const passwordLoginRequest = _passwordLoginRequest(request, (token) => { useSessionStore.setState({ token }); @@ -173,5 +186,9 @@ export const unBindRequest = _oauthProviderUnbind(request); export const signInRequest = _oauthProviderSignIn(request); export const mergeUserRequest = _mergeUser(request); export const deleteUserRequest = _deleteUser(request); +export const checkRemainResource = _checkRemainResource(request); +export const forceDeleteUser = _forceDeleteUser(request); export const realNameAuthRequest = _realNameAuthRequest(request); + +export const getAmount = _getAmount(request); diff --git a/frontend/desktop/src/api/namespace.ts b/frontend/desktop/src/api/namespace.ts index 3a07eac2d9b..eb2fc6c6b04 100644 --- a/frontend/desktop/src/api/namespace.ts +++ b/frontend/desktop/src/api/namespace.ts @@ -3,7 +3,6 @@ import { ApiResp } from '@/types'; import { NamespaceDto, UserRole, teamMessageDto } from '@/types/team'; import { TeamUserDto } from '@/types/user'; import { AxiosInstance } from 'axios'; -import { Session } from 'sealos-desktop-sdk/*'; export const _abdicateRequest = (request: AxiosInstance) => (data: { ns_uid: string; targetUserCrUid: string }) => diff --git a/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx index c667a2c1a03..54900c7f836 100644 --- a/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/DeleteAccountModal.tsx @@ -1,89 +1,131 @@ -import { useCustomToast } from '@/hooks/useCustomToast'; +import { checkRemainResource, forceDeleteUser, getAmount } from '@/api/auth'; +import { RegionResourceType, ResourceType } from '@/services/backend/svc/checkResource'; +import useAppStore from '@/stores/app'; import useSessionStore from '@/stores/session'; import { ValueOf } from '@/types'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; import { - ButtonProps, - useDisclosure, - Text, Button, + ButtonProps, + Center, + Divider, + FormControl, + HStack, + IconButton, Modal, - ModalOverlay, - ModalContent, + ModalBody, ModalCloseButton, + ModalContent, ModalHeader, + ModalOverlay, Spinner, - ModalBody, - VStack, - HStack, - FormControl, - Divider + Table, + TableContainer, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + useDisclosure, + VStack } from '@chakra-ui/react'; -import { InfoCircleIcon, WarnTriangeIcon } from '@sealos/ui'; -import { useQueryClient, useMutation } from '@tanstack/react-query'; -import { useState } from 'react'; +import { InfoCircleIcon, LinkIcon, WarnTriangeIcon } from '@sealos/ui'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; import { SettingInput } from './SettingInput'; import { SettingInputGroup } from './SettingInputGroup'; -import { useRouter } from 'next/router'; -import { RESOURCE_STATUS } from '@/types/response/checkResource'; -import { deleteUserRequest } from '@/api/auth'; -import { I18nErrorKey } from '@/types/i18next'; enum PageStatus { IDLE, - REMAIN_OTHER_REGION_RESOURCE, - REMAIN_WORKSPACE, - REMAIN_APP, - REMAIN_DATABASE, - REMAIN_OBJECT_STORAGE, - REMAIN_TEMPLATE, - INSUFFICIENT_BALANCE + REMAIN_RESOURCES, + FORCE_DELETE } +const appKeyList = [ + '', + 'system-dbprovider', + 'system-applaunchpad', + '', + 'system-cronjob', + '', + 'system-objectstorage', + 'system-cloudserver', + 'system-template' +] as const; +const generateURL = (resource: ResourceType, domain: string) => { + const appKey = appKeyList[resource.type]; + if (!appKey) return ''; + let openapp: string = appKey; + let url = `https://${domain}?workspaceUid=${resource.workspace.uid}&openapp=${openapp}`; + return url; +}; export default function DeleteAccount({ ...props }: ButtonProps) { - const { onOpen, isOpen, onClose } = useDisclosure(); + const { onOpen, isOpen, onClose: _onClose } = useDisclosure(); const { t } = useTranslation(); + const { t: cloudProvidersT } = useTranslation('cloudProviders'); + const { t: appT } = useTranslation('applist'); const queryClient = useQueryClient(); - const { toast } = useCustomToast({ status: 'error' }); const { delSession, setToken, session } = useSessionStore(); const [pagestatus, setPagestatus] = useState(PageStatus.IDLE); const [nickname, setnickname] = useState(''); const verifyWords = t('common:deletemyaccount'); + const forceDeleteKeywords = t('common:force_delete_keywords'); const router = useRouter(); const [verifyValue, setVerifyValue] = useState(''); - const mutation = useMutation({ - mutationFn: deleteUserRequest, + const [forceDeleteValue, setForceDeleteValue] = useState(''); + const [code, setCode] = useState(''); + const { openApp } = useAppStore(); + const appType = [ + '', // 0 + appT('db'), // 1 + appT('app'), // 2 + appT('terminal'), // 3 + appT('job'), // 4 + appT('other'), // 5 + appT('object-storage'), // 6 + appT('cloud-vm'), // 7 + appT('app-store') // 8 + ]; + const onClose = () => { + setPagestatus(PageStatus.IDLE); + setForceDeleteValue(''); + setVerifyValue(''); + setnickname(''); + setCode(''); + _onClose(); + }; + const mutationForce = useMutation({ + mutationFn: forceDeleteUser, onSuccess() { delSession(); queryClient.clear(); setToken(''); router.replace('/signin'); setToken(''); + } + }); + + const { data: amountData } = useQuery({ + queryKey: ['getAmount', { userId: session?.user?.userCrUid }], + queryFn: getAmount, + enabled: !!session?.user, + staleTime: 60 * 1000 + }); + const isInsufficientBalance = + (amountData?.data?.balance || 0) < (amountData?.data?.deductionBalance || 0); + + const mutationCheck = useMutation({ + mutationFn: checkRemainResource, + onSuccess(data) { + if (data.message === RESOURCE_STATUS.REMAIN_RESOURCE) + setPagestatus(PageStatus.REMAIN_RESOURCES); }, - onError(error: { message: ValueOf }) { - const message = error?.message; - if (message === RESOURCE_STATUS.REMAIN_APP) { - return setPagestatus(PageStatus.REMAIN_APP); - } else if (message === RESOURCE_STATUS.REMAIN_DATABASE) { - return setPagestatus(PageStatus.REMAIN_DATABASE); - } else if (message === RESOURCE_STATUS.REMAIN_OBJECT_STORAGE) { - return setPagestatus(PageStatus.REMAIN_OBJECT_STORAGE); - } else if (message === RESOURCE_STATUS.REMAIN_TEMPLATE) { - return setPagestatus(PageStatus.REMAIN_TEMPLATE); - } else if (message === RESOURCE_STATUS.REMAIN_OTHER_REGION_RESOURCE) { - return setPagestatus(PageStatus.REMAIN_OTHER_REGION_RESOURCE); - } else if (message === RESOURCE_STATUS.INSUFFICENT_BALANCE) { - return setPagestatus(PageStatus.INSUFFICIENT_BALANCE); - } else if (message === RESOURCE_STATUS.REMAIN_WORKSACE_OWNER) { - return setPagestatus(PageStatus.REMAIN_WORKSPACE); - } else { - setPagestatus(PageStatus.IDLE); - toast({ title: t(message as I18nErrorKey, { ns: 'error' }) }); - } + onError(data: { code: number; message: ValueOf; data: RegionResourceType[] }) { + if (data.message === RESOURCE_STATUS.REMAIN_RESOURCE) + setPagestatus(PageStatus.REMAIN_RESOURCES); } }); - const deleteModalOnClose = () => { - setPagestatus(PageStatus.IDLE); - onClose(); - }; return ( <> { @@ -98,151 +140,248 @@ export default function DeleteAccount({ ...props }: ButtonProps) { {t('common:delete_account_button')} } - + - - + {t('common:delete_account_title')} - {mutation.isLoading ? ( - - ) : ( - - {pagestatus === PageStatus.IDLE ? ( - + + + {mutationCheck.isLoading || mutationForce.isLoading ? ( +
+ +
+ ) : ( + + {pagestatus === PageStatus.IDLE ? ( {t('common:deleteaccounttitle')} + ) : pagestatus === PageStatus.REMAIN_RESOURCES ? ( + {t('common:delete_account_remain_resources')} + ) : ( + {t('common:force_delete_tips')} + )} + {isInsufficientBalance && ( - {t('common:irreversibleactiontips')} + {t('common:insufficient_balance')} - - - - {t('common:enter')} - {session?.user.name} - {t('common:confirm')} - - - { - setnickname(e.target.value); - }} - /> - - - + )} + + + {t('common:irreversibleactiontips')} + + + {pagestatus !== PageStatus.REMAIN_RESOURCES ? ( + + ) : null} + {pagestatus === PageStatus.IDLE ? ( + <> + {' '} + + + {t('common:please_enter_username')} + {session?.user.name} + {t('common:confirm')} + + + { + setnickname(e.target.value); + }} + /> + + + + + {t('common:please_enter')} + {verifyWords} + {t('common:confirm_again')} + + + { + setVerifyValue(e.target.value); + }} + /> + + + + ) : pagestatus === PageStatus.REMAIN_RESOURCES ? ( + + + + + {[ + t('common:region'), + t('common:team'), + appT('app_type'), + t('common:link') + ].map((t, index, arr) => ( + + ))} + + + + {(mutationCheck.data?.data?.regionResourceList || []) + .flatMap((item) => + item.resource + .filter((item) => [1, 2, 4, 6, 7, 8].includes(item.type)) + .map( + (resouce) => + [ + item.region.displayName, + resouce.workspace.displayName, + appType[resouce.type], + generateURL(resouce, item.region.domain) + ] as const + ) + ) + .map((item, index) => ( + + {[ + {cloudProvidersT(item[0] as any)}, + {item[1]}, + {item[2]}, + item[3] ? ( + } + minW={'auto'} + variant={'ghost'} + boxSize={'32px'} + color={'grayModern.600'} + aria-label={''} + onClick={() => { + window.open(item[3], '_self'); + }} + /> + ) : ( + - + ) + ].map((item, index) => ( + + ))} + + ))} + +
+ {t} +
+ {item} +
+
+ ) : ( + - {t('common:enter')} - {verifyWords} + {t('common:please_enter')} + {forceDeleteKeywords} {t('common:confirm')} { - setVerifyValue(e.target.value); + setForceDeleteValue(e.target.value); }} /> - - - - -
- ) : ( - - {pagestatus === PageStatus.INSUFFICIENT_BALANCE ? ( - {t('common:insufficient_balance_tips')} - ) : pagestatus === PageStatus.REMAIN_APP ? ( - {t('common:remain_app_tips')} - ) : pagestatus === PageStatus.REMAIN_TEMPLATE ? ( - {t('common:remain_template_tips')} - ) : pagestatus === PageStatus.REMAIN_OBJECT_STORAGE ? ( - {t('common:remain_objectstorage_tips')} - ) : pagestatus === PageStatus.REMAIN_DATABASE ? ( - {t('common:remain_database_tips')} - ) : pagestatus === PageStatus.REMAIN_WORKSPACE ? ( - {t('common:remain_workspace_tips')} - ) : pagestatus === PageStatus.REMAIN_OTHER_REGION_RESOURCE ? ( - {t('common:remain_other_region_resource_tips')} - ) : null} - + - - - - )} -
- )} + {t('common:cancel')} + + + +
+ )} +
diff --git a/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx b/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx index 9b3d6563a2f..defdfe6a7b7 100644 --- a/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/SettingInputGroup.tsx @@ -1,4 +1,4 @@ -import { InputProps, Flex, Input, forwardRef, InputGroup, InputGroupProps } from '@chakra-ui/react'; +import { forwardRef, InputGroup, InputGroupProps } from '@chakra-ui/react'; export const SettingInputGroup = forwardRef(function SettingInputGroup( props, @@ -10,7 +10,8 @@ export const SettingInputGroup = forwardRef(function Set as={'div'} flex={1} borderRadius="6px" - border="1px solid #DEE0E2" + border="1px solid " + borderColor={'grayModern.200'} bgColor={'grayModern.50'} alignItems={'center'} py={'8px'} diff --git a/frontend/desktop/src/components/account/cost.tsx b/frontend/desktop/src/components/account/cost.tsx index 485f8e16136..3d6b73ea90e 100644 --- a/frontend/desktop/src/components/account/cost.tsx +++ b/frontend/desktop/src/components/account/cost.tsx @@ -1,9 +1,8 @@ +import { getAmount } from '@/api/auth'; import { getUserBilling } from '@/api/platform'; -import request from '@/services/request'; import useAppStore from '@/stores/app'; import { useConfigStore } from '@/stores/config'; import useSessionStore from '@/stores/session'; -import { ApiResp } from '@/types'; import { formatMoney } from '@/utils/format'; import { Accordion, @@ -22,10 +21,10 @@ import { useQuery } from '@tanstack/react-query'; import { Decimal } from 'decimal.js'; import { useTranslation } from 'next-i18next'; import { useMemo } from 'react'; +import CustomTooltip from '../AppDock/CustomTooltip'; import { blurBackgroundStyles } from '../desktop_content'; import Monitor from '../desktop_content/monitor'; import { ClockIcon, DesktopSealosCoinIcon, HelpIcon, InfiniteIcon } from '../icons'; -import CustomTooltip from '../AppDock/CustomTooltip'; export default function Cost() { const { t } = useTranslation(); @@ -38,10 +37,7 @@ export default function Cost() { const { data } = useQuery({ queryKey: ['getAmount', { userId: user?.userCrUid }], - queryFn: () => - request>( - '/api/account/getAmount' - ), + queryFn: getAmount, enabled: !!user, staleTime: 60 * 1000 }); diff --git a/frontend/desktop/src/components/account/github.tsx b/frontend/desktop/src/components/account/github.tsx index efb82ab222a..7207b89b7c8 100644 --- a/frontend/desktop/src/components/account/github.tsx +++ b/frontend/desktop/src/components/account/github.tsx @@ -1,16 +1,6 @@ -import { useConfigStore } from '@/stores/config'; -import { Flex, FlexProps, Icon, Tooltip } from '@chakra-ui/react'; -import { useQuery } from '@tanstack/react-query'; +import { Flex, FlexProps, Icon } from '@chakra-ui/react'; export default function GithubComponent(props: FlexProps) { - // const { data } = useQuery( - // ['getGithubStar'], - // () => fetch('https://api.github.com/repos/labring/sealos').then((res) => res.json()), - // { - // staleTime: 24 * 60 * 60 * 1000 - // } - // ); - return ( ), - click: () => { - console.log(111); - } + click: () => {} } ]; diff --git a/frontend/desktop/src/components/team/WorkspaceToggle.tsx b/frontend/desktop/src/components/team/WorkspaceToggle.tsx index 1c5cf3af2f6..c9b02ef69bf 100644 --- a/frontend/desktop/src/components/team/WorkspaceToggle.tsx +++ b/frontend/desktop/src/components/team/WorkspaceToggle.tsx @@ -7,7 +7,7 @@ import { NSType } from '@/types/team'; import { AccessTokenPayload } from '@/types/token'; import { sessionConfig } from '@/utils/sessionConfig'; import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; -import { Box, Divider, HStack, Text, VStack, useDisclosure } from '@chakra-ui/react'; +import { Box, HStack, Text, VStack, useDisclosure } from '@chakra-ui/react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { jwtDecode } from 'jwt-decode'; import { useTranslation } from 'next-i18next'; @@ -49,12 +49,6 @@ export default function WorkspaceToggle() { const namespaces = data?.data?.namespaces || []; const namespace = namespaces.find((x) => x.uid === ns_uid); - const defaultNamespace = namespaces.find((x) => x.nstype === NSType.Private); - - if (!namespace && defaultNamespace && namespaces.length > 0) { - // will be deleted - switchTeam({ uid: defaultNamespace.uid }); - } return ( showDisclosure.onOpen(), content: } - // { - // key: UserMenuKeys.Account, - // button: ( - // user avator - // ), - // click: () => accountDisclosure.onOpen(), - // content: - // } ]; return ( diff --git a/frontend/desktop/src/pages/_app.tsx b/frontend/desktop/src/pages/_app.tsx index 894000d3968..2205a6583b4 100644 --- a/frontend/desktop/src/pages/_app.tsx +++ b/frontend/desktop/src/pages/_app.tsx @@ -1,23 +1,21 @@ +import { useConfigStore } from '@/stores/config'; import { theme } from '@/styles/chakraTheme'; import '@/styles/globals.scss'; import { getCookie } from '@/utils/cookieUtils'; import { ChakraProvider } from '@chakra-ui/react'; +import '@sealos/driver/src/driver.css'; import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { appWithTranslation, useTranslation } from 'next-i18next'; import type { AppProps } from 'next/app'; import Router from 'next/router'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; -import '@sealos/driver/src/driver.css'; import { useEffect } from 'react'; -import { useConfigStore } from '@/stores/config'; const queryClient = new QueryClient({ defaultOptions: { queries: { - // refetchOnWindowFocus: false, retry: false - // cacheTime: 0 } } }); diff --git a/frontend/desktop/src/pages/api/auth/checkStatus.ts b/frontend/desktop/src/pages/api/auth/checkStatus.ts new file mode 100644 index 00000000000..1476a6e16ea --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/checkStatus.ts @@ -0,0 +1,7 @@ +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { checkUserStatusSvc } from '@/services/backend/svc/access'; +import { NextApiRequest, NextApiResponse } from 'next'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAccessToken(req, res, async ({ userUid }) => checkUserStatusSvc(userUid)(res)); +}); diff --git a/frontend/desktop/src/pages/api/auth/delete/checkAllResource.ts b/frontend/desktop/src/pages/api/auth/delete/checkAllResource.ts new file mode 100644 index 00000000000..b7d3c12a3c6 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/delete/checkAllResource.ts @@ -0,0 +1,9 @@ +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { allRegionResourceSvc } from '@/services/backend/svc/checkResource'; +import { NextApiRequest, NextApiResponse } from 'next'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAccessToken(req, res, async ({ userId, userUid, userCrName }) => { + await allRegionResourceSvc(userUid, userId, userCrName)(res); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/delete/checkCurrentResource.ts b/frontend/desktop/src/pages/api/auth/delete/checkCurrentResource.ts new file mode 100644 index 00000000000..947edb08657 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/delete/checkCurrentResource.ts @@ -0,0 +1,9 @@ +import { filterAuthenticationToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { checkCurrentResourceSvc } from '@/services/backend/svc/checkResource'; +import { NextApiRequest, NextApiResponse } from 'next'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAuthenticationToken(req, res, async ({ userId, userUid }) => { + await checkCurrentResourceSvc(userId, userUid)(res); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/delete/force.ts b/frontend/desktop/src/pages/api/auth/delete/force.ts new file mode 100644 index 00000000000..7f7d503f4bf --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/delete/force.ts @@ -0,0 +1,12 @@ +import { filterAccessToken } from '@/services/backend/middleware/access'; +import { filterForceDelete } from '@/services/backend/middleware/checkResource'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { forceDeleteUserSvc } from '@/services/backend/svc/deleteUser'; +import { NextApiRequest, NextApiResponse } from 'next'; +export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { + await filterAccessToken(req, res, async ({ userUid, userId, userCrName }) => { + await filterForceDelete(req, res, async ({ code }) => { + await forceDeleteUserSvc(userUid, code)(res); + }); + }); +}); diff --git a/frontend/desktop/src/pages/api/auth/namespace/create.ts b/frontend/desktop/src/pages/api/auth/namespace/create.ts index be19019f99f..c654d135a62 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/create.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/create.ts @@ -1,20 +1,19 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { prisma } from '@/services/backend/db/init'; import { getTeamKubeconfig } from '@/services/backend/kubernetes/admin'; import { GetUserDefaultNameSpace } from '@/services/backend/kubernetes/user'; +import { get_k8s_username } from '@/services/backend/regionAuth'; import { jsonRes } from '@/services/backend/response'; import { bindingRole, modifyWorkspaceRole } from '@/services/backend/team'; import { getTeamLimit } from '@/services/enable'; import { NSType, NamespaceDto, UserRole } from '@/types/team'; import { NextApiRequest, NextApiResponse } from 'next'; -import { prisma } from '@/services/backend/db/init'; -import { get_k8s_username } from '@/services/backend/regionAuth'; -import { verifyAccessToken } from '@/services/backend/auth'; const TEAM_LIMIT = getTeamLimit(); 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 verify error' }); - // const { user: tokenUser } = payload; const { teamName } = req.body as { teamName?: string }; if (!teamName) return jsonRes(res, { code: 400, message: 'teamName is required' }); const currentNamespaces = await prisma.userWorkspace.findMany({ diff --git a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts index 4769b2a41ec..ebde0efbfef 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts @@ -1,12 +1,12 @@ import { verifyAccessToken } from '@/services/backend/auth'; +import { prisma } from '@/services/backend/db/init'; import { jsonRes } from '@/services/backend/response'; import { modifyBinding, modifyWorkspaceRole } from '@/services/backend/team'; import { UserRole } from '@/types/team'; import { isUserRole, roleToUserRole, vaildManage } from '@/utils/tools'; import { NextApiRequest, NextApiResponse } from 'next'; -import { prisma } from '@/services/backend/db/init'; -import { validate } from 'uuid'; import { JoinStatus } from 'prisma/region/generated/client'; +import { validate } from 'uuid'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -25,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return jsonRes(res, { code: 400, message: 'ns_uid is invalid' }); if (targetUserCrUid === payload.userCrUid) return jsonRes(res, { code: 403, message: 'target user is not self' }); - // 翻出utn + // get utn const queryResults = await prisma.userWorkspace.findMany({ where: { workspaceUid: ns_uid, @@ -47,7 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!vaildFn(roleToUserRole(targetUser.role), targetUser.userCrUid === own.userCrUid)) return jsonRes(res, { code: 403, message: 'you are not manager' }); - // 权限一致,不用管 + // if role is same, do nothing if (roleToUserRole(targetUser.role) === tRole) return jsonRes(res, { code: 200, message: 'Successfully' }); diff --git a/frontend/desktop/src/pages/api/auth/publicWechat/handleWechat.ts b/frontend/desktop/src/pages/api/auth/publicWechat/handleWechat.ts index 651a3a19bd1..22772b6ac81 100644 --- a/frontend/desktop/src/pages/api/auth/publicWechat/handleWechat.ts +++ b/frontend/desktop/src/pages/api/auth/publicWechat/handleWechat.ts @@ -1,6 +1,6 @@ -import { NextApiRequest, NextApiResponse } from 'next'; import { addOrUpdateWechatCode } from '@/services/backend/db/wechatCode'; import crypto from 'crypto'; +import { NextApiRequest, NextApiResponse } from 'next'; const xml2js = require('xml2js'); type MessageType = 'event' | 'text' | 'image'; @@ -106,7 +106,6 @@ export function textMsg(message: WechatMessage, content: string) { export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - console.log(req.body, req.method, '===\n'); if (req.method === 'GET') { const str = verifyWeChatRequest(req); return res.send(str); diff --git a/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts b/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts index 360146f8baf..51cb73f379d 100644 --- a/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts +++ b/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts @@ -1,16 +1,25 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; import { K8sApi, ListCRD } from '@/services/backend/kubernetes/user'; import { jsonRes } from '@/services/backend/response'; import { CRDMeta, TAppCRList, TAppConfig } from '@/types'; -import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; -import { verifyAccessToken } from '@/services/backend/auth'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { globalPrisma } from '@/services/backend/db/init'; import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; +import { UserStatus } from 'prisma/global/generated/client'; 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 user = await globalPrisma.user.findUnique({ + where: { + uid: payload.userUid, + status: UserStatus.NORMAL_USER + } + }); + if (!user) return jsonRes(res, { code: 401, message: 'user is locked' }); const _kc = await getUserKubeconfigNotPatch(payload.userCrName); if (!_kc) return jsonRes(res, { code: 404, message: 'user is not found' }); const realKc = switchKubeconfigNamespace(_kc, payload.workspaceId); diff --git a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts index 3b90c3f61fe..b375b4b9c8c 100644 --- a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts @@ -1,13 +1,13 @@ import { jsonRes } from '@/services/backend/response'; import { AppConfigType, - DefaultAuthClientConfig, + AuthClientConfigType, AuthConfigType, - AuthClientConfigType + DefaultAuthClientConfig } from '@/types/system'; import { readFileSync } from 'fs'; -import type { NextApiRequest, NextApiResponse } from 'next'; import yaml from 'js-yaml'; +import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const config = await getAuthClientConfig(); @@ -53,7 +53,8 @@ function genResAuthClientConfig(conf: AuthConfigType) { } }, proxyAddress: conf.proxyAddress || '', - hasBaiduToken: !!conf.baiduToken + hasBaiduToken: !!conf.baiduToken, + billingToken: '' }; return authClientConfig; } diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index 23004771b2d..cb125e75d70 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -1,19 +1,26 @@ +import { nsListRequest, switchRequest } from '@/api/namespace'; import DesktopContent from '@/components/desktop_content'; import useAppStore from '@/stores/app'; +import useCallbackStore from '@/stores/callback'; import { useConfigStore } from '@/stores/config'; import useSessionStore from '@/stores/session'; +import { SemData } from '@/types/sem'; +import { NSType } from '@/types/team'; +import { AccessTokenPayload } from '@/types/token'; import { parseOpenappQuery } from '@/utils/format'; -import { setBaiduId, setInviterId, setUserSemData } from '@/utils/sessionConfig'; +import { sessionConfig, setBaiduId, setInviterId, setUserSemData } from '@/utils/sessionConfig'; +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; import { compareFirstLanguages } from '@/utils/tools'; import { Box, useColorMode } from '@chakra-ui/react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { jwtDecode } from 'jwt-decode'; +import { isString } from 'lodash'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import { createContext, useEffect, useState } from 'react'; -import useCallbackStore from '@/stores/callback'; +import { createContext, useEffect, useMemo, useState } from 'react'; import 'react-contexify/dist/ReactContexify.css'; -import { SemData } from '@/types/sem'; const destination = '/signin'; interface IMoreAppsContext { @@ -28,15 +35,41 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str const { colorMode, toggleColorMode } = useColorMode(); const init = useAppStore((state) => state.init); const setAutoLaunch = useAppStore((state) => state.setAutoLaunch); + const { autolaunchWorkspaceUid } = useAppStore(); + const { session } = useSessionStore(); const { layoutConfig } = useConfigStore(); const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore(); - useEffect(() => { colorMode === 'dark' ? toggleColorMode() : null; }, [colorMode, toggleColorMode]); const [showMoreApps, setShowMoreApps] = useState(false); + const queryClient = useQueryClient(); + const swtichWorksapceMutation = useMutation({ + mutationFn: switchRequest, + async onSuccess(data) { + if (data.code === 200 && !!data.data && session) { + const payload = jwtDecode(data.data.token); + await sessionConfig({ + ...data.data, + kubeconfig: switchKubeconfigNamespace(session.kubeconfig, payload.workspaceId) + }); + queryClient.clear(); + } else { + throw Error('session in invalid'); + } + } + }); + const workspaceQuery = useQuery({ + queryKey: ['teamList', 'teamGroup'], + queryFn: nsListRequest, + enabled: isUserLogin() + }); + const workspaces = useMemo( + () => workspaceQuery?.data?.data?.namespaces || [], + [workspaceQuery.data] + ); - // openApp by query + // openApp by query && switch workspace useEffect(() => { const { query } = router; const is_login = isUserLogin(); @@ -55,33 +88,92 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str ); return; } - if (appkey && typeof appQuery === 'string') setAutoLaunch(appkey, { raw: appQuery }); + let workspaceUid: string | undefined; + if (isString(query?.workspaceUid)) workspaceUid = query.workspaceUid; + if (appkey && typeof appQuery === 'string') + setAutoLaunch(appkey, { raw: appQuery }, workspaceUid); router.replace(destination); } else { - init().then((state) => { - let appQuery = ''; - let appkey = ''; - if (!state.autolaunch) { - const result = parseOpenappQuery((query?.openapp as string) || ''); - appQuery = result.appQuery; - appkey = result.appkey; - if (!!query.openapp) router.replace(router.pathname); - } else { - appkey = state.autolaunch; - appQuery = state.launchQuery.raw; - } - if (!appkey) return; - if (appkey === 'system-fastdeploy') { - appkey = 'system-template'; - } - const app = state.installedApps.find((item) => item.key === appkey); - if (!app) return; - state.openApp(app, { raw: appQuery }).then(() => { - state.cancelAutoLaunch(); + let workspaceUid: string | undefined; + // Check if there's no autolaunch workspace UID + if (!autolaunchWorkspaceUid) { + // Use workspace UID from query if no autolaunch + if (isString(query?.workspaceUid)) workspaceUid = query.workspaceUid; + } else { + // Use autolaunch workspace UID if available + workspaceUid = autolaunchWorkspaceUid; + } + Promise.resolve() + .then(() => { + if (!workspaceUid) { + return Promise.resolve(); + } + return swtichWorksapceMutation + .mutateAsync(workspaceUid) + .then((data) => { + return Promise.resolve(); + }) + .catch((err) => { + // workspace not found or other error + console.error(err); + return Promise.resolve(); + }); + }) + .then(() => { + return init(); + }) + .then((state) => { + let appQuery = ''; + let appkey = ''; + let appRoute = ''; + if (!state.autolaunch) { + const result = parseOpenappQuery((query?.openapp as string) || ''); + appQuery = result.appQuery; + appkey = result.appkey; + if (!!query.openapp) router.replace(router.pathname); + } else { + appkey = state.autolaunch; + appQuery = state.launchQuery.raw; + } + if (!appkey) return; + if (appkey === 'system-fastdeploy') { + appkey = 'system-template'; + } + const app = state.installedApps.find((item) => item.key === appkey); + if (!app) return; + state.openApp(app, { raw: appQuery, pathname: appRoute }).then(() => { + state.cancelAutoLaunch(); + }); }); - }); } - }, [router, init, setAutoLaunch, sealos_cloud_domain]); + }, [router, sealos_cloud_domain]); + + // check workspace + useEffect(() => { + if (swtichWorksapceMutation.isLoading) return; + let workspaceUid: string | undefined; + // Check if there's no autolaunch workspace UID + const currentWorkspaceUid = session?.user?.ns_uid; + if (currentWorkspaceUid) { + workspaceUid = currentWorkspaceUid; + } + // Ensure workspaces exist + if (workspaces.length === 0) { + console.log('No workspaces found'); + // throw new Error('No workspaces found'); + } + const needDefault = + workspaces.findIndex((w) => w.uid === workspaceUid) === -1 && workspaces.length > 0; + if (!needDefault) { + return; + } + const defaultWorkspace = workspaces.find((w) => w.nstype === NSType.Private); + // Fallback to default workspace UID + workspaceUid = defaultWorkspace?.uid; + + if (!workspaceUid) return; + swtichWorksapceMutation.mutate(workspaceUid); + }, [session?.user?.ns_uid, workspaces]); // handle baidu useEffect(() => { @@ -133,12 +225,11 @@ export async function getServerSideProps({ req, res, locales }: any) { res.setHeader('Set-Cookie', `NEXT_LOCALE=${local}; Max-Age=2592000; Secure; SameSite=None`); const sealos_cloud_domain = global.AppConfig?.cloud.domain || 'cloud.sealos.io'; - return { props: { ...(await serverSideTranslations( local, - ['common', 'cloudProviders', 'error'], + ['common', 'cloudProviders', 'error', 'applist'], null, locales || [] )), diff --git a/frontend/desktop/src/pages/switchRegion.tsx b/frontend/desktop/src/pages/switchRegion.tsx index 98fa84fc2d1..235ed7a21b1 100644 --- a/frontend/desktop/src/pages/switchRegion.tsx +++ b/frontend/desktop/src/pages/switchRegion.tsx @@ -1,16 +1,18 @@ -import type { NextPage } from 'next'; -import { useRouter } from 'next/router'; -import { useEffect } from 'react'; -import useSessionStore from '@/stores/session'; -import { Flex, Spinner } from '@chakra-ui/react'; import { getRegionToken } from '@/api/auth'; -import { isString } from 'lodash'; -import { jwtDecode } from 'jwt-decode'; +import { nsListRequest, switchRequest } from '@/api/namespace'; +import useAppStore from '@/stores/app'; +import useSessionStore from '@/stores/session'; import { AccessTokenPayload } from '@/types/token'; +import { parseOpenappQuery } from '@/utils/format'; import { sessionConfig } from '@/utils/sessionConfig'; -import { useMutation } from '@tanstack/react-query'; -import { nsListRequest, switchRequest } from '@/api/namespace'; import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; +import { Flex, Spinner } from '@chakra-ui/react'; +import { useMutation } from '@tanstack/react-query'; +import { jwtDecode } from 'jwt-decode'; +import { isString } from 'lodash'; +import type { NextPage } from 'next'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; const Callback: NextPage = () => { const router = useRouter(); @@ -18,6 +20,7 @@ const Callback: NextPage = () => { const delSession = useSessionStore((s) => s.delSession); const { token: curToken, session } = useSessionStore((s) => s); const { lastWorkSpaceId } = useSessionStore(); + const { setAutoLaunch } = useAppStore(); const mutation = useMutation({ mutationFn: switchRequest, @@ -36,6 +39,13 @@ const Callback: NextPage = () => { useEffect(() => { if (!router.isReady) return; + const { query } = router; + const { appkey, appQuery } = parseOpenappQuery((query?.openapp as string) || ''); + let workspaceUid: string | undefined; + if (isString(query?.workspaceUid)) workspaceUid = query.workspaceUid; + if (appkey && typeof appQuery === 'string') { + setAutoLaunch(appkey, { raw: appQuery }, workspaceUid); + } (async () => { try { if (!!curToken) { diff --git a/frontend/desktop/src/services/backend/auth.ts b/frontend/desktop/src/services/backend/auth.ts index 0d8f114ff8d..013fdae2df1 100644 --- a/frontend/desktop/src/services/backend/auth.ts +++ b/frontend/desktop/src/services/backend/auth.ts @@ -1,13 +1,19 @@ +import { JWTPayload } from '@/types'; +import { + AccessTokenPayload, + AuthenticationTokenPayload, + BillingTokenPayload, + CronJobTokenPayload, + OnceTokenPayload +} from '@/types/token'; import { IncomingHttpHeaders } from 'http'; import { sign, verify } from 'jsonwebtoken'; -import { JWTPayload } from '@/types'; -import { AuthenticationTokenPayload, AccessTokenPayload, CronJobTokenPayload } from '@/types/token'; const regionUID = () => global.AppConfig?.cloud.regionUID || '123456789'; const grobalJwtSecret = () => global.AppConfig?.desktop.auth.jwt.global || '123456789'; const regionalJwtSecret = () => global.AppConfig?.desktop.auth.jwt.regional || '123456789'; const internalJwtSecret = () => global.AppConfig?.desktop.auth.jwt.internal || '123456789'; - +const billingJwtSecret = () => global.AppConfig?.desktop.auth.jwt.billing || '123456789'; const verifyToken = async (header: IncomingHttpHeaders) => { try { if (!header?.authorization) { @@ -57,6 +63,9 @@ export const verifyJWT = (token?: string, secret? } }); }); + +export const generateBillingToken = (props: BillingTokenPayload) => + sign(props, billingJwtSecret(), { expiresIn: '3600000' }); export const generateAccessToken = (props: AccessTokenPayload) => sign(props, regionalJwtSecret(), { expiresIn: '7d' }); export const generateAppToken = (props: AccessTokenPayload) => @@ -64,5 +73,8 @@ export const generateAppToken = (props: AccessTokenPayload) => export const generateAuthenticationToken = (props: AuthenticationTokenPayload) => sign(props, grobalJwtSecret(), { expiresIn: '60000' }); +export const generateOnceToken = (props: OnceTokenPayload) => + sign(props, regionalJwtSecret(), { expiresIn: '1800000' }); + export const generateCronJobToken = (props: CronJobTokenPayload) => sign(props, internalJwtSecret(), { expiresIn: '60000' }); diff --git a/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts b/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts index 880cd60f831..9af925f2845 100644 --- a/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts +++ b/frontend/desktop/src/services/backend/cronjob/deleteUserCr.ts @@ -1,9 +1,10 @@ -import { setUserDelete } from '../kubernetes/admin'; -import { globalPrisma, prisma } from '../db/init'; -import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; import { CronJobStatus } from '@/services/backend/cronjob/index'; -import { DeleteUserEvent } from '@/types/db/event'; import { getRegionUid } from '@/services/enable'; +import { DeleteUserEvent } from '@/types/db/event'; +import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; +import { Role } from 'prisma/region/generated/client'; +import { globalPrisma, prisma } from '../db/init'; +import { setUserDelete, setUserWorkspaceLock as setWorkspaceLock } from '../kubernetes/admin'; export class DeleteUserCrJob implements CronJobStatus { private userUid = ''; @@ -25,8 +26,16 @@ export class DeleteUserCrJob implements CronJobStatus { async unit() { await this.init(); const userUid = this.userUid; + // lock owner workspace const userCr = await prisma.userCr.findUnique({ - where: { userUid } + where: { userUid }, + include: { + userWorkspace: { + include: { + workspace: true + } + } + } }); if (!userCr) { await globalPrisma.eventLog.create({ @@ -42,7 +51,48 @@ export class DeleteUserCrJob implements CronJobStatus { }); return; // throw new Error('the userCR not found'); - } + } // lock owner workspace + const workspaceList = userCr.userWorkspace + .filter((item) => item.role === Role.OWNER) + .map((item) => item.workspace); + await Promise.all( + workspaceList.map(async (workspace) => { + await setWorkspaceLock(workspace.id); + // kick out all user workspace except owner + await prisma.userWorkspace.deleteMany({ + where: { + workspaceUid: workspace.uid, + role: { + not: Role.OWNER + } + } + }); + await globalPrisma.eventLog.create({ + data: { + eventName: DeleteUserEvent['_SET_LOCK_WORKSPACE'], + mainId: userUid, + data: JSON.stringify({ + userUid, + userCrName: userCr.crName, + workspaceId: workspace.id, + regionUid: getRegionUid(), + message: `delete user success` + }) + } + }); + }) + ); + + // kick off self from other workspace, igonre rolebinding + const clearResult = await prisma.userWorkspace.deleteMany({ + where: { + userCrUid: userCr.uid, + role: { + not: Role.OWNER + } + } + }); + const deleteResult = await setUserDelete(userCr.crName); if (!deleteResult) { @@ -59,11 +109,6 @@ export class DeleteUserCrJob implements CronJobStatus { }) } }); - await prisma.userWorkspace.deleteMany({ - where: { - userCrUid: userCr.uid - } - }); } canCommit() { return true; diff --git a/frontend/desktop/src/services/backend/cronjob/index.ts b/frontend/desktop/src/services/backend/cronjob/index.ts index b8e99a2cceb..1f7eed24303 100644 --- a/frontend/desktop/src/services/backend/cronjob/index.ts +++ b/frontend/desktop/src/services/backend/cronjob/index.ts @@ -1,14 +1,14 @@ -import { globalPrisma } from '../db/init'; +import { DeleteUserCrJob } from '@/services/backend/cronjob/deleteUserCr'; +import { MergeUserCrJob } from '@/services/backend/cronjob/mergeUserCr'; +import { getRegionUid } from '@/services/enable'; +import { Prisma } from '@prisma/client/extension'; +import dayjs from 'dayjs'; import { + Prisma as GlobalPrisma, TransactionStatus, - TransactionType, - Prisma as GlobalPrisma + TransactionType } from 'prisma/global/generated/client'; -import { getRegionUid } from '@/services/enable'; -import dayjs from 'dayjs'; -import { DeleteUserCrJob } from '@/services/backend/cronjob/deleteUserCr'; -import { Prisma } from '@prisma/client/extension'; -import { MergeUserCrJob } from '@/services/backend/cronjob/mergeUserCr'; +import { globalPrisma } from '../db/init'; export type CronJobStatus = { unit: (infoUid: string, transactionUid: string) => Promise; @@ -129,7 +129,6 @@ export const runTransactionjob = async () => { } else { return; } - // try { await job.unit(); await globalPrisma.transactionDetail.update({ where: { diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index 421d641ec16..6878b0616c9 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -1,12 +1,12 @@ -import { hashPassword, verifyPassword } from '@/utils/crypto'; -import { enableSignUp } from '../enable'; -import { globalPrisma, prisma } from '@/services/backend/db/init'; -import { ProviderType, User } from 'prisma/global/generated/client'; -import { nanoid } from 'nanoid'; +import { uploadConvertData } from '@/api/platform'; import { generateAuthenticationToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; import { AuthConfigType } from '@/types'; import { SemData } from '@/types/sem'; -import { uploadConvertData } from '@/api/platform'; +import { hashPassword } from '@/utils/crypto'; +import { nanoid } from 'nanoid'; +import { ProviderType, User, UserStatus } from 'prisma/global/generated/client'; +import { enableSignUp } from '../enable'; async function signIn({ provider, id }: { provider: ProviderType; id: string }) { const userProvider = await globalPrisma.oauthProvider.findUnique({ @@ -309,9 +309,9 @@ export const getGlobalToken = async ({ result && (user = result.user); } } - if (!user) throw new Error('Failed to edit db'); - + // user is deleted or banned + if (user.status !== UserStatus.NORMAL_USER) return null; const token = generateAuthenticationToken({ userUid: user.uid, userId: user.name diff --git a/frontend/desktop/src/services/backend/kubernetes/admin.ts b/frontend/desktop/src/services/backend/kubernetes/admin.ts index bef250d9faf..711b7fe4535 100644 --- a/frontend/desktop/src/services/backend/kubernetes/admin.ts +++ b/frontend/desktop/src/services/backend/kubernetes/admin.ts @@ -1,6 +1,6 @@ -import * as k8s from '@kubernetes/client-node'; -import { k8sFormatTime } from '@/utils/format'; import { StatusCR, UserCR } from '@/types'; +import { k8sFormatTime } from '@/utils/format'; +import * as k8s from '@kubernetes/client-node'; import { KubeConfig } from '@kubernetes/client-node'; export function K8sApiDefault(): k8s.KubeConfig { @@ -348,3 +348,44 @@ export const setUserDelete = async (k8s_username: string) => { }); return !!body; }; + +export const setUserWorkspaceLock = async (namespace: string) => { + try { + const kc = K8sApiDefault(); + const client = kc.makeApiClient(k8s.CoreV1Api); + const res = await client.patchNamespace( + namespace, + { + metadata: { + annotations: { + 'debt.sealos/status': WorkspaceDebtStatus.TerminateSuspend + } + } + }, + undefined, + undefined, + undefined, + undefined, + undefined, + { + headers: { + 'Content-Type': 'application/strategic-merge-patch+json' + } + } + ); + return ( + res.body.metadata?.annotations?.['debt.sealos/status'] === + WorkspaceDebtStatus.TerminateSuspend + ); + } catch (e) { + console.log(e); + throw e; + } +}; + +enum WorkspaceDebtStatus { + Normal = 'Normal', + Suspend = 'Suspend', + Resume = 'Resume', + TerminateSuspend = 'TerminateSuspend' +} diff --git a/frontend/desktop/src/services/backend/middleware/access.ts b/frontend/desktop/src/services/backend/middleware/access.ts index 4c8ed8ad120..bdcd205b683 100644 --- a/frontend/desktop/src/services/backend/middleware/access.ts +++ b/frontend/desktop/src/services/backend/middleware/access.ts @@ -1,7 +1,7 @@ -import { jsonRes } from '../response'; +import { AccessTokenPayload, AuthenticationTokenPayload } from '@/types/token'; import { NextApiRequest, NextApiResponse } from 'next'; import { verifyAccessToken, verifyAuthenticationToken } from '../auth'; -import { AccessTokenPayload, AuthenticationTokenPayload } from '@/types/token'; +import { jsonRes } from '../response'; export const filterAccessToken = async ( req: NextApiRequest, diff --git a/frontend/desktop/src/services/backend/middleware/checkResource.ts b/frontend/desktop/src/services/backend/middleware/checkResource.ts index 1f63487e4f0..bdd9c3b9a5a 100644 --- a/frontend/desktop/src/services/backend/middleware/checkResource.ts +++ b/frontend/desktop/src/services/backend/middleware/checkResource.ts @@ -1,10 +1,11 @@ -import { NextApiResponse } from 'next'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { generateAuthenticationToken } from '../auth'; import { globalPrisma, prisma } from '../db/init'; -import { getUserKubeconfigNotPatch, K8sApiDefault } from '../kubernetes/admin'; +import { getUserKubeconfigNotPatch } from '../kubernetes/admin'; import { jsonRes } from '../response'; -import { generateAuthenticationToken } from '../auth'; -import { JoinStatus } from 'prisma/region/generated/client'; -import { RESOURCE_STATUS } from '@/types/response/checkResource'; + export const resourceGuard = (userUid: string) => async (res: NextApiResponse, next?: () => void) => { const userCr = await prisma.userCr.findUnique({ @@ -168,3 +169,17 @@ export const otherRegionResourceGuard = } await Promise.resolve(next?.()); }; + +export const filterForceDelete = async ( + req: NextApiRequest, + res: NextApiResponse, + next: (data: { code: string }) => void +) => { + const { code } = req.body as { code: string }; + if (!code) + return jsonRes(res, { + code: 400, + message: 'invalid code' + }); + await Promise.resolve(next({ code })); +}; diff --git a/frontend/desktop/src/services/backend/svc/access.ts b/frontend/desktop/src/services/backend/svc/access.ts index ac0143770b2..847b95a6117 100644 --- a/frontend/desktop/src/services/backend/svc/access.ts +++ b/frontend/desktop/src/services/backend/svc/access.ts @@ -1,9 +1,9 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '../response'; -import { ProviderType } from 'prisma/global/generated/client'; -import { getGlobalToken } from '../globalAuth'; -import { use } from 'react'; import { SemData } from '@/types/sem'; +import { NextApiResponse } from 'next'; +import { ProviderType, UserStatus } from 'prisma/global/generated/client'; +import { globalPrisma } from '../db/init'; +import { getGlobalToken } from '../globalAuth'; +import { jsonRes } from '../response'; export const getGlobalTokenSvc = ( @@ -101,3 +101,23 @@ export const getGlobalTokenByGoogleSvc = ( semData, bdVid ); + +export const checkUserStatusSvc = + (userUid: string) => async (res: NextApiResponse, next?: () => void) => { + const user = await globalPrisma.user.findUnique({ + where: { + uid: userUid, + status: UserStatus.NORMAL_USER + } + }); + if (!user) + return jsonRes(res, { + code: 401, + message: 'Unauthorized' + }); + jsonRes(res, { + code: 200, + message: 'Successfully' + }); + next?.(); + }; diff --git a/frontend/desktop/src/services/backend/svc/checkResource.ts b/frontend/desktop/src/services/backend/svc/checkResource.ts new file mode 100644 index 00000000000..287f8617968 --- /dev/null +++ b/frontend/desktop/src/services/backend/svc/checkResource.ts @@ -0,0 +1,224 @@ +import { ApiResp } from '@/types'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import dayjs from 'dayjs'; +import { NextApiResponse } from 'next'; +import { Region } from 'prisma/global/generated/client'; +import { Role, Workspace } from 'prisma/region/generated/client'; +import { generateAuthenticationToken, generateBillingToken, generateOnceToken } from '../auth'; +import { globalPrisma, prisma } from '../db/init'; +import { jsonRes } from '../response'; +type ResourceRawType = { + namespace: string; + type: number; + parent_type?: 8; + parent_name?: string; + name?: string; +}; +export type ResourceType = { + workspace: Workspace; + type: number; + name?: string; +}; +export type RegionResourceType = { + region: Region; + resource: ResourceType[]; +}; +const checkResourceResponse = async (userId: string, userUid: string, userCrName: string) => { + const itemList = await prisma.userCr.findMany({ + where: { + userUid + }, + include: { + userWorkspace: { + include: { + workspace: true + }, + where: { + role: Role.OWNER + } + } + } + }); + const workspaceList = itemList.flatMap((item) => + item.userWorkspace.map((userWorkspace) => userWorkspace.workspace) + ); + const namespaceList = workspaceList.map((workspace) => workspace.id); + const endTime = new Date(); + const startTime = dayjs(endTime).subtract(1, 'hours').toISOString(); + const authorization = + 'Bearer ' + + encodeURI( + generateBillingToken({ + userUid, + userId + }) + ); + const response = await fetch( + `${global.AppConfig.desktop.auth.billingUrl}/account/v1alpha1/user-usage`, + { + method: 'POST', + headers: { + authorization + }, + body: JSON.stringify({ + startTime, + endTime, + namespaceList + }) + } + ); + const data = (await response.json()) as { + data: ResourceRawType[]; + }; + if (!response.ok) { + throw new Error(RESOURCE_STATUS.GET_RESOURCE_ERROR); + } + return (data.data || []) + .flatMap((d) => { + const workspace = workspaceList.find((workspace) => workspace.id === d.namespace); + if (!workspace) return []; + if (d?.parent_type === 8 && d.parent_name) { + return [ + { + workspace, + type: d.parent_type, + name: d.name + } + ]; + } + return [ + { + workspace, + type: d.type, + name: d.name + } + ]; + }) + .reduce((acc, current) => { + const x = acc.findIndex((item) => item.name === current.name); + if (x === -1) { + return acc.concat([current]); + } else { + return acc; + } + }, []); +}; +export const checkCurrentResourceSvc = + (userId: string, userUid: string) => async (res: NextApiResponse, next?: () => void) => { + const userCr = await prisma.userCr.findUnique({ + where: { + userUid + } + }); + if (!userCr) { + return jsonRes(res, { + code: 200, + message: RESOURCE_STATUS.RESULT_SUCCESS, + data: [] + }); + } + const resoucreList = await checkResourceResponse(userId, userUid, userCr.crName); + + if (resoucreList.length > 0) { + return jsonRes(res, { + code: 409, + message: RESOURCE_STATUS.REMAIN_RESOURCE, + data: resoucreList + }); + } else { + jsonRes(res, { + code: 200, + message: RESOURCE_STATUS.RESULT_SUCCESS, + data: [] + }); + } + await Promise.resolve(next?.()); + }; + +export const allRegionResourceSvc = + (userUid: string, userId: string, userCrName: string) => + async (res: NextApiResponse, next?: () => void) => { + const regionList = await globalPrisma.region.findMany(); + const regionTarget = regionList.filter( + (region) => region.uid !== global.AppConfig.cloud.regionUID + ); + const currentRegion = regionList.find( + (region) => region.uid === global.AppConfig.cloud.regionUID + ); + + const regionResourceList: RegionResourceType[] = []; + + const currentResourceCheck = await checkResourceResponse(userId, userUid, userCrName); + if (!currentRegion) throw new Error('current region not found'); + + if (currentResourceCheck.length > 0) + regionResourceList.push({ + region: currentRegion, + resource: currentResourceCheck + }); + const otherCheckResp = await Promise.all( + regionTarget.map((region) => + fetch( + process.env.NODE_ENV === 'development' + ? `http://127.0.0.1:3000/api/auth/delete/checkCurrentResource` + : `https://${region.domain}/api/auth/delete/checkCurrentResource`, + { + headers: { + authorization: encodeURI( + generateAuthenticationToken({ + userUid: userUid, + userId: userId + }) + ) + } + } + ) + ) + ); + if (otherCheckResp.some((resp) => !resp.ok)) + return jsonRes(res, { + code: 500, + message: RESOURCE_STATUS.GET_RESOURCE_ERROR + }); + const otherData = await Promise.all(otherCheckResp.map((resp) => resp.clone().json())); + for (let i = 0; i < otherData.length; i++) { + const resp = otherData[i] as ApiResp; + if (resp?.message === RESOURCE_STATUS.INTERNAL_SERVER_ERROR) { + return jsonRes(res, { + code: 500, + message: RESOURCE_STATUS.GET_RESOURCE_ERROR + }); + } + if (resp?.message === RESOURCE_STATUS.REMAIN_RESOURCE) { + regionResourceList.push({ + region: regionTarget[i], + resource: resp.data || [] + }); + } + } + const code = await generateOnceToken({ + userUid, + type: 'deleteUser' + }); + + if (regionResourceList.length > 0) { + jsonRes(res, { + code: 200, + message: RESOURCE_STATUS.REMAIN_RESOURCE, + data: { + regionResourceList, + code + } + }); + } else { + jsonRes(res, { + code: 200, + message: RESOURCE_STATUS.RESULT_SUCCESS, + data: { + regionResourceList, + code + } + }); + } + await Promise.resolve(next?.()); + }; diff --git a/frontend/desktop/src/services/backend/svc/deleteUser.ts b/frontend/desktop/src/services/backend/svc/deleteUser.ts index 22f18379a75..632a027a8da 100644 --- a/frontend/desktop/src/services/backend/svc/deleteUser.ts +++ b/frontend/desktop/src/services/backend/svc/deleteUser.ts @@ -1,10 +1,13 @@ +import { DeleteUserEvent } from '@/types/db/event'; +import { RESOURCE_STATUS } from '@/types/response/checkResource'; +import { DELETE_USER_STATUS } from '@/types/response/deleteUser'; +import { OnceTokenPayload } from '@/types/token'; import { NextApiResponse } from 'next'; -import { jsonRes } from '../response'; -import { globalPrisma, prisma } from '../db/init'; +import { TransactionStatus, TransactionType, UserStatus } from 'prisma/global/generated/client'; import { v4 } from 'uuid'; -import { TransactionType, TransactionStatus, AuditAction } from 'prisma/global/generated/client'; -import { RESOURCE_STATUS } from '@/types/response/checkResource'; -import { DeleteUserEvent } from '@/types/db/event'; +import { verifyJWT } from '../auth'; +import { globalPrisma } from '../db/init'; +import { jsonRes } from '../response'; export const deleteUserSvc = (userUid: string) => async (res: NextApiResponse) => { const user = await globalPrisma.user.findUnique({ @@ -33,27 +36,27 @@ export const deleteUserSvc = (userUid: string) => async (res: NextApiResponse) = const regionList = regionResults.map((r) => r.uid); // add task ( catch by outer ) await globalPrisma.$transaction(async (tx) => { - for await (const oauthProvider of oauthProviderList) { - await tx.oauthProvider.delete({ - where: { - uid: oauthProvider.uid - } - }); - const eventName = DeleteUserEvent['_DELETE_OAUTH_PROVIDER']; - const _data = { - userUid, - providerType: oauthProvider.providerType, - providerId: oauthProvider.providerId, - message: `${oauthProvider.providerType}:${oauthProvider.providerId}, delete` - }; - await tx.eventLog.create({ - data: { - eventName, - mainId: userUid, - data: JSON.stringify(_data) - } - }); - } + await tx.user.update({ + where: { + uid: userUid + }, + data: { + status: UserStatus.LOCK_USER + } + }); + const eventName = DeleteUserEvent['_SET_LOCK_USER']; + const _data = { + userUid, + message: 'Set lock user' + }; + + await tx.eventLog.create({ + data: { + eventName, + mainId: userUid, + data: JSON.stringify(_data) + } + }); await tx.eventLog.create({ data: { eventName: DeleteUserEvent['_PUB_TRANSACTION'], @@ -86,7 +89,103 @@ export const deleteUserSvc = (userUid: string) => async (res: NextApiResponse) = }); }); return jsonRes(res, { - message: RESOURCE_STATUS.RESULT_SUCCESS, + message: DELETE_USER_STATUS.RESULT_SUCCESS, code: 200 }); }; +export const forceDeleteUserSvc = + (userUid: string, code: string) => async (res: NextApiResponse) => { + const user = await globalPrisma.user.findUnique({ + where: { + uid: userUid + }, + include: { + oauthProvider: true + } + }); + if (!user) + return jsonRes(res, { + message: RESOURCE_STATUS.USER_NOT_FOUND, + code: 404 + }); + const deleteUserCode = await verifyJWT(code); + if ( + !deleteUserCode || + deleteUserCode.type !== 'deleteUser' || + deleteUserCode.userUid !== userUid + ) { + return jsonRes(res, { + message: DELETE_USER_STATUS.CODE_ERROR, + code: 403 + }); + } + const oauthProviderList = user.oauthProvider; + if (oauthProviderList.length === 0) + return jsonRes(res, { + message: RESOURCE_STATUS.OAUTHPROVIDER_NOT_FOUND, + code: 404 + }); + const txUid = v4(); + const infoUid = v4(); + const regionResults = await globalPrisma.region.findMany(); + if (!regionResults) throw Error('region list is null'); + const regionList = regionResults.map((r) => r.uid); + // add task ( catch by outer ) + await globalPrisma.$transaction(async (tx) => { + await tx.user.update({ + where: { + uid: userUid + }, + data: { + status: UserStatus.LOCK_USER + } + }); + const eventName = DeleteUserEvent['_SET_LOCK_USER']; + const _data = { + userUid, + message: 'Set lock user' + }; + + await tx.eventLog.create({ + data: { + eventName, + mainId: userUid, + data: JSON.stringify(_data) + } + }); + await tx.eventLog.create({ + data: { + eventName: DeleteUserEvent['_PUB_TRANSACTION'], + mainId: userUid, + data: JSON.stringify({ + message: `${userUid} publish delete user transaction` + }) + } + }); + await tx.precommitTransaction.create({ + data: { + uid: txUid, + status: TransactionStatus.READY, + infoUid, + transactionType: TransactionType.DELETE_USER + } + }); + await tx.deleteUserTransactionInfo.create({ + data: { + uid: infoUid, + userUid + } + }); + await tx.transactionDetail.createMany({ + data: regionList.map((regionUid) => ({ + status: TransactionStatus.READY, + transactionUid: txUid, + regionUid + })) + }); + }); + return jsonRes(res, { + message: DELETE_USER_STATUS.RESULT_SUCCESS, + code: 200 + }); + }; diff --git a/frontend/desktop/src/services/backend/svc/mergeUser.ts b/frontend/desktop/src/services/backend/svc/mergeUser.ts index 2fc90ca4975..d49403ad982 100644 --- a/frontend/desktop/src/services/backend/svc/mergeUser.ts +++ b/frontend/desktop/src/services/backend/svc/mergeUser.ts @@ -1,10 +1,10 @@ +import { MergeUserEvent } from '@/types/db/event'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; import { NextApiResponse } from 'next'; -import { jsonRes } from '../response'; -import { globalPrisma } from '../db/init'; +import { TransactionStatus, TransactionType, UserStatus } from 'prisma/global/generated/client'; import { v4 } from 'uuid'; -import { TransactionType, TransactionStatus, AuditAction } from 'prisma/global/generated/client'; -import { USER_MERGE_STATUS } from '@/types/response/merge'; -import { MergeUserEvent } from '@/types/db/event'; +import { globalPrisma } from '../db/init'; +import { jsonRes } from '../response'; export const mergeUserSvc = (userUid: string, mergeUserUid: string) => async (res: NextApiResponse) => { @@ -66,6 +66,23 @@ export const mergeUserSvc = } }); } + await tx.user.update({ + where: { uid: mergeUserUid }, + data: { + status: UserStatus.DELETE_USER + } + }); + // Log the delete event + await tx.eventLog.create({ + data: { + eventName: MergeUserEvent['_SET_DELETE_USER'], + mainId: userUid, + data: JSON.stringify({ + mergeUserUid, + message: 'Delete merge process' + }) + } + }); await tx.precommitTransaction.create({ data: { uid: txUid, diff --git a/frontend/desktop/src/services/backend/svc/sms.ts b/frontend/desktop/src/services/backend/svc/sms.ts index 4f9dd65c75e..3d3caccd1ab 100644 --- a/frontend/desktop/src/services/backend/svc/sms.ts +++ b/frontend/desktop/src/services/backend/svc/sms.ts @@ -1,6 +1,6 @@ import { NextApiResponse } from 'next'; -import { jsonRes } from '../response'; import { addOrUpdateCode, SmsType } from '../db/verifyCode'; +import { jsonRes } from '../response'; import { emailSmsReq, smsReq } from '../sms'; export const sendSmsCodeResp = @@ -13,7 +13,6 @@ export const sendSmsCodeResp = }); }; export const sendPhoneCodeSvc = (phone: string) => async (res: NextApiResponse) => { - console.log('svc!'); const code = await smsReq(phone); return sendSmsCodeResp('phone', phone, code)(res); }; diff --git a/frontend/desktop/src/services/request.ts b/frontend/desktop/src/services/request.ts index 269b5191e7c..5b7c2af7230 100644 --- a/frontend/desktop/src/services/request.ts +++ b/frontend/desktop/src/services/request.ts @@ -47,7 +47,7 @@ request.interceptors.response.use( const apiResp = data as ApiResp; if (apiResp?.code && (apiResp.code < 200 || apiResp.code >= 300)) { - return Promise.reject({ code: apiResp.code, message: apiResp.message }); + return Promise.reject({ code: apiResp.code, message: apiResp.message, data: apiResp.data }); } return data; diff --git a/frontend/desktop/src/stores/app.ts b/frontend/desktop/src/stores/app.ts index e2ab853a452..81d3e7be845 100644 --- a/frontend/desktop/src/stores/app.ts +++ b/frontend/desktop/src/stores/app.ts @@ -64,6 +64,7 @@ const useAppStore = create()( maxZIndex: 10, launchQuery: {}, autolaunch: '', + autolaunchWorkspaceUid: '', runner: new AppStateManager([]), async init() { const res = await request('/api/desktop/getInstalledApps'); @@ -191,16 +192,18 @@ const useAppStore = create()( const appToDelete = minBy(get().runningInfo, (app) => app.zIndex); get().runningInfo = get().runningInfo.filter((app) => app.pid !== appToDelete?.pid); }, - setAutoLaunch(autolaunch, launchQuery) { + setAutoLaunch(autolaunch, launchQuery, autolaunchWorkspaceUid) { set((state) => { state.autolaunch = autolaunch; state.launchQuery = launchQuery; + state.autolaunchWorkspaceUid = autolaunchWorkspaceUid; }); }, cancelAutoLaunch: () => { set((state) => { state.autolaunch = ''; state.launchQuery = {}; + state.autolaunchWorkspaceUid = ''; }); } })), diff --git a/frontend/desktop/src/stores/session.ts b/frontend/desktop/src/stores/session.ts index 72eeb15355e..051e7c50ad7 100644 --- a/frontend/desktop/src/stores/session.ts +++ b/frontend/desktop/src/stores/session.ts @@ -1,8 +1,8 @@ +import { Session, sessionKey } from '@/types'; +import { OauthProvider } from '@/types/user'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import { Session, sessionKey } from '@/types'; -import { OauthProvider } from '@/types/user'; type StatePayload = { rad: string; action: OauthAction; @@ -67,7 +67,6 @@ const useSessionStore = create()( compareState: (state: string) => { // fix wechat let isSuccess = decodeURIComponent(state) === decodeURIComponent(get().oauth_state); - console.log(state, get().oauth_state); const [action, ...statePayload] = state.split('_'); set({ oauth_state: undefined }); return { diff --git a/frontend/desktop/src/types/app.ts b/frontend/desktop/src/types/app.ts index b6acf79dcf6..bb7781f988d 100644 --- a/frontend/desktop/src/types/app.ts +++ b/frontend/desktop/src/types/app.ts @@ -65,9 +65,14 @@ export type TOSState = { runningInfo: AppInfo[]; currentAppPid: number; autolaunch: string; + autolaunchWorkspaceUid?: string; launchQuery: Record; // store deploy template - setAutoLaunch: (autolaunch: string, launchQuery: Record) => void; + setAutoLaunch: ( + autolaunch: string, + launchQuery: Record, + autolaunchWorkspaceId?: string + ) => void; cancelAutoLaunch: () => void; // init desktop init(): Promise; diff --git a/frontend/desktop/src/types/db/event.ts b/frontend/desktop/src/types/db/event.ts index d2a39fa002c..1a5c7214516 100644 --- a/frontend/desktop/src/types/db/event.ts +++ b/frontend/desktop/src/types/db/event.ts @@ -2,11 +2,13 @@ export enum MergeUserEvent { '_MERGE_OAUTH_PROVIDER' = '_MERGE_OAUTH_PROVIDER', '_PUB_TRANSACTION' = '_PUB_TRANSACTION', '_MERGE_WORKSPACE' = '_MERGE_WORKSPACE', - '_COMMIT' = '_COMMIT' + '_COMMIT' = '_COMMIT', + '_SET_DELETE_USER' = '_SET_DELETE_USER' } // export type EventName = 'DELETE_USER'; export enum DeleteUserEvent { - '_DELETE_OAUTH_PROVIDER' = '_DELETE_OAUTH_PROVIDER', + '_SET_LOCK_USER' = '_SET_LOCK_USER', + '_SET_LOCK_WORKSPACE' = '_SET_LOCK_WORKSPACE', '_PUB_TRANSACTION' = '_PUB_TRANSACTION', '_DELETE_USERCR' = '_DELETE_USERCR', '_COMMIT' = '_COMMIT' diff --git a/frontend/desktop/src/types/i18next.d.ts b/frontend/desktop/src/types/i18next.d.ts index e78cd62bb0d..7ae2f014682 100644 --- a/frontend/desktop/src/types/i18next.d.ts +++ b/frontend/desktop/src/types/i18next.d.ts @@ -1,13 +1,14 @@ import 'i18next'; -import common from '../../public/locales/zh/common.json'; +import applist from '../../public/locales/zh/applist.json'; import cloudProviders from '../../public/locales/zh/cloudProviders.json'; +import common from '../../public/locales/zh/common.json'; import error from '../../public/locales/zh/error.json'; - export interface I18nNamespaces { common: typeof common; cloudProviders: typeof cloudProviders; error: typeof error; + applist: typeof applist; } export type I18nNsType = (keyof I18nNamespaces)[]; @@ -15,6 +16,7 @@ export type I18nNsType = (keyof I18nNamespaces)[]; export type I18nCommonKey = NestedKeyOf['common']; export type I18nCloudProvidersKey = NestedKeyOf['cloudProviders']; export type I18nErrorKey = NestedKeyOf['error']; +export type I18nApplistKey = NestedKeyOf['applist']; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object @@ -33,7 +35,7 @@ export type I18nKeyFunction = { declare module 'i18next' { interface CustomTypeOptions { returnNull: false; - defaultNS: ['common', 'cloudProviders', 'error']; + defaultNS: ['common', 'cloudProviders', 'error', 'applist']; resources: I18nNamespaces; } } diff --git a/frontend/desktop/src/types/response/changeBind.ts b/frontend/desktop/src/types/response/changeBind.ts index 624e42296dd..f0ea505d425 100644 --- a/frontend/desktop/src/types/response/changeBind.ts +++ b/frontend/desktop/src/types/response/changeBind.ts @@ -3,9 +3,6 @@ import { MERGE_USER_READY, PROVIDER_STATUS, RESPONSE_MESSAGE } from './utils'; enum _CHANGE_BIND_STATUS { USER_NOT_FOUND = 'USER_NOT_FOUND', NOT_SUPPORT = 'NOT_SUPPORT', - // OLD_PROVIDER_NOT_EXIST = 'PROVIDER_NOT_EXIST', - // NEW_PROVIDER_USED_CONFLICT = 'NEW_PROVIDER_USED_CONFLICT', - // NEW_PROVIDER_USED_MERGE = 'NEW_PROVIDER_USED_MERGE', RESOURCE_CONFLICT = 'RESOURCE_CONFLICT', DB_ERROR = 'DB_ERROR' } diff --git a/frontend/desktop/src/types/response/checkResource.ts b/frontend/desktop/src/types/response/checkResource.ts index e1c4d1ca96f..9a097287382 100644 --- a/frontend/desktop/src/types/response/checkResource.ts +++ b/frontend/desktop/src/types/response/checkResource.ts @@ -13,7 +13,8 @@ enum _RESOURCE_STATUS { REMAIN_TEMPLATE = 'REMAIN_TEMPLATE', REMAIN_OBJECT_STORAGE = 'REMAIN_OBJECT_STORAGE', REMAIN_DATABASE = 'REMAIN_DATABASE', - KUBECONFIG_NOT_FOUND = 'KUBECONFIG_NOT_FOUND' + KUBECONFIG_NOT_FOUND = 'KUBECONFIG_NOT_FOUND', + REMAIN_RESOURCE = 'REMAIN_RESOURCE' } export const RESOURCE_STATUS = Object.assign( diff --git a/frontend/desktop/src/types/response/deleteUser.ts b/frontend/desktop/src/types/response/deleteUser.ts new file mode 100644 index 00000000000..1a422d9abb6 --- /dev/null +++ b/frontend/desktop/src/types/response/deleteUser.ts @@ -0,0 +1,8 @@ +import { RESPONSE_MESSAGE } from './utils'; + +enum _DELETE_USERSTATUS { + CODE_ERROR = 'CODE_ERROR' +} + +export const DELETE_USER_STATUS = Object.assign({}, _DELETE_USERSTATUS, RESPONSE_MESSAGE); +export type DELETE_USER_STATUS = typeof DELETE_USER_STATUS; diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index 3259e73584e..8a6fcff78e6 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -1,4 +1,4 @@ -import { DeepRequired, OmitPath, OmitPathArr } from './tools'; +import { DeepRequired, OmitPathArr } from './tools'; export type CloudConfigType = { domain: string; @@ -70,6 +70,7 @@ export type LayoutConfigType = { }; export type AuthConfigType = { + billingToken?: string; proxyAddress?: string; callbackURL: string; signUpEnabled?: boolean; @@ -161,6 +162,7 @@ export type JwtConfigType = { internal?: string; regional?: string; global?: string; + billing?: string; }; export type DesktopConfigType = { @@ -260,7 +262,8 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { userInfoURL: '' } }, - proxyAddress: '' + proxyAddress: '', + billingToken: '' }; export const DefaultAppClientConfig: AppClientConfigType = { diff --git a/frontend/desktop/src/types/token.ts b/frontend/desktop/src/types/token.ts index 37cfe813e92..f2852f59b7e 100644 --- a/frontend/desktop/src/types/token.ts +++ b/frontend/desktop/src/types/token.ts @@ -14,3 +14,9 @@ export type CronJobTokenPayload = { mergeUserUid: string; userUid: string; }; +export type BillingTokenPayload = AuthenticationTokenPayload; + +export type OnceTokenPayload = { + userUid: string; + type: 'deleteUser'; +}; diff --git a/frontend/desktop/src/utils/ProcessManager.ts b/frontend/desktop/src/utils/ProcessManager.ts index 075ca067cd2..f1c819cca52 100644 --- a/frontend/desktop/src/utils/ProcessManager.ts +++ b/frontend/desktop/src/utils/ProcessManager.ts @@ -20,7 +20,6 @@ export default class AppStateManager { constructor(apps: string[]) { this.loadApps(apps || []); this.openedApps = []; - // this.currentPid = -1; } suspendApp(pid: number) { const _state = this.findState(pid); diff --git a/frontend/desktop/src/utils/tools.ts b/frontend/desktop/src/utils/tools.ts index e4ebb3bbd79..d6f5b0cbcb9 100644 --- a/frontend/desktop/src/utils/tools.ts +++ b/frontend/desktop/src/utils/tools.ts @@ -1,10 +1,9 @@ -import { InvitedStatus, UserNsStatus, UserRole } from '@/types/team'; +import { globalPrisma } from '@/services/backend/db/init'; +import { InvitedStatus, UserRole } from '@/types/team'; +import { Prisma } from '@prisma/client/extension'; import dayjs from 'dayjs'; -import { JoinStatus, Role } from 'prisma/region/generated/client'; import { Prisma as GlobalPrisma } from 'prisma/global/generated/client'; -import { OauthProvider } from '@/types/user'; -import { Prisma } from '@prisma/client/extension'; -import { globalPrisma } from '@/services/backend/db/init'; +import { JoinStatus, Role } from 'prisma/region/generated/client'; export const validateNumber = (num: number) => typeof num === 'number' && isFinite(num) && num > 0; @@ -72,7 +71,6 @@ export const retrySerially = (fn: () => Promise, times: number) => export const vaildManage = (ownRole: UserRole) => (targetRole: UserRole, isSelf: boolean) => { if (targetRole === UserRole.Owner) return [UserRole.Owner].includes(ownRole); // only owner else if (targetRole === UserRole.Manager) - // return [UserRole.Owner, ...(isSelf ? [UserRole.Manager] : [])].includes(ownRole); // manager via self else if (targetRole === UserRole.Developer)