From 7091b8665cab2b149c9e2fef629f64289cb17f49 Mon Sep 17 00:00:00 2001 From: zijiren <84728412+zijiren233@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:54:04 +0800 Subject: [PATCH] feat: user private ns invite (#5043) * feat: user private ns support invite * feat: user controller * feat: ns rename * fix: canAbdicate * feat: operationrequest controller cannot transfer personal workspace * adapt old rolebinding (#73) * replace owner reference user to bind user * make format --------- Co-authored-by: Jiahui Co-authored-by: jiahui --- .../adapt_rolebinding_controller.go | 137 ++++++++++++++++++ .../user/controllers/helper/config/rbac.go | 6 +- .../operationrequest_controller.go | 39 +++-- .../user/controllers/user_controller.go | 27 ++-- controllers/user/main.go | 7 + .../desktop/public/locales/en/common.json | 3 +- .../desktop/public/locales/zh/common.json | 3 +- frontend/desktop/src/api/namespace.ts | 4 + .../src/components/team/RenameTeam.tsx | 127 ++++++++++++++++ .../src/components/team/TeamCenter.tsx | 58 +++++--- .../src/components/team/WorkspaceToggle.tsx | 3 +- .../desktop/src/components/team/userTable.tsx | 35 ++--- .../src/pages/api/auth/namespace/details.ts | 5 +- .../pages/api/auth/namespace/getInviteCode.ts | 1 - .../src/pages/api/auth/namespace/invite.ts | 1 - .../pages/api/auth/namespace/modifyRole.ts | 1 - .../pages/api/auth/namespace/removeUser.ts | 1 - .../src/pages/api/auth/namespace/rename.ts | 49 +++++++ .../pages/api/auth/namespace/verifyInvite.ts | 1 - .../src/services/backend/regionAuth.ts | 2 +- 20 files changed, 429 insertions(+), 81 deletions(-) create mode 100644 controllers/user/controllers/adapt_rolebinding_controller.go create mode 100644 frontend/desktop/src/components/team/RenameTeam.tsx create mode 100644 frontend/desktop/src/pages/api/auth/namespace/rename.ts diff --git a/controllers/user/controllers/adapt_rolebinding_controller.go b/controllers/user/controllers/adapt_rolebinding_controller.go new file mode 100644 index 00000000000..735d4baa487 --- /dev/null +++ b/controllers/user/controllers/adapt_rolebinding_controller.go @@ -0,0 +1,137 @@ +/* +Copyright 2022 labring. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/labring/sealos/controllers/user/controllers/helper/config" + + "sigs.k8s.io/controller-runtime/pkg/builder" + + v1 "k8s.io/api/rbac/v1" + + "sigs.k8s.io/controller-runtime/pkg/event" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// TODO This controller is used to adapt the old RoleBinding. only need to deploy the logic once for conversion and delete the controller in the future + +// AdaptRoleBindingReconciler reconciles a RoleBinding object, Old Role bindings are backward compatible and will be deleted in the future +type AdaptRoleBindingReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger +} + +func (r *AdaptRoleBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + rolebinding := &v1.RoleBinding{} + if err := r.Get(ctx, req.NamespacedName, rolebinding); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // old rolebinding only has one subject + if len(rolebinding.Subjects) != 1 { + return ctrl.Result{}, nil + } + + if rolebinding.Subjects[0].Namespace != config.GetUserSystemNamespace() { + userName := rolebinding.GetAnnotations()[userAnnotationOwnerKey] + user := &userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Name: userName}, user); err != nil { + r.Logger.Error(err, "get user failed") + return ctrl.Result{}, err + } + appendSubject := rolebinding.Subjects[0].DeepCopy() + appendSubject.Namespace = config.GetUserSystemNamespace() + rolebinding.Subjects = append(rolebinding.Subjects, *appendSubject) + if err := r.Update(ctx, rolebinding); err != nil { + r.Logger.Error(err, "update rolebinding failed") + return ctrl.Result{}, err + } + if err := controllerutil.SetControllerReference(user, rolebinding, r.Scheme); err != nil { + r.Logger.Error(err, "set controller reference failed") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AdaptRoleBindingReconciler) SetupWithManager(mgr ctrl.Manager) error { + const controllerName = "adapt_rolebinding_controller" + if r.Client == nil { + r.Client = mgr.GetClient() + } + r.Logger = ctrl.Log.WithName(controllerName) + r.Scheme = mgr.GetScheme() + r.Logger.V(1).Info("init reconcile AdaptRoleBinding controller") + return ctrl.NewControllerManagedBy(mgr). + For(&v1.RoleBinding{}, builder.WithPredicates(WorkspacePredicate{})). + Complete(r) +} + +type WorkspacePredicate struct { +} + +func (WorkspacePredicate) Create(e event.CreateEvent) bool { + return isWorkspaceObject(e.Object) +} + +func (WorkspacePredicate) Delete(_ event.DeleteEvent) bool { + return false +} + +func (WorkspacePredicate) Update(_ event.UpdateEvent) bool { + return false +} + +func (WorkspacePredicate) Generic(_ event.GenericEvent) bool { + return false +} + +func isWorkspaceObject(obj client.Object) bool { + rolebinding, ok := obj.(*v1.RoleBinding) + if !ok { + return false + } + anno := obj.GetAnnotations() + if anno == nil { + return false + } + if anno["user.sealos.io/owner"] == "" { + return false + } + if len(obj.GetOwnerReferences()) > 0 { + return false + } + + for _, sub := range rolebinding.Subjects { + if sub.Namespace == config.GetUserSystemNamespace() { + return false + } + } + return true +} diff --git a/controllers/user/controllers/helper/config/rbac.go b/controllers/user/controllers/helper/config/rbac.go index d3bf86fca92..03f2712b93f 100644 --- a/controllers/user/controllers/helper/config/rbac.go +++ b/controllers/user/controllers/helper/config/rbac.go @@ -26,6 +26,10 @@ import ( userv1 "github.com/labring/sealos/controllers/user/api/v1" ) +func GetUserSystemNamespace() string { + return "user-system" +} + func GetDefaultNamespace() string { return os.Getenv("NAMESPACE_NAME") } @@ -35,7 +39,7 @@ func GetUsersSubject(user string) []rbacv1.Subject { { Kind: "ServiceAccount", Name: user, - Namespace: GetUsersNamespace(user), + Namespace: GetUserSystemNamespace(), }, } } diff --git a/controllers/user/controllers/operationrequest_controller.go b/controllers/user/controllers/operationrequest_controller.go index edbfd966217..d9bcab550d8 100644 --- a/controllers/user/controllers/operationrequest_controller.go +++ b/controllers/user/controllers/operationrequest_controller.go @@ -135,22 +135,37 @@ func (r *OperationReqReconciler) reconcile(ctx context.Context, request *userv1. "rolebinding.roleRef", rolebinding.RoleRef, ) + user := &userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Name: config.GetUserNameByNamespace(request.Namespace)}, user); err != nil { + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to get user", "Failed to get user %s", request.Spec.User) + return ctrl.Result{}, err + } + if request.Spec.Role == userv1.OwnerRoleType { + if user.Name == user.Annotations[userv1.UserAnnotationOwnerKey] { + // 不允许转移个人空间 + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to grant role", "Failed to grant role %s to user %s, cannot transfer personal workspace", request.Spec.Role, request.Spec.User) + return ctrl.Result{}, r.updateRequestStatus(ctx, request, userv1.RequestFailed) + } + } + bindUser := &userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Name: request.Spec.User}, bindUser); err != nil { + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to get bind user", "Failed to get bind user %s", request.Spec.User) + return ctrl.Result{}, err + } + setUpOwnerReferenceFc := func() error { + return ctrl.SetControllerReference(bindUser, rolebinding, r.Scheme) + } + // handle OperationRequest, create or delete rolebinding switch request.Spec.Action { case userv1.Grant: r.Recorder.Eventf(request, v1.EventTypeNormal, "Grant", "Grant role %s to user %s", request.Spec.Role, request.Spec.User) - if _, err := ctrl.CreateOrUpdate(ctx, r.Client, rolebinding, func() error { return nil }); err != nil { + if _, err := ctrl.CreateOrUpdate(ctx, r.Client, rolebinding, setUpOwnerReferenceFc); err != nil { r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to create/update rolebinding", "Failed to create rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) return ctrl.Result{}, err } if request.Spec.Role == userv1.OwnerRoleType { // update user annotation - user := &userv1.User{} - if err := r.Get(ctx, client.ObjectKey{Name: config.GetUserNameByNamespace(request.Namespace)}, user); err != nil { - r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to get user", "Failed to get user %s", request.Spec.User) - return ctrl.Result{}, err - } - user.Annotations[userv1.UserAnnotationOwnerKey] = request.Spec.User if err := r.Update(ctx, user); err != nil { r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to update user", "Failed to update user %s", request.Spec.User) @@ -169,18 +184,12 @@ func (r *OperationReqReconciler) reconcile(ctx context.Context, request *userv1. r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to delete rolebinding", "Failed to delete rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) return ctrl.Result{}, err } - if _, err := ctrl.CreateOrUpdate(ctx, r.Client, rolebinding, func() error { return nil }); err != nil { + if _, err := ctrl.CreateOrUpdate(ctx, r.Client, rolebinding, setUpOwnerReferenceFc); err != nil { r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to create/update rolebinding", "Failed to create rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) return ctrl.Result{}, err } if request.Spec.Role == userv1.OwnerRoleType { // update user annotation - user := &userv1.User{} - if err := r.Get(ctx, client.ObjectKey{Name: config.GetUserNameByNamespace(request.Namespace)}, user); err != nil { - r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to get user", "Failed to get user %s", request.Spec.User) - return ctrl.Result{}, err - } - user.Annotations[userv1.UserAnnotationOwnerKey] = request.Spec.User if err := r.Update(ctx, user); err != nil { r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to update user", "Failed to update user %s", request.Spec.User) @@ -260,7 +269,7 @@ func conventRequestToRolebinding(request *userv1.Operationrequest) *rbacv1.RoleB { Kind: rbacv1.ServiceAccountKind, Name: request.Spec.User, - Namespace: config.GetUsersNamespace(request.Spec.User), + Namespace: config.GetUserSystemNamespace(), }, }, RoleRef: rbacv1.RoleRef{ diff --git a/controllers/user/controllers/user_controller.go b/controllers/user/controllers/user_controller.go index 88d18b0a818..d05598b4e5a 100644 --- a/controllers/user/controllers/user_controller.go +++ b/controllers/user/controllers/user_controller.go @@ -57,9 +57,11 @@ import ( "github.com/labring/sealos/controllers/user/controllers/helper" ) -var userAnnotationCreatorKey = userv1.UserAnnotationCreatorKey -var userAnnotationOwnerKey = userv1.UserAnnotationOwnerKey -var userLabelOwnerKey = userv1.UserLabelOwnerKey +const ( + userAnnotationCreatorKey = userv1.UserAnnotationCreatorKey + userAnnotationOwnerKey = userv1.UserAnnotationOwnerKey + userLabelOwnerKey = userv1.UserLabelOwnerKey +) // UserReconciler reconciles a User object type UserReconciler struct { @@ -378,22 +380,15 @@ func (r *UserReconciler) syncServiceAccount(ctx context.Context, user *userv1.Us } }() ctx = context.WithValue(ctx, ctxKey("reNew"), false) - sa := &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: user.Name, - Namespace: config.GetDefaultNamespace(), - }, - } - _ = r.Delete(context.Background(), sa) if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { var change controllerutil.OperationResult var err error - sa = &v1.ServiceAccount{} + sa := &v1.ServiceAccount{} sa.Name = user.Name - sa.Namespace = config.GetUsersNamespace(user.Name) + sa.Namespace = config.GetUserSystemNamespace() sa.Labels = map[string]string{} if err = r.Get(context.Background(), client.ObjectKey{ - Namespace: config.GetUsersNamespace(user.Name), + Namespace: config.GetUserSystemNamespace(), Name: user.Name, }, sa); err != nil { if apierrors.IsNotFound(err) { @@ -423,12 +418,12 @@ func (r *UserReconciler) syncServiceAccount(ctx context.Context, user *userv1.Us ctx = context.WithValue(ctx, ctxKey("reNew"), true) } saCondition.Message = fmt.Sprintf("sync namespace sa %s/%s successfully", sa.Name, sa.ResourceVersion) + ctx = context.WithValue(ctx, ctxKey("serviceAccount"), sa) return nil }); err != nil { helper.SetConditionError(saCondition, "SyncUserError", err) r.Recorder.Eventf(user, v1.EventTypeWarning, "syncUserServiceAccount", "Sync User namespace sa %s is error: %v", user.Name, err) } - ctx = context.WithValue(ctx, ctxKey("serviceAccount"), sa) return ctx } @@ -459,7 +454,7 @@ func (r *UserReconciler) syncServiceAccountSecrets(ctx context.Context, user *us secretName := sa.Secrets[0].Name secrets := &v1.Secret{} secrets.Name = secretName - secrets.Namespace = config.GetUsersNamespace(user.Name) + secrets.Namespace = config.GetUserSystemNamespace() var err error if err = r.Get(ctx, client.ObjectKeyFromObject(secrets), secrets); err == nil { return nil @@ -512,7 +507,7 @@ func (r *UserReconciler) syncKubeConfig(ctx context.Context, user *userv1.User) return ctx } user.Status.ObservedCSRExpirationSeconds = user.Spec.CSRExpirationSeconds - cfg := kubeconfig.NewConfig(user.Name, "", user.Spec.CSRExpirationSeconds).WithServiceAccountConfig(config.GetUsersNamespace(user.Name), sa) + cfg := kubeconfig.NewConfig(user.Name, "", user.Spec.CSRExpirationSeconds).WithServiceAccountConfig(config.GetUserSystemNamespace(), sa) apiConfig, err := cfg.Apply(r.config, r.Client) if err != nil { helper.SetConditionError(userCondition, "SyncKubeConfigError", err) diff --git a/controllers/user/main.go b/controllers/user/main.go index fdea924c3fe..f52448906a0 100644 --- a/controllers/user/main.go +++ b/controllers/user/main.go @@ -161,6 +161,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DeleteRequest") os.Exit(1) } + if err = (&controllers.AdaptRoleBindingReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AdaptRoleBinding") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index da7b15a43a6..e58238ecbe5 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -224,5 +224,6 @@ "you_can_use_the_kubectl_command_directly_from_the_terminal": "You can use the kubectl command directly from the terminal", "you_can_view_fees_through_the_fee_center": "You can view fees through the fee center", "you_have_not_purchased_the_license": "You have not purchased the License", - "yuan": "Yuan" + "yuan": "Yuan", + "rename": "Rename" } diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index 828abe4f60d..6e9d0bf7bea 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -217,5 +217,6 @@ "you_can_use_the_kubectl_command_directly_from_the_terminal": "您可通过终端直接使用 kubectl 命令", "you_can_view_fees_through_the_fee_center": "您可通过费用中心查看费用", "you_have_not_purchased_the_license": "您还没有购买 License", - "yuan": "元" + "yuan": "元", + "rename": "重命名" } diff --git a/frontend/desktop/src/api/namespace.ts b/frontend/desktop/src/api/namespace.ts index 6af483d3784..3a07eac2d9b 100644 --- a/frontend/desktop/src/api/namespace.ts +++ b/frontend/desktop/src/api/namespace.ts @@ -78,6 +78,9 @@ export const _switchRequest = (request: AxiosInstance) => (ns_uid: string) => request.post>('/api/auth/namespace/switch', { ns_uid }); +export const _renameRequest = + (request: AxiosInstance) => (data: { ns_uid: string; teamName: string }) => + request.post>('/api/auth/namespace/rename', data); // for prod/dev export const abdicateRequest = _abdicateRequest(request); export const createRequest = _createRequest(request); @@ -93,3 +96,4 @@ export const switchRequest = _switchRequest(request); export const getInviteCodeRequest = _getInviteCodeRequest(request); export const getInviteCodeInfoRequest = _getInviteCodeInfoRequest(request); export const verifyInviteCodeRequest = _verifyInviteCodeRequest(request); +export const renameRequest = _renameRequest(request); diff --git a/frontend/desktop/src/components/team/RenameTeam.tsx b/frontend/desktop/src/components/team/RenameTeam.tsx new file mode 100644 index 00000000000..e50d40f7803 --- /dev/null +++ b/frontend/desktop/src/components/team/RenameTeam.tsx @@ -0,0 +1,127 @@ +import { + Button, + Image, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + useDisclosure, + Text, + Spinner, + ButtonProps +} from '@chakra-ui/react'; +import CustomInput from './Input'; +import { useState, useEffect } from 'react'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { renameRequest } from '@/api/namespace'; +import { ApiResp } from '@/types'; +import { useTranslation } from 'next-i18next'; +import { EditIcon } from '@sealos/ui'; + +export default function RenameTeam({ + ns_uid, + defaultTeamName, + ...props +}: { + ns_uid: string; + defaultTeamName: string; +} & ButtonProps) { + console.log(defaultTeamName); + const { onOpen, isOpen, onClose } = useDisclosure(); + const [teamName, setTeamName] = useState(defaultTeamName); + const { toast } = useCustomToast({ status: 'error' }); + const queryClient = useQueryClient(); + + useEffect(() => { + setTeamName(defaultTeamName); + }, [defaultTeamName]); + + const mutation = useMutation({ + mutationFn: renameRequest, + onSuccess() { + setTeamName(defaultTeamName); + queryClient.invalidateQueries({ + queryKey: ['teamList'], + exact: false + }); + queryClient.invalidateQueries({ + queryKey: ['ns-detail'], + exact: false + }); + onClose(); + }, + onError(error) { + toast({ title: (error as ApiResp).message }); + } + }); + const { t } = useTranslation(); + const submit = () => { + mutation.mutate({ ns_uid, teamName }); + }; + + return ( + <> + + + + + + + {t('common:rename')} + + {mutation.isLoading ? ( + + ) : ( + + { + e.preventDefault(); + setTeamName(e.target.value); + }} + placeholder={t('common:name_of_team') || ''} + value={teamName} + /> + + + )} + + + + ); +} diff --git a/frontend/desktop/src/components/team/TeamCenter.tsx b/frontend/desktop/src/components/team/TeamCenter.tsx index 18bc1aab970..7d668ab0c59 100644 --- a/frontend/desktop/src/components/team/TeamCenter.tsx +++ b/frontend/desktop/src/components/team/TeamCenter.tsx @@ -34,6 +34,7 @@ import { nsListRequest, reciveMessageRequest, teamDetailsRequest } from '@/api/n import { useTranslation } from 'next-i18next'; import { CopyIcon, ListIcon, SettingIcon, StorageIcon } from '@sealos/ui'; import NsListItem from '@/components/team/NsListItem'; +import RenameTeam from './RenameTeam'; export default function TeamCenter(props: StackProps) { const session = useSessionStore((s) => s.session); @@ -69,7 +70,7 @@ export default function TeamCenter(props: StackProps) { const users: TeamUserDto[] = [...(data?.data?.users || [])]; const curTeamUser = users.find((user) => user.crUid === userCrUid); const namespace = data?.data?.namespace; - const isTeam = namespace?.nstype === NSType.Team; + const isPrivate = namespace?.nstype === NSType.Private; // inviting message list const reciveMessage = useQuery({ queryKey: ['teamRecive', 'teamGroup'], @@ -85,7 +86,7 @@ export default function TeamCenter(props: StackProps) { return data.data?.namespaces; } }); - const namespaces = _namespaces?.filter((ns) => ns.nstype !== NSType.Private) || []; + const namespaces = _namespaces || []; useEffect(() => { const defaultNamespace = namespaces?.length > 0 @@ -209,22 +210,33 @@ export default function TeamCenter(props: StackProps) { <> - + - {namespace.teamName} + {isPrivate + ? `${t('common:default_team')} - ${namespace.teamName}` + : namespace.teamName} - {isTeam && curTeamUser?.role === UserRole.Owner && ( - { - if (delete_ns_uid === ns_uid) { - setNs_uid(''); - setNsid(''); - } - }} - /> + {curTeamUser?.role === UserRole.Owner && ( + + + {!isPrivate && ( + { + if (delete_ns_uid === ns_uid) { + setNs_uid(''); + setNsid(''); + } + }} + /> + )} + )} @@ -272,19 +284,25 @@ export default function TeamCenter(props: StackProps) { > {users.length} - {isTeam && - curTeamUser && + {curTeamUser && [UserRole.Owner, UserRole.Manager].includes(curTeamUser.role) && ( )} - + diff --git a/frontend/desktop/src/components/team/WorkspaceToggle.tsx b/frontend/desktop/src/components/team/WorkspaceToggle.tsx index fb052b8936c..1c5cf3af2f6 100644 --- a/frontend/desktop/src/components/team/WorkspaceToggle.tsx +++ b/frontend/desktop/src/components/team/WorkspaceToggle.tsx @@ -18,7 +18,8 @@ export default function WorkspaceToggle() { const disclosure = useDisclosure(); const { setWorkSpaceId, session } = useSessionStore(); const { t } = useTranslation(); - const ns_uid = session?.user?.ns_uid || ''; + const user = session?.user; + const ns_uid = user?.ns_uid || ''; const router = useRouter(); const queryClient = useQueryClient(); const { init } = useAppStore(); diff --git a/frontend/desktop/src/components/team/userTable.tsx b/frontend/desktop/src/components/team/userTable.tsx index b78a1969c83..4a75ff1b6d2 100644 --- a/frontend/desktop/src/components/team/userTable.tsx +++ b/frontend/desktop/src/components/team/userTable.tsx @@ -23,14 +23,14 @@ import { useConfigStore } from '@/stores/config'; export default function UserTable({ users = [], - isTeam, ns_uid, - nsid + nsid, + canAbdicate }: { users: TeamUserDto[]; - isTeam: boolean; ns_uid: string; nsid: string; + canAbdicate: boolean; }) { const { t } = useTranslation(); const headList = [ @@ -115,22 +115,23 @@ export default function UserTable({ {status[user.status]} - {isTeam && - (userCrUid && - canManage(user.role, user.crUid === userCrUid) && - otherWorkspaceUsers.length !== 0 ? ( - user.role === UserRole.Owner ? ( + {userCrUid && + canManage(user.role, user.crUid === userCrUid) && + otherWorkspaceUsers.length !== 0 ? ( + user.role === UserRole.Owner ? ( + canAbdicate ? ( - ) : userCrUid !== user.uid ? ( - ) : null - ) : null)} + ) : userCrUid !== user.uid ? ( + + ) : null + ) : null} ))} diff --git a/frontend/desktop/src/pages/api/auth/namespace/details.ts b/frontend/desktop/src/pages/api/auth/namespace/details.ts index ca3ecaec173..43531d74c53 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/details.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/details.ts @@ -16,8 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return jsonRes(res, { code: 400, message: 'ns_uid is invaild' }); const queryResult = await prisma.userWorkspace.findMany({ where: { - workspaceUid: ns_uid, - isPrivate: false + workspaceUid: ns_uid }, include: { workspace: true, @@ -45,7 +44,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) role: roleToUserRole(selfItem.role), createTime: workspace.createdAt, teamName: workspace.displayName, - nstype: NSType.Team + nstype: workspace.id === 'ns-' + payload.userCrName ? NSType.Private : NSType.Team }; const users = queryResult.flatMap((x) => { const user = userResult.find((user) => user.uid === x.userCr.userUid); diff --git a/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts b/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts index 327779cc67d..5e6ad076b20 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/getInviteCode.ts @@ -38,7 +38,6 @@ 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 = ([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) diff --git a/frontend/desktop/src/pages/api/auth/namespace/invite.ts b/frontend/desktop/src/pages/api/auth/namespace/invite.ts index 6f73625a572..c8736e7a3e9 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/invite.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/invite.ts @@ -53,7 +53,6 @@ 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))(role, false); if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' }); if (queryResults.length === 0) diff --git a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts index 4b800d6babf..4769b2a41ec 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/modifyRole.ts @@ -29,7 +29,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const queryResults = await prisma.userWorkspace.findMany({ where: { workspaceUid: ns_uid, - isPrivate: false, userCrUid: { in: [payload.userCrUid, targetUserCrUid] }, diff --git a/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts b/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts index d2145af7fdb..38ae0c40ec3 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/removeUser.ts @@ -24,7 +24,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const queryResults = await prisma.userWorkspace.findMany({ where: { workspaceUid: ns_uid, - isPrivate: false, userCrUid: { in: [payload.userCrUid, targetUserCrUid] } diff --git a/frontend/desktop/src/pages/api/auth/namespace/rename.ts b/frontend/desktop/src/pages/api/auth/namespace/rename.ts new file mode 100644 index 00000000000..3ed599c2186 --- /dev/null +++ b/frontend/desktop/src/pages/api/auth/namespace/rename.ts @@ -0,0 +1,49 @@ +import { jsonRes } from '@/services/backend/response'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { prisma } from '@/services/backend/db/init'; +import { validate } from 'uuid'; +import { Role } from 'prisma/region/generated/client'; +import { verifyAccessToken } from '@/services/backend/auth'; + +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 { teamName, ns_uid } = req.body as { teamName: string; ns_uid: string }; + if (!teamName) return jsonRes(res, { code: 400, message: 'teamName is required' }); + if (!ns_uid || !validate(ns_uid)) + return jsonRes(res, { code: 400, message: 'ns_uid is invaild' }); + + const queryResult = await prisma.userWorkspace.findFirst({ + where: { + workspaceUid: ns_uid, + userCrUid: payload.userCrUid, + role: Role.OWNER + }, + include: { + workspace: true + } + }); + + if (!queryResult) return jsonRes(res, { code: 404, message: 'the namespace is not found' }); + + const updatedWorkspace = await prisma.workspace.update({ + where: { + uid: ns_uid + }, + data: { + displayName: teamName + } + }); + + if (!updatedWorkspace) return jsonRes(res, { code: 500, message: 'failed to rename team' }); + + jsonRes(res, { + code: 200, + message: 'Successfully' + }); + } catch (e) { + console.log(e); + jsonRes(res, { code: 500, message: 'failed to rename team' }); + } +} diff --git a/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts b/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts index efab03ddc78..1b3b2fd52f9 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/verifyInvite.ts @@ -28,7 +28,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) workspaceUid: ns_uid, userCrUid: payload.userCrUid }, - isPrivate: false, status: JoinStatus.INVITED }, include: { diff --git a/frontend/desktop/src/services/backend/regionAuth.ts b/frontend/desktop/src/services/backend/regionAuth.ts index 8cb18d5842b..3c50e3b177f 100644 --- a/frontend/desktop/src/services/backend/regionAuth.ts +++ b/frontend/desktop/src/services/backend/regionAuth.ts @@ -62,7 +62,7 @@ export async function getRegionToken({ if (userCrResult) { // get a exist user const relations = userCrResult.userWorkspace!; - const privateRelation = relations.find((r) => r.isPrivate === true); + const privateRelation = relations.find((r) => r.isPrivate); return { userUid: userCrResult.userUid, userCrUid: userCrResult.uid,