improvements: address feedback

This commit is contained in:
Scott Wilson
2025-09-02 12:44:02 -07:00
parent 394538769b
commit 4d6e12d6b2
10 changed files with 59 additions and 42 deletions
+1 -1
View File
@@ -850,7 +850,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
name: GenericResourceNameSchema, name: GenericResourceNameSchema,
inviteAdminEmails: z.string().email().array() inviteAdminEmails: z.string().email().array().min(1)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -9,7 +9,6 @@ interface OrganizationAssignmentTemplateProps extends Omit<BaseEmailWrapperProps
inviterFirstName?: string; inviterFirstName?: string;
inviterUsername?: string; inviterUsername?: string;
organizationName: string; organizationName: string;
email: string;
callback_url: string; callback_url: string;
} }
@@ -75,7 +75,7 @@ type TSuperAdminServiceFactoryDep = {
kmsRootConfigDAL: TKmsRootConfigDALFactory; kmsRootConfigDAL: TKmsRootConfigDALFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "inviteUserToOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization" | "inviteUserToOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">; keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "updateSubscriptionOrgMemberCount">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">; microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
invalidateCacheQueue: TInvalidateCacheQueueFactory; invalidateCacheQueue: TInvalidateCacheQueueFactory;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
@@ -811,13 +811,13 @@ export const superAdminServiceFactory = ({
} }
const inviteeUserId = inviteeUser?.id; const inviteeUserId = inviteeUser?.id;
const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx); const existingEncryptionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
// when user is missing the encrytion keys // when user is missing the encrytion keys
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp // this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key // So what we do is we generate a random secure password and then encrypt it with a random pub-private key
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) { if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncryptionKey)) {
await userDAL.createUserEncryption( await userDAL.createUserEncryption(
{ {
userId: inviteeUserId, userId: inviteeUserId,
@@ -853,6 +853,8 @@ export const superAdminServiceFactory = ({
return { organization: org, users }; return { organization: org, users };
}); });
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
await Promise.allSettled( await Promise.allSettled(
usersToEmail.map(async (user) => { usersToEmail.map(async (user) => {
if (!user.email) return; if (!user.email) return;
+3 -1
View File
@@ -1,3 +1,5 @@
import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
import { Organization } from "../types"; import { Organization } from "../types";
export enum LoginMethod { export enum LoginMethod {
@@ -20,7 +22,7 @@ export type OrganizationWithProjects = Organization & {
lastName: string | null; lastName: string | null;
}; };
membershipId: string; membershipId: string;
status: string; status: OrgMembershipStatus;
role: string; role: string;
roleId: string | null; roleId: string | null;
}[]; }[];
@@ -159,3 +159,8 @@ export enum OrgIdentityOrderBy {
Name = "name", Name = "name",
Role = "role" Role = "role"
} }
export enum OrgMembershipStatus {
Invited = "invited",
Accepted = "accepted"
}
@@ -192,14 +192,18 @@ export const Navbar = () => {
<p className="pl-1 pr-3 text-lg text-mineshaft-400/70">/</p> <p className="pl-1 pr-3 text-lg text-mineshaft-400/70">/</p>
{isServerAdminPanel ? ( {isServerAdminPanel ? (
<> <>
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"> <Link
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div> <div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" /> <FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div> </div>
<div className="whitespace-nowrap">Server Console</div> <div className="whitespace-nowrap">Server Console</div>
</div> </Link>
<p className="pl-3 pr-3 text-lg text-mineshaft-400/70">/</p> <p className="pl-3 pr-3 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? ( {breadcrumbs ? (
// scott: remove /admin as we show server console above
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} /> <BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
) : null} ) : null}
</> </>
@@ -89,11 +89,7 @@ const Content = ({ onClose }: ContentProps) => {
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Controller <Controller
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl isError={Boolean(error)} errorText={error?.message} label="User">
isError={Boolean(error)}
errorText={error?.message}
label="Assign Organization Admins"
>
<FilterableSelect <FilterableSelect
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching} isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
className="w-full" className="w-full"
@@ -120,7 +116,7 @@ const Content = ({ onClose }: ContentProps) => {
isDisabled={isSubmitting} isDisabled={isSubmitting}
colorSchema="secondary" colorSchema="secondary"
> >
Grant Server Admin Grant
</Button> </Button>
<Button onClick={() => onClose()} variant="plain" colorSchema="secondary"> <Button onClick={() => onClose()} variant="plain" colorSchema="secondary">
Cancel Cancel
@@ -133,7 +129,11 @@ const Content = ({ onClose }: ContentProps) => {
export const AddServerAdminModal = ({ isOpen, onOpenChange }: Props) => { export const AddServerAdminModal = ({ isOpen, onOpenChange }: Props) => {
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent bodyClassName="overflow-visible" title="Grant Server Admin"> <ModalContent
bodyClassName="overflow-visible"
title="Grant Server Admin"
subTitle="Grant server admin status to a user"
>
<Content onClose={() => onOpenChange(false)} /> <Content onClose={() => onOpenChange(false)} />
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -424,14 +424,14 @@ export const ServerAdminsTable = () => {
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.removeUsers.isOpen} isOpen={popUp.removeUsers.isOpen}
title="Are you sure you want to remove the following users?" title="Are you sure you want to delete the following users?"
onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)} onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => handleRemoveUsers()} onDeleteApproved={() => handleRemoveUsers()}
buttonText="Remove" buttonText="Remove"
> >
<div className="mt-4 text-sm text-mineshaft-400"> <div className="mt-4 text-sm text-mineshaft-400">
The following members will be removed: The following users will be deleted:
</div> </div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200"> <div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc"> <ul className="list-disc">
@@ -458,7 +458,7 @@ export const ServerAdminsTable = () => {
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap" className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
> >
<FontAwesomeIcon icon={faWarning} /> <FontAwesomeIcon icon={faWarning} />
<span>Removing Yourself</span> <span>Deleting Yourself</span>
</Badge> </Badge>
</div> </div>
</Tooltip> </Tooltip>
@@ -5,6 +5,7 @@ import {
faBuilding, faBuilding,
faCircleQuestion, faCircleQuestion,
faEllipsisV, faEllipsisV,
faEnvelope,
faEye, faEye,
faMagnifyingGlass, faMagnifyingGlass,
faPlus, faPlus,
@@ -52,6 +53,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { OrganizationWithProjects } from "@app/hooks/api/admin/types"; import { OrganizationWithProjects } from "@app/hooks/api/admin/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { AddOrganizationModal } from "@app/pages/admin/ResourceOverviewPage/components/AddOrganizationModal"; import { AddOrganizationModal } from "@app/pages/admin/ResourceOverviewPage/components/AddOrganizationModal";
@@ -120,7 +122,7 @@ const ViewMembersModalContent = ({
switch (orderBy) { switch (orderBy) {
case MembersOrderBy.Email: case MembersOrderBy.Email:
valueOne = memberOne.user.email || memberOne.user.username; valueOne = memberOne.user.email || memberOne.user.username;
valueTwo = memberTwo.user.email || memberOne.user.username; valueTwo = memberTwo.user.email || memberTwo.user.username;
break; break;
case MembersOrderBy.Name: case MembersOrderBy.Name:
default: default:
@@ -255,23 +257,26 @@ const ViewMembersModalContent = ({
<Td className="max-w-0"> <Td className="max-w-0">
<div className="flex items-center"> <div className="flex items-center">
<p className="truncate">{username || email}</p> <p className="truncate">{username || email}</p>
{role === OrgMembershipRole.Admin && status !== "accepted" && ( {role === OrgMembershipRole.Admin &&
<Button status !== OrgMembershipStatus.Accepted && (
isDisabled={resendOrgInvite.isPending} <Button
className="ml-2 h-8 border-mineshaft-600 bg-mineshaft-800/50 font-normal" isDisabled={resendOrgInvite.isPending}
colorSchema="primary" className="ml-2 h-7 border-mineshaft-600 bg-mineshaft-800/50 font-normal"
variant="outline_bg" colorSchema="primary"
isLoading={ variant="outline_bg"
resendOrgInvite.isPending && resendInviteId === member.membershipId size="xs"
} isLoading={
onClick={(e) => { resendOrgInvite.isPending && resendInviteId === member.membershipId
onResendInvite(member.membershipId); }
e.stopPropagation(); leftIcon={<FontAwesomeIcon icon={faEnvelope} />}
}} onClick={(e) => {
> onResendInvite(member.membershipId);
Resend Invite e.stopPropagation();
</Button> }}
)} >
Resend Invite
</Button>
)}
</div> </div>
</Td> </Td>
<Td> <Td>
@@ -485,9 +490,9 @@ const OrganizationsPanelTable = ({
{!org.members.some( {!org.members.some(
(member) => (member) =>
member.role === OrgMembershipRole.Admin && member.role === OrgMembershipRole.Admin &&
member.status === "accepted" member.status === OrgMembershipStatus.Accepted
) && ( ) && (
<Tooltip content="No admins have accepted their invites."> <Tooltip content="No admins have accepted their invitations.">
<div className="ml-1"> <div className="ml-1">
<Badge> <Badge>
<FontAwesomeIcon icon={faWarning} /> <FontAwesomeIcon icon={faWarning} />
@@ -521,14 +521,14 @@ export const UserIdentitiesTable = () => {
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.removeUsers.isOpen} isOpen={popUp.removeUsers.isOpen}
title="Are you sure you want to remove the following users?" title="Are you sure you want to delete the following users?"
onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)} onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => handleRemoveUsers()} onDeleteApproved={() => handleRemoveUsers()}
buttonText="Remove" buttonText="Delete"
> >
<div className="mt-4 text-sm text-mineshaft-400"> <div className="mt-4 text-sm text-mineshaft-400">
The following members will be removed: The following users will be deleted:
</div> </div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200"> <div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc"> <ul className="list-disc">
@@ -555,7 +555,7 @@ export const UserIdentitiesTable = () => {
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap" className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
> >
<FontAwesomeIcon icon={faWarning} /> <FontAwesomeIcon icon={faWarning} />
<span>Removing Yourself</span> <span>Deleting Yourself</span>
</Badge> </Badge>
</div> </div>
</Tooltip> </Tooltip>