diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 2935e2655..6eb1804f1 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -435,6 +435,42 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "DELETE", + url: "/user-management/users/:userId/admin-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + userId: z.string() + }), + response: { + 200: z.object({ + user: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true + }) + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const user = await server.services.superAdmin.deleteUserSuperAdminAccess(req.params.userId); + + return { + user + }; + } + }); + server.route({ method: "POST", url: "/bootstrap", @@ -450,9 +486,23 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ message: z.string(), - user: UsersSchema, - organization: OrganizationsSchema, - identity: IdentitiesSchema.extend({ + user: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true, + superAdmin: true + }), + organization: OrganizationsSchema.pick({ + id: true, + name: true, + slug: true + }), + identity: IdentitiesSchema.pick({ + id: true, + name: true + }).extend({ credentials: z.object({ token: z.string() }) // would just be Token AUTH for now @@ -478,7 +528,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }); return { - message: "Successfully boostrapped instance", + message: "Successfully bootstrapped instance", user: user.user, organization, identity: machineIdentity diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index b8fb5a3e3..317348cac 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -27,7 +27,7 @@ import { UserAliasType } from "../user-alias/user-alias-types"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { LoginMethod, - TAdminBoostrapInstanceDTO, + TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO @@ -291,7 +291,7 @@ export const superAdminServiceFactory = ({ return { token, user: userInfo, organization }; }; - const bootstrapInstance = async ({ email, password, organizationName }: TAdminBoostrapInstanceDTO) => { + const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => { const appCfg = getConfig(); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); if (serverCfg?.initialized) { @@ -453,6 +453,17 @@ export const superAdminServiceFactory = ({ return identity; }; + const deleteUserSuperAdminAccess = async (userId: string) => { + const user = await userDAL.findById(userId); + if (!user) { + throw new NotFoundError({ name: "User", message: "User not found" }); + } + + const updatedUser = userDAL.updateById(userId, { superAdmin: false }); + + return updatedUser; + }; + const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { const identities = await identityDAL.getIdentitiesByFilter({ limit, @@ -571,6 +582,7 @@ export const superAdminServiceFactory = ({ updateRootEncryptionStrategy, getConfiguredEncryptionStrategies, grantServerAdminAccessToUser, - deleteIdentitySuperAdminAccess + deleteIdentitySuperAdminAccess, + deleteUserSuperAdminAccess }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 49d48e464..64ec92632 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -16,7 +16,7 @@ export type TAdminSignUpDTO = { userAgent: string; }; -export type TAdminBoostrapInstanceDTO = { +export type TAdminBootstrapInstanceDTO = { email: string; password: string; organizationName: string; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 04fd8819e..ec92f2ad2 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -601,8 +601,8 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error { return nil } -func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (*BootstrapInstanceResponse, error) { - var resBody BootstrapInstanceResponse +func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) { + var resBody map[string]interface{} response, err := httpClient. R(). SetResult(&resBody). @@ -618,5 +618,5 @@ func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRe return nil, fmt.Errorf("CallBootstrapInstance: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) } - return &resBody, nil + return resBody, nil } diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index dba6bc3c7..3e22ab908 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -661,20 +661,3 @@ type BootstrapInstanceRequest struct { Organization string `json:"organization"` Domain string `json:"domain"` } - -type BootstrapInstanceResponseOrganization struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type BootstrapInstanceResponseIdentity struct { - ID string `json:"id"` - Name string `json:"name"` - Credentials interface{} `json:"credentials"` -} - -type BootstrapInstanceResponse struct { - Message string `json:"message"` - Organization BootstrapInstanceResponseOrganization `json:"organization"` - Identity BootstrapInstanceResponseIdentity `json:"identity"` -} diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go index c91ee75d9..5aaa0ebb8 100644 --- a/cli/packages/cmd/bootstrap.go +++ b/cli/packages/cmd/bootstrap.go @@ -70,11 +70,13 @@ var bootstrapCmd = &cobra.Command{ if err != nil { log.Error().Msgf("Failed to bootstrap instance: %v", err) + return } responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") if err != nil { log.Fatal().Msgf("Failed to convert response to JSON: %v", err) + return } fmt.Println(string(responseJSON)) }, diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index f4982dc14..d43fbc080 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -3,6 +3,7 @@ export { useAdminGrantServerAdminAccess, useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, + useRemoveUserServerAdminAccess, useUpdateAdminSlackConfig, useUpdateServerConfig, useUpdateServerEncryptionStrategy diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 754aca5e9..b3e1e37b4 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -88,6 +88,22 @@ export const useAdminRemoveIdentitySuperAdminAccess = () => { }); }; +export const useRemoveUserServerAdminAccess = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (userId: string) => { + await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}/admin-access`); + + return {}; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [adminStandaloneKeys.getUsers] + }); + } + }); +}; + export const useAdminGrantServerAdminAccess = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx index 84d0ed6f0..9395a9a8a 100644 --- a/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx +++ b/frontend/src/pages/admin/OverviewPage/components/UserPanel.tsx @@ -33,22 +33,26 @@ import { THead, Tr } from "@app/components/v2"; -import { useSubscription, useUser } from "@app/context"; +import { useSubscription } from "@app/context"; import { useDebounce, usePopUp } from "@app/hooks"; import { useAdminDeleteUser, useAdminGetUsers, - useAdminGrantServerAdminAccess + useAdminGrantServerAdminAccess, + useRemoveUserServerAdminAccess } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions"; +const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user"; const UserPanelTable = ({ handlePopUpOpen }: { handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["removeUser", "upgradePlan", "upgradeToServerAdmin"]>, + popUpName: keyof UsePopUpState< + ["removeUser", "upgradePlan", "upgradeToServerAdmin", "removeServerAdmin"] + >, data?: { username: string; id: string; @@ -58,8 +62,6 @@ const UserPanelTable = ({ }) => { const [searchUserFilter, setSearchUserFilter] = useState(""); const [adminsOnly, setAdminsOnly] = useState(false); - const { user } = useUser(); - const userId = user?.id || ""; const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { subscription } = useSubscription(); @@ -143,45 +145,61 @@ const UserPanelTable = ({ {email} - {userId !== id && ( -
- - -
- -
-
- +
+ + +
+ +
+
+ + { + e.stopPropagation(); + handlePopUpOpen("removeUser", { username, id }); + }} + > + Remove User + + {!superAdmin && ( { e.stopPropagation(); - handlePopUpOpen("removeUser", { username, id }); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: addServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("upgradeToServerAdmin", { username, id }); }} > - Remove User + Make User Server Admin - {!superAdmin && ( - { - e.stopPropagation(); - if (!subscription?.instanceUserManagement) { - handlePopUpOpen("upgradePlan", { - username, - id, - message: addServerAdminUpgradePlanMessage - }); - return; - } - handlePopUpOpen("upgradeToServerAdmin", { username, id }); - }} - > - Make User Server Admin - - )} - -
-
- )} + )} + {superAdmin && ( + { + e.stopPropagation(); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: removeServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("removeServerAdmin", { username, id }); + }} + > + Remove Server Admin + + )} +
+
+
); @@ -212,11 +230,13 @@ export const UserPanel = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "removeUser", "upgradePlan", - "upgradeToServerAdmin" + "upgradeToServerAdmin", + "removeServerAdmin" ] as const); const { mutateAsync: deleteUser } = useAdminDeleteUser(); const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess(); + const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess(); const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; @@ -256,6 +276,25 @@ export const UserPanel = () => { handlePopUpClose("upgradeToServerAdmin"); }; + const handleRemoveServerAdminAccess = async () => { + const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string }; + + try { + await removeAdminAccess(id); + createNotification({ + type: "success", + text: "Successfully removed server admin access from user" + }); + } catch { + createNotification({ + type: "error", + text: "Error removing server admin access from user" + }); + } + + handlePopUpClose("removeServerAdmin"); + }; + return (
@@ -282,6 +321,17 @@ export const UserPanel = () => { onDeleteApproved={handleGrantServerAdminAccess} buttonText="Grant Access" /> + handlePopUpToggle("removeServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleRemoveServerAdminAccess} + buttonText="Remove Access" + /> handlePopUpToggle("upgradePlan", isOpen)}