diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index f50d1b6ed..bb08442cd 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -84,12 +84,19 @@ "Added": "Added", "Invaild Context": "You need switch to other workspace for handling", "Remove Member Tips": "Determine that you want to remove the member?", + "Quit Workspace Tips":"Confirm leaving workspace?", "Invalid User ID": "Invalid User ID", "The invited user must be others": "The invited user must be others", "Dissovle Tips": "Deleting the workspace 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}}", + "Receive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}", + "Invite members to workspace ": "Invite members to workspace {{workspace}}", + "Generate invitation link": "Generate invitation link", + "Failed to generate invitation link": "Failed to generate invitation link", + "Redirecting to homepage in 3 seconds": "Redirecting to homepage in 3 seconds", + "Invalid invitation link": "Invalid invitation link", + "Invitation reminder": "Invitation reminder", "pay with stripe": "Pay With Stripe", "pay with wechat": "Pay With Wechat", "License Buy": "License Buy", diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index dc680a927..22a7a0b8a 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -24,8 +24,6 @@ "confirmNewPassword": "确认新密码", "passwordMismatch": "密码不一致", "passwordChangeSuccess": "密码修改成功", - "passwordMismatch": "密码不一致", - "newPassword": "新密码", "Verify password": "确认密码", "change": "修改", "Invalid username or password": "用户名或密码错误", @@ -80,12 +78,19 @@ "Added": "已加入", "Invaild Context": "你需要切换到其他工作空间操作", "Remove Member Tips": "确认要移除该成员?", + "Quit Workspace Tips":"确认要退出工作空间吗?", "Invalid User ID": "用户的 ID 不合法", "The invited user must be others": "只能邀请其他人", "Dissovle Tips": "删除工作空间会清空所有资源,确定要删除吗?", "Enter Confirm.": "请输入 {{value}} 确认", "Accept Invitation": "接受邀请", - "Recive Tips": "{{managerName}} 邀请你到 {{teamName}} 当 {{role}}", + "Receive Tips": "{{managerName}} 邀请你到 {{teamName}} 成为 {{role}}", + "Invite members to workspace": "邀请成员至工作空间 {{workspace}}", + "Generate invitation link": "生成邀请链接", + "Failed to generate invitation link": "生成邀请链接失败", + "Redirecting to homepage in 3 seconds": "3秒后跳回主页", + "Invalid invitation link": "邀请链接非法", + "Invitation reminder": "受邀提醒", "pay with wechat": "微信支付", "pay with stripe": "Stripe 支付", "License Buy": "License 购买", diff --git a/frontend/desktop/src/__tests__/api/e2e/namespace/inviteCode.test.ts b/frontend/desktop/src/__tests__/api/e2e/namespace/inviteCode.test.ts new file mode 100644 index 000000000..34aa74ca6 --- /dev/null +++ b/frontend/desktop/src/__tests__/api/e2e/namespace/inviteCode.test.ts @@ -0,0 +1,127 @@ +import { _passwordLoginRequest } from '@/api/auth'; +import { + _createRequest, + _getInviteCodeInfoRequest, + _getInviteCodeRequest, + _nsListRequest, + _verifyInviteCodeRequest, + reciveAction +} from '@/api/namespace'; +import { NamespaceDto, UserRole } from '@/types/team'; +import * as k8s from '@kubernetes/client-node'; +import request from '@/__tests__/api/request'; +import { _setAuth, cleanDb, cleanK8s } from '@/__tests__/api/tools'; +import { AccessTokenPayload } from '@/types/token'; +import { prisma } from '@/services/backend/db/init'; +import { jwtDecode } from 'jwt-decode'; +import { getTeamInviteLimit } from '@/services/enable'; + +const createRequest = _createRequest(request); + +const inviteMemberRequest = _getInviteCodeRequest(request); +const getInviteInfoRequest = _getInviteCodeInfoRequest(request); +const verifyInviteRequest = _verifyInviteCodeRequest(request); +const listNamespaceRequest = _nsListRequest(request); +const TEAM_INVITE_LIMIT = getTeamInviteLimit(); + +describe('invite member', () => { + let token1: string; + let payload1: AccessTokenPayload; + let token2: string; + let payload2: AccessTokenPayload; + const setAuth = _setAuth(request); + let ns: NamespaceDto; + const passwordLoginRequest = _passwordLoginRequest(request, setAuth); + beforeAll(async () => { + //@ts-ignore + // console.log('MONGODB_URI', uri) + const kc = new k8s.KubeConfig(); + await cleanK8s(kc, prisma); + await cleanDb(prisma); + setAuth(); + const res = await passwordLoginRequest({ user: 'abdicatetesttest', password: 'testtest' }); + // make sure valid session + expect(res!.data).toBeDefined(); + token1 = res!.data!.token; + payload1 = jwtDecode(token1); + console.log('payload1', payload1); + setAuth(); + const res2 = await passwordLoginRequest({ user: 'inviteTesttest2', password: 'testtest2' }); + // make sure valid session + expect(res2!.data).toBeDefined(); + token2 = res2!.data!.token; + payload2 = jwtDecode(token2); + setAuth(token1); + const nsRes = await createRequest({ teamName: 'teamZero' }); + expect(nsRes.data?.namespace).toBeDefined(); + ns = nsRes.data?.namespace!; + }, 100000); + describe('invite Owner', () => { + it('invite Owner', async () => { + const res = await inviteMemberRequest({ + ns_uid: ns.uid, + role: UserRole.Owner + }); + expect(res.code).toBe(403); + }); + }); + describe.each([[UserRole.Developer], [UserRole.Manager]])('owner request', (role) => { + it('null param', async () => { + setAuth(token1); + const res = await inviteMemberRequest({ ns_uid: '', role }); + // invalid param + expect(res.code).toBe(400); + const res3 = await inviteMemberRequest({ + ns_uid: 'xxx', + role: '' as any + }); + + expect(res3.code).toBe(400); + }); + it('invite prviate team', async () => { + setAuth(token1); + const res = await inviteMemberRequest({ + ns_uid: payload1.workspaceUid, + role: role + }); + // bug + expect(res.code).toBe(403); + }); + + it('invite self', async () => { + setAuth(token2); + const ns_uid = ns.uid; + const { data } = await inviteMemberRequest({ + ns_uid, + role + }); + expect(data).toBeDefined(); + const res = await verifyInviteRequest({ code: data!.code, action: reciveAction.Accepte }); + expect(res.code).toBe(403); + }); + it('invite exist member', async () => { + setAuth(token1); + const nsRes = await createRequest({ teamName: 'teamLimit2' + role }); + expect(nsRes.data?.namespace).toBeDefined(); + const ns = nsRes.data?.namespace!; + const ns_uid = ns.uid; + const data1 = await inviteMemberRequest({ + ns_uid, + role + }); + console.log(ns); + const res = await verifyInviteRequest({ + code: data1.data!.code, + action: reciveAction.Accepte + }); + console.log(payload1); + expect(res.code).toBe(200); + setAuth(token2); + const res2 = await verifyInviteRequest({ + code: data1.data!.code, + action: reciveAction.Accepte + }); + expect(res2.code).toBe(403); + }, 10000); + }); +}); diff --git a/frontend/desktop/src/__tests__/api/tools.ts b/frontend/desktop/src/__tests__/api/tools.ts index 3056dffdd..cb549e2fb 100644 --- a/frontend/desktop/src/__tests__/api/tools.ts +++ b/frontend/desktop/src/__tests__/api/tools.ts @@ -5,7 +5,6 @@ import { PrismaClient as GlobalPrismaClient } from 'prisma/global/generated/clie export const cleanK8s = async (kc: k8s.KubeConfig, prisma: PrismaClient) => { kc.loadFromDefault(); - console.log('cleanK8s'); const userCrs = await prisma.userCr.findMany(); await Promise.all( userCrs.map(async (user) => { diff --git a/frontend/desktop/src/api/namespace.ts b/frontend/desktop/src/api/namespace.ts index 1722827f8..6af483d37 100644 --- a/frontend/desktop/src/api/namespace.ts +++ b/frontend/desktop/src/api/namespace.ts @@ -4,6 +4,7 @@ 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 }) => request.post>('/api/auth/namespace/abdicate', data); @@ -37,10 +38,31 @@ export enum reciveAction { Accepte = 'accept', Reject = 'reject' } -type verifyParam = { ns_uid: string; action: reciveAction }; +type verifyParam = { ns_uid: string; action: reciveAction }; +type verifyCodeParam = { code: string; action: reciveAction }; export const _verifyInviteRequest = (request: AxiosInstance) => (data: verifyParam) => request.post>('/api/auth/namespace/verifyInvite', data); +export const _verifyInviteCodeRequest = (request: AxiosInstance) => (data: verifyCodeParam) => + request.post>( + '/api/auth/namespace/verifyInviteCode', + data + ); +export const _getInviteCodeRequest = + (request: AxiosInstance) => (data: { ns_uid: string; role: UserRole }) => + request.post>( + '/api/auth/namespace/getInviteCode', + data + ); +export const _getInviteCodeInfoRequest = (request: AxiosInstance) => (data: { code: string }) => + request.post< + typeof data, + ApiResp<{ + workspace: string; + role: UserRole; + inviterNickname: string; + }> + >('/api/auth/namespace/getInviteCodeInfo', data); export const _removeMemberRequest = (request: AxiosInstance) => (data: { ns_uid: string; targetUserCrUid: string }) => request.post>('/api/auth/namespace/removeUser', data); @@ -53,13 +75,10 @@ export const _teamDetailsRequest = (request: AxiosInstance) => (ns_uid: string) export const _reciveMessageRequest = (request: AxiosInstance) => () => request.post>('/api/auth/namespace/recive'); export const _switchRequest = (request: AxiosInstance) => (ns_uid: string) => - request.post>( - '/api/auth/namespace/switch', - { - ns_uid - } - ); -// 提供给prod/dev环境使用 + request.post>('/api/auth/namespace/switch', { + ns_uid + }); +// for prod/dev export const abdicateRequest = _abdicateRequest(request); export const createRequest = _createRequest(request); export const deleteTeamRequest = _deleteTeamRequest(request); @@ -71,3 +90,6 @@ export const removeMemberRequest = _removeMemberRequest(request); export const teamDetailsRequest = _teamDetailsRequest(request); export const reciveMessageRequest = _reciveMessageRequest(request); export const switchRequest = _switchRequest(request); +export const getInviteCodeRequest = _getInviteCodeRequest(request); +export const getInviteCodeInfoRequest = _getInviteCodeInfoRequest(request); +export const verifyInviteCodeRequest = _verifyInviteCodeRequest(request); diff --git a/frontend/desktop/src/components/account/index.tsx b/frontend/desktop/src/components/account/index.tsx index 383526d8b..006b5bc94 100644 --- a/frontend/desktop/src/components/account/index.tsx +++ b/frontend/desktop/src/components/account/index.tsx @@ -9,114 +9,22 @@ import { Stack, Text, type UseDisclosureReturn, - PopoverTrigger, - Popover, - PopoverContent, - PopoverBody, IconButton, HStack, VStack } from '@chakra-ui/react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { useMemo, useState } from 'react'; import useAppStore from '@/stores/app'; import { ApiResp, Region } from '@/types'; import { formatMoney } from '@/utils/format'; -import TeamCenter from '@/components/team/TeamCenter'; -import NsList from '@/components/team/NsList'; -import { nsListRequest, switchRequest } from '@/api/namespace'; -import { NSType } from '@/types/team'; import PasswordModify from './PasswordModify'; import { useGlobalStore } from '@/stores/global'; import { CopyIcon, DownloadIcon, LogoutIcon, RightArrowIcon } from '@sealos/ui'; import { ImageFallBackUrl } from '@/stores/config'; -import { jwtDecode } from 'jwt-decode'; -import { AccessTokenPayload } from '@/types/token'; -import { sessionConfig } from '@/utils/sessionConfig'; -const NsMenu = () => { - const { t } = useTranslation(); - const { session } = useSessionStore(); - const ns_uid = session?.user?.ns_uid || ''; - const router = useRouter(); - const mutation = useMutation({ - mutationFn: switchRequest, - async onSuccess(data) { - if (data.code === 200 && !!data.data) { - await sessionConfig(data.data); - } else { - throw Error('session in invalid'); - } - } - }); - const switchTeam = async ({ uid }: { uid: string }) => { - if (ns_uid !== uid) mutation.mutateAsync(uid).then(router.reload); - }; - const { data } = useQuery({ - queryKey: ['teamList', 'teamGroup'], - queryFn: nsListRequest - }); - 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 ( - - - { - - - {t('Team')} - - - - {namespace?.nstype === NSType.Private ? t('Default Team') : namespace?.teamName} - - - - - } - - - - - - - - ); -}; export default function Account({ disclosure }: { disclosure: UseDisclosureReturn }) { const [showId, setShowId] = useState(true); const { needPassword, rechargeEnabled } = useGlobalStore((state) => state.systemEnv); @@ -143,12 +51,7 @@ export default function Account({ disclosure }: { disclosure: UseDisclosureRetur return real_balance; }, [data]); const queryclient = useQueryClient(); - const kubeconfigQuery = useQuery({ - queryKey: [user, 'kubeconfig'], - queryFn: () => request.get>('/api/auth/getKubeconfig'), - enabled: !!user - }); - const kubeconfig = kubeconfigQuery.data?.data?.kubeconfig; + const kubeconfig = session?.kubeconfig || ''; const logout = (e: React.MouseEvent) => { e.preventDefault(); delSession(); @@ -217,8 +120,6 @@ export default function Account({ disclosure }: { disclosure: UseDisclosureRetur /> - {/**/} - - - {t('Manage Team')} - - {needPassword && ( {regionList?.length > 1 && ( - + disclosure.onOpen()} > @@ -59,13 +63,17 @@ export default function RegionToggle() { + {disclosure.isOpen ? ( <> disclosure.onClose()} + onClick={(e) => { + e.stopPropagation(); + disclosure.onClose(); + }} > { - let trim_to = userId.trim(); - if (!trim_to || trim_to.length !== 10) { + const { copyData } = useCopyData(); + const getLinkCode = useMutation({ + mutationFn: getInviteCodeRequest, + mutationKey: [session?.user.ns_uid], + onError() { toast({ status: 'error', - title: t('Invalid User ID'), + title: t('Failed to generate invitation link'), isClosable: true, position: 'top' }); - return; } - const tk8s_username = trim_to.startsWith('ns-') ? trim_to.replace('ns-', '') : trim_to; - if (tk8s_username === k8s_username) { - toast({ - status: 'error', - title: t('The invited user must be others'), - isClosable: true, - position: 'top' - }); - return; - } - mutation.mutate({ + }); + + const generateLink = (code: string) => { + return window.location.origin + encodeURI(`/WorkspaceInvite/?code=${code}`); + }; + const handleGenLink: MouseEventHandler = async (e) => { + e.preventDefault(); + const data = await getLinkCode.mutateAsync({ ns_uid, - targetUserId: trim_to, role }); + const code = data.data?.code!; + const link = generateLink(code); + await copyData(link); }; return ( <> @@ -121,69 +124,66 @@ export default function InviteMember({ ) : ( - { - e.preventDefault(); - setUserId(e.target.value); - }} - placeholder={t('private team ID of user') || ''} - value={userId} - /> - - + {t('Invite members to workspace', { + workspace: workspaceName + })} + + + + + + {ROLE_LIST[role]} + + + + + {ROLE_LIST.map((role, idx) => ( + { + e.preventDefault(); + setRole(idx); + }} + key={idx} + > + {role} + + )).filter((_, i) => canManage(i, false) && i !== UserRole.Owner)} + + + - + {t('Generate invitation link')} + + )} diff --git a/frontend/desktop/src/components/team/NsList.tsx b/frontend/desktop/src/components/team/NsList.tsx deleted file mode 100644 index a3a9ce691..000000000 --- a/frontend/desktop/src/components/team/NsList.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useCopyData } from '@/hooks/useCopyData'; -import { Box, Flex, Text } from '@chakra-ui/react'; -import { useQueryClient } from '@tanstack/react-query'; -import { NSType, NamespaceDto } from '@/types/team'; -import { useTranslation } from 'react-i18next'; -const NsList = ({ - click, - selected_ns_uid, - namespaces, - displayPoint = false, - ...boxprop -}: { - displayPoint: boolean; - click?: (ns: NamespaceDto) => void; - selected_ns_uid: string; - namespaces: NamespaceDto[]; -} & Parameters[0]) => { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - return ( - - {namespaces.length > 0 && - namespaces.map((ns) => ( - { - e.preventDefault(); - queryClient.invalidateQueries({ queryKey: ['teamList'] }); - click && click(ns); - }} - cursor={'pointer'} - {...(selected_ns_uid === ns.uid - ? { - background: 'rgba(0, 0, 0, 0.05)' - } - : { - bgColor: 'unset' - })} - px={'4px'} - _hover={{ - '> .namespace-option': { - display: 'flex' - }, - bgColor: 'rgba(0, 0, 0, 0.03)' - }} - > - - - - {ns.nstype === NSType.Private ? t('Default Team') : ns.teamName} - - - - ))} - - ); -}; - -export default NsList; diff --git a/frontend/desktop/src/components/team/NsListItem.tsx b/frontend/desktop/src/components/team/NsListItem.tsx new file mode 100644 index 000000000..a4b7fe6da --- /dev/null +++ b/frontend/desktop/src/components/team/NsListItem.tsx @@ -0,0 +1,69 @@ +import { Box, Flex, FlexProps, HStack, Text, VStack } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +const NsListItem = ({ + isSelected, + isPrivate, + displayPoint = false, + teamName, + ...flexprop +}: { + displayPoint: boolean; + teamName: string; + isPrivate: boolean; + isSelected: boolean; +} & FlexProps) => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + return ( + { + e.preventDefault(); + queryClient.invalidateQueries({ queryKey: ['teamList'] }); + }} + cursor={'pointer'} + {...flexprop} + {...(isSelected + ? { + background: 'rgba(0, 0, 0, 0.05)' + } + : { + bgColor: 'unset' + })} + _hover={{ + '> .namespace-option': { + display: 'flex' + }, + bgColor: 'rgba(0, 0, 0, 0.03)' + }} + > + + + + {isPrivate ? t('Default Team') : teamName} + + + + ); +}; + +export default NsListItem; diff --git a/frontend/desktop/src/components/team/ReciveMessage.tsx b/frontend/desktop/src/components/team/ReciveMessage.tsx index 284befee1..06b0f1826 100644 --- a/frontend/desktop/src/components/team/ReciveMessage.tsx +++ b/frontend/desktop/src/components/team/ReciveMessage.tsx @@ -39,7 +39,7 @@ export default function ReciveMessage({ }); const { t, i18n } = useTranslation(); const inviteTips = ({ managerName, teamName, role }: Record) => - t('Recive Tips', { + t('Receive Tips', { managerName, teamName, role diff --git a/frontend/desktop/src/components/team/RemoveMember.tsx b/frontend/desktop/src/components/team/RemoveMember.tsx index e7837382c..1fcaeae1d 100644 --- a/frontend/desktop/src/components/team/RemoveMember.tsx +++ b/frontend/desktop/src/components/team/RemoveMember.tsx @@ -20,6 +20,7 @@ import { removeMemberRequest } from '@/api/namespace'; import { useCustomToast } from '@/hooks/useCustomToast'; import { ApiResp } from '@/types'; import { useTranslation } from 'react-i18next'; + export default function RemoveMember({ ns_uid, status, @@ -41,10 +42,7 @@ export default function RemoveMember({ const mutation = useMutation({ mutationFn: removeMemberRequest, onSuccess() { - queryClient.invalidateQueries({ - queryKey: ['ns-detail'], - exact: false - }); + queryClient.invalidateQueries(); onClose(); }, onError(error) { @@ -61,6 +59,8 @@ export default function RemoveMember({ : selfUserCrUid === targetUserCrUid ? t('Quit') : t('Remove'); + const removeTips = + selfUserCrUid === targetUserCrUid ? t('Quit Workspace Tips') : t('Remove Member Tips'); return ( <> + + ) : infoResp.isError ? ( + + + {t('Invalid invitation link')} + + + {t('Redirecting to homepage in 3 seconds')} + + + ) : null} + + + ); +}; +export async function getServerSideProps({ req, res, locales }: any) { + const local = + req?.cookies?.NEXT_LOCALE || compareFirstLanguages(req?.headers?.['accept-language'] || 'zh'); + res.setHeader('Set-Cookie', `NEXT_LOCALE=${local}; Max-Age=2592000; Secure; SameSite=None`); + + const queryClient = new QueryClient(); + const props = { + ...(await serverSideTranslations(local, undefined, null, locales || [])), + dehydratedState: dehydrate(queryClient) + }; + return { + props + }; +} +export default Callback; diff --git a/frontend/desktop/src/pages/api/auth/getKubeconfig.ts b/frontend/desktop/src/pages/api/auth/getKubeconfig.ts index f375eee2b..0754410fe 100644 --- a/frontend/desktop/src/pages/api/auth/getKubeconfig.ts +++ b/frontend/desktop/src/pages/api/auth/getKubeconfig.ts @@ -2,7 +2,8 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/services/backend/response'; import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; import { verifyAccessToken } from '@/services/backend/auth'; -import { switchKubeconfigNamespace } from '@/services/backend/kubernetes/user'; + +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -15,7 +16,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const defaultKc = await getUserKubeconfigNotPatch(regionUser.userCrName); if (!defaultKc) throw new Error('get kubeconfig error, regionUserUid: ' + regionUser.userCrUid); const kubeconfig = switchKubeconfigNamespace(defaultKc, regionUser.workspaceId); - return jsonRes(res, { code: 200, message: 'Successfully', diff --git a/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts b/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts new file mode 100644 index 000000000..327779cc6 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts @@ -0,0 +1,69 @@ +import { jsonRes } from '@/services/backend/response'; +import { bindingRole } from '@/services/backend/team'; +import { UserRole } from '@/types/team'; +import { isUserRole, roleToUserRole, vaildManage } from '@/utils/tools'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { globalPrisma, prisma } from '@/services/backend/db/init'; +import { v4, validate } from 'uuid'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { getTeamInviteLimit } from '@/services/enable'; +const TEAM_INVITE_LIMIT = getTeamInviteLimit(); +import { Role } from 'prisma/region/generated/client'; +import { addOrUpdateInviteCode } from '@/services/backend/db/workspaceInviteCode'; +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 { ns_uid, role } = req.body as { + ns_uid?: string; + role?: UserRole; + }; + if (!ns_uid || !validate(ns_uid)) + return jsonRes(res, { code: 400, message: 'ns_uid is invaild' }); + if (!isUserRole(role)) return jsonRes(res, { code: 400, message: 'role is required' }); + if (role === UserRole.Owner) return jsonRes(res, { code: 403, message: 'role must be others' }); + + const queryResults = await prisma.userWorkspace.findMany({ + where: { + workspaceUid: ns_uid, + status: { + in: [JoinStatus.IN_WORKSPACE, JoinStatus.INVITED] + } + }, + include: { + workspace: true, + userCr: true + } + }); + const own = queryResults.find((x) => x.userCrUid === payload.userCrUid); + if (!own) return jsonRes(res, { code: 403, message: 'you are not in the namespace' }); + if (own.isPrivate) return jsonRes(res, { code: 403, message: 'the namespace is invalid' }); + const vaild = ([Role.OWNER, Role.MANAGER] as Role[]).includes(own.role); + if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' }); + if (queryResults.length >= TEAM_INVITE_LIMIT) + return jsonRes(res, { code: 403, message: 'these invitees are too many' }); + const spec = { + inviterUid: payload.userUid, + workspaceUid: ns_uid, + inviterCrUid: payload.userCrUid, + role + }; + const code = v4(); + const result = await addOrUpdateInviteCode({ + ...spec, + code + }); + if (!result) throw Error('invite member error'); + return jsonRes(res, { + code: 200, + data: { + code + }, + message: 'Successfully' + }); + } catch (e) { + console.log(e); + return jsonRes(res, { code: 500, message: 'invite member error' }); + } +} diff --git a/frontend/desktop/src/pages/api/auth/namespace/getInviteCodeInfo.ts b/frontend/desktop/src/pages/api/auth/namespace/getInviteCodeInfo.ts new file mode 100644 index 000000000..55ae72b96 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/namespace/getInviteCodeInfo.ts @@ -0,0 +1,55 @@ +import { jsonRes } from '@/services/backend/response'; +import { roleToUserRole, vaildManage } from '@/utils/tools'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { globalPrisma, prisma } from '@/services/backend/db/init'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { findInviteCode } from '@/services/backend/db/workspaceInviteCode'; + +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 { code } = req.body as { + code?: string; + }; + if (!code) return jsonRes(res, { code: 400, message: 'code is required' }); + const linkResults = await findInviteCode(code); + if (!linkResults) return jsonRes(res, { code: 404, message: 'the link is not found' }); + + const queryResults = await prisma.userWorkspace.findMany({ + where: { + workspaceUid: linkResults.workspaceUid, + userCrUid: { + in: [linkResults.inviterCrUid, payload.userCrUid] + } + }, + include: { + workspace: true, + userCr: true + } + }); + const inviteeStatus = queryResults.find((item) => item.userCrUid === payload.userCrUid); + if (inviteeStatus) return jsonRes(res, { code: 204, message: 'you are already in namespace' }); + const inviterStatus = queryResults.find((r) => r.userCrUid === linkResults.inviterCrUid); + if (!inviterStatus || !vaildManage(roleToUserRole(inviterStatus.role))(linkResults.role, false)) + return jsonRes(res, { code: 404, message: 'the inviter or the namespace is not found' }); + const user = await globalPrisma.user.findUnique({ + where: { + uid: inviterStatus.userCr.userUid + } + }); + if (!user) throw Error('invalid user'); + return jsonRes(res, { + code: 200, + data: { + workspace: inviterStatus.workspace.displayName, + role: linkResults.role, + inviterNickname: user.nickname + }, + message: 'Successfully' + }); + } catch (e) { + console.log(e); + jsonRes(res, { code: 500, message: 'invite member error' }); + } +} diff --git a/frontend/desktop/src/pages/api/auth/namespace/invite.ts b/frontend/desktop/src/pages/api/auth/namespace/invite.ts index 920663e2b..6f73625a5 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/invite.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/invite.ts @@ -54,7 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const own = queryResults.find((x) => x.userCrUid === payload.userCrUid); if (!own) return jsonRes(res, { code: 403, message: 'you are not in the namespace' }); if (own.isPrivate) return jsonRes(res, { code: 403, message: 'the namespace is invalid' }); - const vaild = vaildManage(roleToUserRole(own.role), own.userCrUid)(role, targetRegionUser.uid); + const vaild = vaildManage(roleToUserRole(own.role))(role, false); if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' }); if (queryResults.length === 0) return jsonRes(res, { code: 404, message: 'there are not user in the namespace ' }); diff --git a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts index f9ee5de04..00aa8bca4 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts @@ -42,10 +42,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const own = queryResults.find((x) => x.userCrUid === payload.userCrUid); if (!own) return jsonRes(res, { code: 403, message: 'you are not in namespace' }); - const vaildFn = vaildManage(roleToUserRole(own.role), own.userCrUid); + const vaildFn = vaildManage(roleToUserRole(own.role)); const targetUser = queryResults.find((x) => x.userCrUid === targetUserCrUid); if (!targetUser) return jsonRes(res, { code: 404, message: 'target is not in namespace' }); - if (!vaildFn(roleToUserRole(targetUser.role), targetUser.userCrUid)) + if (!vaildFn(roleToUserRole(targetUser.role), targetUser.userCrUid === own.userCrUid)) return jsonRes(res, { code: 403, message: 'you are not manager' }); // 权限一致,不用管 diff --git a/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts b/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts index c797bf319..bfac7a05c 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts @@ -6,12 +6,11 @@ import { validate } from 'uuid'; import { prisma } from '@/services/backend/db/init'; import { JoinStatus } from 'prisma/region/generated/client'; import { verifyAccessToken } from '@/services/backend/auth'; - +import { Role } from 'prisma/region/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 verify error' }); - // const { ns_uid, targetUserCrUid } = req.body as { ns_uid?: string; targetUserCrUid?: string; @@ -22,9 +21,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!targetUserCrUid || !validate(targetUserCrUid)) return jsonRes(res, { code: 400, message: 'tUserId is invalid' }); - if (targetUserCrUid === payload.userCrUid) { - return jsonRes(res, { code: 403, message: 'target user must be others' }); - } const queryResults = await prisma.userWorkspace.findMany({ where: { workspaceUid: ns_uid, @@ -39,26 +35,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); const own = queryResults.find((x) => x.userCrUid === payload.userCrUid); + if (!own || own.status !== JoinStatus.IN_WORKSPACE) return jsonRes(res, { code: 403, message: 'you are not in the namespace' }); + if (targetUserCrUid === payload.userCrUid && own.role === Role.OWNER) { + return jsonRes(res, { code: 403, message: 'target user must be others' }); + } const tItem = queryResults.find((item) => item.userCr.uid === targetUserCrUid); - // 之前没绑上, + // for inviting state, if (!tItem) return jsonRes(res, { code: 404, message: 'target user is not in namespace' }); - const vaild = vaildManage(roleToUserRole(own.role), own.userCrUid)( + const vaild = vaildManage(roleToUserRole(own.role))( roleToUserRole(tItem.role), - targetUserCrUid + tItem.userCrUid === payload.userCrUid ); if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' }); let unbinding_result = null; if (JoinStatus.INVITED === tItem.status) { - // 等于取消邀请 + // equal to cancel inviting unbinding_result = await unbindingRole({ userCrUid: tItem.userCrUid, workspaceUid: ns_uid }); } else if (JoinStatus.IN_WORKSPACE === tItem.status) { - // 删除权限 - + // modify role await modifyTeamRole({ k8s_username: tItem.userCr.crName, role: roleToUserRole(tItem.role), diff --git a/frontend/desktop/src/pages/api/auth/namespace/switch.ts b/frontend/desktop/src/pages/api/auth/namespace/switch.ts index 3140e4ad1..a0e84c1eb 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/switch.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/switch.ts @@ -1,8 +1,6 @@ import { jsonRes } from '@/services/backend/response'; import { NextApiRequest, NextApiResponse } from 'next'; import { prisma } from '@/services/backend/db/init'; -import { getUserKubeconfig } from '@/services/backend/kubernetes/admin'; -import { switchKubeconfigNamespace } from '@/services/backend/kubernetes/user'; import { validate } from 'uuid'; import { JoinStatus } from 'prisma/region/generated/client'; import { generateAccessToken, generateAppToken, verifyAccessToken } from '@/services/backend/auth'; @@ -29,9 +27,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const newWorkspaceItem = queryResults.find((item) => item.workspace.uid === ns_uid); if (!newWorkspaceItem) return jsonRes(res, { code: 403, message: 'You are not in this workspace' }); - const oldKcRaw = await getUserKubeconfig(payload.userCrUid, payload.userCrName); - if (!oldKcRaw) return jsonRes(res, { code: 404, message: 'The kubeconfig is not found' }); - const kubeconfig = switchKubeconfigNamespace(oldKcRaw, newWorkspaceItem.workspace.id); const jwtPayload = { workspaceUid: newWorkspaceItem.workspaceUid, workspaceId: newWorkspaceItem.workspace.id, @@ -45,7 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const appToken = generateAppToken(jwtPayload); const data = { token, - kubeconfig, appToken }; return jsonRes(res, { diff --git a/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts b/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts index 4aaf1d786..62604309b 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts @@ -56,7 +56,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); if (!unbindingResult) throw new Error('fail to unbinding'); } - jsonRes(res, { code: 200, message: 'Successfully' diff --git a/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts b/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts new file mode 100644 index 000000000..d74bb5ba4 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/namespace/verifyInviteCode.ts @@ -0,0 +1,76 @@ +import { reciveAction } from '@/api/namespace'; +import { jsonRes } from '@/services/backend/response'; +import { modifyTeamRole } from '@/services/backend/team'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { prisma } from '@/services/backend/db/init'; +import { UserRoleToRole } from '@/utils/tools'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { findInviteCode } from '@/services/backend/db/workspaceInviteCode'; + +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 { code, action } = req.body as { code?: string; action?: reciveAction }; + + if (!code) return jsonRes(res, { code: 400, message: 'code is invalid' }); + + if (action === undefined || ![reciveAction.Accepte, reciveAction.Reject].includes(action)) + return jsonRes(res, { + code: 400, + message: `action must be ${reciveAction.Accepte}, ${reciveAction.Reject}` + }); + if (action === reciveAction.Accepte) { + const linkResults = await findInviteCode(code); + if (!linkResults) return jsonRes(res, { code: 404, message: 'the link is not found' }); + const queryResults = await prisma.userWorkspace.findMany({ + where: { + workspaceUid: linkResults.workspaceUid, + userCrUid: { + in: [linkResults.inviterCrUid, payload.userCrUid] + }, + status: JoinStatus.IN_WORKSPACE + }, + include: { + workspace: true, + userCr: true + } + }); + const inviteeStatus = queryResults.find((item) => item.userCrUid === payload.userCrUid); + if (inviteeStatus) + return jsonRes(res, { code: 409, message: 'you are already in namespace' }); + const inviterStatus = queryResults.find((r) => r.userCrUid === linkResults.inviterCrUid); + if (!inviterStatus) + return jsonRes(res, { code: 404, message: 'the inviter or the namespace is not found' }); + + await modifyTeamRole({ + k8s_username: payload.userCrName, + role: linkResults.role, + workspaceId: inviterStatus.workspace.id, + action: 'Grant' + }); + const result = await prisma.userWorkspace.create({ + data: { + status: JoinStatus.IN_WORKSPACE, + role: UserRoleToRole(linkResults.role), + isPrivate: false, + workspaceUid: linkResults.workspaceUid, + userCrUid: payload.userCrUid, + joinAt: new Date(), + handlerUid: linkResults.inviterUid + } + }); + + if (!result) throw new Error('failed to change Status'); + } + return jsonRes(res, { + code: 200, + message: 'Successfully' + }); + } catch (e) { + console.log(e); + return jsonRes(res, { code: 500, message: 'get price error' }); + } +} diff --git a/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts b/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts index 863f1e795..9968b70ae 100644 --- a/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts +++ b/frontend/desktop/src/pages/api/desktop/getInstalledApps.ts @@ -1,10 +1,12 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { K8sApi, ListCRD, switchKubeconfigNamespace } from '@/services/backend/kubernetes/user'; +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 { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const payload = await verifyAccessToken(req.headers); diff --git a/frontend/desktop/src/pages/api/notification/list.ts b/frontend/desktop/src/pages/api/notification/list.ts index 81810f7cc..90b9d93d7 100644 --- a/frontend/desktop/src/pages/api/notification/list.ts +++ b/frontend/desktop/src/pages/api/notification/list.ts @@ -1,13 +1,10 @@ -import { - CRDMeta, - K8sApi, - ListCRD, - switchKubeconfigNamespace -} from '@/services/backend/kubernetes/user'; +import { CRDMeta, K8sApi, ListCRD } from '@/services/backend/kubernetes/user'; import { jsonRes } from '@/services/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; import { verifyAccessToken } from '@/services/backend/auth'; import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; + +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const payload = await verifyAccessToken(req.headers); diff --git a/frontend/desktop/src/pages/api/notification/read.ts b/frontend/desktop/src/pages/api/notification/read.ts index 7a747e384..2678ce5c7 100644 --- a/frontend/desktop/src/pages/api/notification/read.ts +++ b/frontend/desktop/src/pages/api/notification/read.ts @@ -1,14 +1,11 @@ import { verifyAccessToken } from '@/services/backend/auth'; -import { - CRDMeta, - K8sApi, - switchKubeconfigNamespace, - UpdateCRD -} from '@/services/backend/kubernetes/user'; +import { CRDMeta, K8sApi, UpdateCRD } from '@/services/backend/kubernetes/user'; import { jsonRes } from '@/services/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; import * as k8s from '@kubernetes/client-node'; import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; + +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { name } = req.body; diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index 396dee515..99b178c0f 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -13,6 +13,7 @@ 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'; const destination = '/signin'; interface IMoreAppsContext { @@ -27,12 +28,13 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str const init = useAppStore((state) => state.init); const setAutoLaunch = useAppStore((state) => state.setAutoLaunch); const { systemConfig } = useSystemConfigStore(); - + const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore(); useEffect(() => { colorMode === 'dark' ? toggleColorMode() : null; }, [colorMode, toggleColorMode]); const [showMoreApps, setShowMoreApps] = useState(false); + // openApp by query useEffect(() => { const { query } = router; const is_login = isUserLogin(); @@ -87,6 +89,13 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str } }, []); + // handle workspaceInvite + useEffect(() => { + if (workspaceInviteCode) { + router.replace('/WorkspaceInvite?code=' + workspaceInviteCode); + return; + } + }, [workspaceInviteCode]); return ( diff --git a/frontend/desktop/src/pages/proxyOAuth.tsx b/frontend/desktop/src/pages/proxyOAuth.tsx index 52f9b7a08..f012c0dff 100644 --- a/frontend/desktop/src/pages/proxyOAuth.tsx +++ b/frontend/desktop/src/pages/proxyOAuth.tsx @@ -29,8 +29,6 @@ export default function Callback() { const oauthProvider = router.query.oauthProxyProvider; // nextjs auto decode let oauthClientId = router.query.oauthProxyClientID; - // const abortController = new AbortController() - console.log(oauthProvider, oauthProxyState, oauthClientId); if (!isString(oauthProxyState) || !isString(oauthProvider) || !isString(oauthClientId)) return; console.log(oauthProvider, oauthProxyState, oauthClientId, callback_url); @@ -42,9 +40,7 @@ export default function Callback() { ).json()) as ApiResp<{ containDomain: boolean }>; console.log(result); if (!result.data?.containDomain) return; - console.log('generate'); const state = generateState(oauthProxyState); - console.log(callback_url, 'callback'); if (oauthProvider === 'github') { await oauthLogin({ diff --git a/frontend/desktop/src/pages/switchRegion.tsx b/frontend/desktop/src/pages/switchRegion.tsx index dbaa13170..62e18051e 100644 --- a/frontend/desktop/src/pages/switchRegion.tsx +++ b/frontend/desktop/src/pages/switchRegion.tsx @@ -3,28 +3,32 @@ import { useRouter } from 'next/router'; import { useEffect } from 'react'; import useSessionStore from '@/stores/session'; import { Flex, Spinner } from '@chakra-ui/react'; -import { uploadConvertData } from '@/api/platform'; -import { getRegionToken, UserInfo } from '@/api/auth'; +import { getRegionToken } from '@/api/auth'; import { isString } from 'lodash'; import { jwtDecode } from 'jwt-decode'; import { AccessTokenPayload } from '@/types/token'; import { sessionConfig } from '@/utils/sessionConfig'; import { useMutation } from '@tanstack/react-query'; import { nsListRequest, switchRequest } from '@/api/namespace'; +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; const Callback: NextPage = () => { const router = useRouter(); const setSession = useSessionStore((s) => s.setSession); const setToken = useSessionStore((s) => s.setToken); const delSession = useSessionStore((s) => s.delSession); - const curToken = useSessionStore((s) => s.token); + const { token: curToken, session } = useSessionStore((s) => s); const { lastWorkSpaceId } = useSessionStore(); const mutation = useMutation({ mutationFn: switchRequest, async onSuccess(data) { - if (data.code === 200 && !!data.data) { - await sessionConfig(data.data); + if (data.code === 200 && !!data.data && session) { + const payload = jwtDecode(data.data.token); + await sessionConfig({ + ...data.data, + kubeconfig: switchKubeconfigNamespace(session.kubeconfig, payload.workspaceId) + }); } else { throw Error('session in invalid'); } diff --git a/frontend/desktop/src/services/backend/db/workspaceInviteCode.ts b/frontend/desktop/src/services/backend/db/workspaceInviteCode.ts new file mode 100644 index 000000000..a2202b773 --- /dev/null +++ b/frontend/desktop/src/services/backend/db/workspaceInviteCode.ts @@ -0,0 +1,71 @@ +import { connectToDatabase } from './mongodb'; +import { UserRole } from '@/types/team'; + +type TWorkspace_invite_link = { + role: UserRole; + code: string; // unique + workspaceUid: string; + createdAt: Date; + inviterUid: string; + inviterCrUid: string; +}; + +async function connectToUserCollection() { + const client = await connectToDatabase(); + const collection = client.db().collection('workspace_invite_links'); + await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 * 30 }); + await collection.createIndex({ code: 1 }, { unique: true }); + return collection; +} + +export async function addOrUpdateInviteCode({ + code, + inviterUid, + role, + workspaceUid, + inviterCrUid +}: Omit) { + const codes = await connectToUserCollection(); + const result = await codes.updateOne( + { + inviterCrUid, + inviterUid, + role, + workspaceUid + }, + { + $set: { + code, + createdAt: new Date() + } + }, + { + upsert: true + } + ); + return result; +} + +export async function getInviteCode({ + inviterUid, + role, + workspaceUid, + inviterCrUid +}: Omit) { + const codes = await connectToUserCollection(); + const result = await codes.findOne({ + inviterUid, + inviterCrUid, + workspaceUid, + role + }); + return result; +} + +export async function findInviteCode(code: string) { + const codes = await connectToUserCollection(); + const result = await codes.findOne({ + code + }); + return result; +} diff --git a/frontend/desktop/src/services/backend/kubernetes/user.ts b/frontend/desktop/src/services/backend/kubernetes/user.ts index c7e77f798..41998f3cf 100644 --- a/frontend/desktop/src/services/backend/kubernetes/user.ts +++ b/frontend/desktop/src/services/backend/kubernetes/user.ts @@ -1,12 +1,7 @@ import * as k8s from '@kubernetes/client-node'; import http from 'http'; import * as yaml from 'js-yaml'; -export function switchKubeconfigNamespace(kc: string, namespace: string) { - const oldKc = yaml.load(kc); - // @ts-ignore - oldKc.contexts[0].context.namespace = namespace; - return yaml.dump(oldKc); -} + export function K8sApi(config: string): k8s.KubeConfig { const kc = new k8s.KubeConfig(); kc.loadFromString(config); diff --git a/frontend/desktop/src/services/backend/team.ts b/frontend/desktop/src/services/backend/team.ts index d6bb1c4b2..1da03e0d9 100644 --- a/frontend/desktop/src/services/backend/team.ts +++ b/frontend/desktop/src/services/backend/team.ts @@ -11,8 +11,8 @@ import { K8sApiDefault } from './kubernetes/admin'; import { ApplyYaml } from './kubernetes/user'; import { prisma } from '@/services/backend/db/init'; import { JoinStatus, Role } from 'prisma/region/generated/client'; +import { UserRoleToRole } from '@/utils/tools'; -const UserRoleToRole = (role: UserRole): Role => [Role.OWNER, Role.MANAGER, Role.DEVELOPER][role]; const _applyRoleRequest = (kc: KubeConfig, nsid: string) => (action: 'Grant' | 'Deprive' | 'Update', types: watchEventType[]) => @@ -192,7 +192,7 @@ export const modifyBinding = async ({ }); }; -// 拒绝邀请 === 解绑 +// reject Invitation export const unbindingRole = async ({ userCrUid, workspaceUid @@ -209,7 +209,7 @@ export const unbindingRole = async ({ } }); }; -// 接受邀请 +// accept Invitation export const acceptInvite = async ({ userCrUid, workspaceUid diff --git a/frontend/desktop/src/stores/app.ts b/frontend/desktop/src/stores/app.ts index dcf50c49b..1fd4a8b64 100644 --- a/frontend/desktop/src/stores/app.ts +++ b/frontend/desktop/src/stores/app.ts @@ -6,6 +6,7 @@ import { immer } from 'zustand/middleware/immer'; import AppStateManager from '../utils/ProcessManager'; import { formatUrl } from '@/utils/format'; import { minBy, cloneDeep } from 'lodash'; + export class AppInfo { pid: number; isShow: boolean; @@ -33,6 +34,7 @@ export class AppInfo { }; displayType: displayType; i18n?: any; + constructor(app: TApp, pid: number) { this.isShow = false; this.zIndex = 1; @@ -66,7 +68,7 @@ const useAppStore = create()( launchQuery: {}, autolaunch: '', runner: new AppStateManager([]), - init: async () => { + async init() { const res = await request('/api/desktop/getInstalledApps'); set((state) => { state.installedApps = res?.data?.map((app: TApp) => new AppInfo(app, -1)); @@ -83,7 +85,12 @@ const useAppStore = create()( state.runningInfo = state.runningInfo.filter((item) => item.pid !== pid); }); }, - + closeAppAll: () => { + set((state) => { + state.runner.closeAppAll(); + state.runningInfo = []; + }); + }, installApp: (app: TApp) => { set((state) => { state.installedApps.push(new AppInfo(app, -1)); diff --git a/frontend/desktop/src/stores/callback.ts b/frontend/desktop/src/stores/callback.ts new file mode 100644 index 000000000..6e1451f2d --- /dev/null +++ b/frontend/desktop/src/stores/callback.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; + +type CallbackState = { + workspaceInviteCode?: string; + setWorkspaceInviteCode: (id?: string) => void; +}; + +const useCallbackStore = create()( + persist( + immer((set, get) => ({ + workspaceInviteCode: undefined, + setWorkspaceInviteCode: (id?: string) => { + set((state) => ({ workspaceInviteCode: id })); + } + })), + { + name: 'callbackParam' + } + ) +); + +export default useCallbackStore; diff --git a/frontend/desktop/src/styles/chakraTheme.ts b/frontend/desktop/src/styles/chakraTheme.ts index d71cc1117..2cd67888f 100644 --- a/frontend/desktop/src/styles/chakraTheme.ts +++ b/frontend/desktop/src/styles/chakraTheme.ts @@ -1,5 +1,6 @@ import { defineStyleConfig, extendTheme } from '@chakra-ui/react'; import { theme as originTheme } from '@sealos/ui'; + const Button = defineStyleConfig({ baseStyle: { borderRadius: '4px' @@ -41,12 +42,20 @@ const Select = defineStyleConfig({ variant: 'filled' } }); - +export const Modal = defineStyleConfig({ + baseStyle: { + header: { + fontWeight: 600, + fontSize: '20px' + } + } +}); export const theme = extendTheme(originTheme, { initialColorMode: 'light', // 'dark | 'light' components: { Button, Input, - Select + Select, + Modal } }); diff --git a/frontend/desktop/src/types/app.ts b/frontend/desktop/src/types/app.ts index 22b7813da..e06a21f87 100644 --- a/frontend/desktop/src/types/app.ts +++ b/frontend/desktop/src/types/app.ts @@ -77,6 +77,7 @@ export type TOSState = { ): Promise; // close app closeAppById: (pid: number) => void; + closeAppAll: () => void; // get current runningApp currentApp: () => AppInfo | undefined; switchAppById: (pid: number) => void; diff --git a/frontend/desktop/src/utils/ProcessManager.ts b/frontend/desktop/src/utils/ProcessManager.ts index 4f0b6e951..075ca067c 100644 --- a/frontend/desktop/src/utils/ProcessManager.ts +++ b/frontend/desktop/src/utils/ProcessManager.ts @@ -31,6 +31,9 @@ export default class AppStateManager { const idx = this.openedApps.findIndex((app) => app.pid === pid); this.openedApps.splice(idx, 1); } + closeAppAll() { + this.openedApps = []; + } loadApps(appKeys: string[]) { this.allApps = new Set(appKeys); } diff --git a/frontend/desktop/src/utils/switchKubeconfigNamespace.ts b/frontend/desktop/src/utils/switchKubeconfigNamespace.ts new file mode 100644 index 000000000..a7162aa7c --- /dev/null +++ b/frontend/desktop/src/utils/switchKubeconfigNamespace.ts @@ -0,0 +1,8 @@ +import * as yaml from 'js-yaml'; + +export function switchKubeconfigNamespace(kc: string, namespace: string) { + const oldKc = yaml.load(kc); + // @ts-ignore + oldKc.contexts[0].context.namespace = namespace; + return yaml.dump(oldKc); +} diff --git a/frontend/desktop/src/utils/tools.ts b/frontend/desktop/src/utils/tools.ts index c10f3bee3..082d2b2dd 100644 --- a/frontend/desktop/src/utils/tools.ts +++ b/frontend/desktop/src/utils/tools.ts @@ -66,24 +66,23 @@ export const retrySerially = (fn: () => Promise, times: number) => }; attempt(); }); -// 自己的权限能管理什么权限 -export const vaildManage = - (ownRole: UserRole, userId: string) => (targetRole: UserRole, tUserId: string) => { - if (targetRole === UserRole.Owner) - return [UserRole.Owner].includes(ownRole); // 不论目标是不是自己,都必须要最高权限 - else if (targetRole === UserRole.Manager) - return [UserRole.Owner, ...(tUserId === userId ? [UserRole.Manager] : [])].includes(ownRole); - //对自己操作定义的role可以是平级 - else if (targetRole === UserRole.Developer) - return [ - UserRole.Owner, - UserRole.Manager, - ...(tUserId === userId ? [UserRole.Developer] : []) - ].includes(ownRole); - else return false; - }; +// search manager relation +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) + return [UserRole.Owner, UserRole.Manager, ...(isSelf ? [UserRole.Developer] : [])].includes( + ownRole + ); + else return false; +}; export const isUserRole = (role: any): role is UserRole => [UserRole.Developer, UserRole.Manager, UserRole.Owner].includes(role); +export const UserRoleToRole = (role: UserRole): Role => + [Role.OWNER, Role.MANAGER, Role.DEVELOPER][role]; export const roleToUserRole = (role: Role): UserRole => { const map: Record = { [Role.OWNER]: UserRole.Owner, diff --git a/frontend/packages/client-sdk/src/app.ts b/frontend/packages/client-sdk/src/app.ts index de003cd8c..13b16d79b 100644 --- a/frontend/packages/client-sdk/src/app.ts +++ b/frontend/packages/client-sdk/src/app.ts @@ -1,11 +1,6 @@ import { v4 } from 'uuid'; import { API_NAME } from './constants'; -import { - AppMessageType, - AppSendMessageType, - MasterReplyMessageType, - SessionV1 -} from './types'; +import { AppMessageType, AppSendMessageType, MasterReplyMessageType, SessionV1 } from './types'; import { isBrowser } from './utils'; class ClientSDK { diff --git a/frontend/providers/applaunchpad/next.config.js b/frontend/providers/applaunchpad/next.config.js index 4ed35a916..e9900e333 100644 --- a/frontend/providers/applaunchpad/next.config.js +++ b/frontend/providers/applaunchpad/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ -const { i18n } = require('./next-i18next.config') -const path = require('path') +const { i18n } = require('./next-i18next.config'); +const path = require('path'); const nextConfig = { i18n, output: 'standalone', @@ -13,14 +13,14 @@ const nextConfig = { issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'] } - ]) - config.plugins = [...config.plugins] - return config + ]); + config.plugins = [...config.plugins]; + return config; }, transpilePackages: ['@sealos/driver', '@sealos/ui'], experimental: { outputFileTracingRoot: path.join(__dirname, '../../') } -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/frontend/providers/invite/next.config.js b/frontend/providers/invite/next.config.js index 61e2ee468..434f43348 100644 --- a/frontend/providers/invite/next.config.js +++ b/frontend/providers/invite/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ -const { i18n } = require('./next-i18next.config') -const path = require('path') +const { i18n } = require('./next-i18next.config'); +const path = require('path'); const nextConfig = { i18n, @@ -12,6 +12,6 @@ const nextConfig = { // this includes files from the monorepo base two directories up outputFileTracingRoot: path.join(__dirname, '../../') } -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/frontend/providers/template/next.config.js b/frontend/providers/template/next.config.js index 701ffccc4..545ed24da 100644 --- a/frontend/providers/template/next.config.js +++ b/frontend/providers/template/next.config.js @@ -1,10 +1,10 @@ /** @type {import('next').NextConfig} */ -const { i18n } = require('./next-i18next.config') -const path = require('path') +const { i18n } = require('./next-i18next.config'); +const path = require('path'); const withBundleAnalyzer = require('@next/bundle-analyzer')({ - enabled: process.env.ANALYZE === 'true', -}) + enabled: process.env.ANALYZE === 'true' +}); const nextConfig = { i18n, @@ -18,9 +18,9 @@ const nextConfig = { issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'] } - ]) - config.plugins = [...config.plugins] - return config + ]); + config.plugins = [...config.plugins]; + return config; }, experimental: { // this includes files from the monorepo base two directories up @@ -32,9 +32,9 @@ const nextConfig = { protocol: 'https', hostname: '**' } - ], + ] }, transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk'] -} +}; -module.exports = withBundleAnalyzer(nextConfig) +module.exports = withBundleAnalyzer(nextConfig);