mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
wip
This commit is contained in:
@@ -1064,6 +1064,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
type: z.nativeEnum(ProjectType).optional(),
|
||||
orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME),
|
||||
orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC),
|
||||
projectIds: z.string().trim().array().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
@@ -399,6 +399,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
name?: string;
|
||||
sortBy?: SearchProjectSortBy;
|
||||
sortDir?: SortDirection;
|
||||
projectIds?: string[];
|
||||
}) => {
|
||||
const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto;
|
||||
|
||||
@@ -454,6 +455,11 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
if (dto.name) {
|
||||
void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`);
|
||||
}
|
||||
|
||||
if (dto.projectIds?.length) {
|
||||
void query.whereIn(`${TableName.Project}.id`, dto.projectIds);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
||||
|
||||
@@ -1806,7 +1806,8 @@ export const projectServiceFactory = ({
|
||||
limit,
|
||||
type,
|
||||
orderBy,
|
||||
orderDirection
|
||||
orderDirection,
|
||||
projectIds
|
||||
}: TSearchProjectsDTO) => {
|
||||
// check user belong to org
|
||||
await permissionService.getOrgPermission(
|
||||
@@ -1822,6 +1823,7 @@ export const projectServiceFactory = ({
|
||||
offset,
|
||||
name,
|
||||
type,
|
||||
projectIds,
|
||||
orgId: permission.orgId,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
|
||||
@@ -221,6 +221,7 @@ export type TSearchProjectsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SearchProjectSortBy;
|
||||
projectIds?: string[];
|
||||
orderDirection?: SortDirection;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
import { useRequestProjectAccess } from "@app/hooks/api";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type ContentProps = {
|
||||
projectId: string;
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const Content = ({ projectId, onComplete }: ContentProps) => {
|
||||
const form = useForm<{ note: string }>();
|
||||
|
||||
const requestProjectAccess = useRequestProjectAccess();
|
||||
|
||||
const onFormSubmit = ({ note }: { note: string }) => {
|
||||
if (requestProjectAccess.isPending) return;
|
||||
requestProjectAccess.mutate(
|
||||
{
|
||||
comment: note,
|
||||
projectId
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
title: "Project Access Request Sent",
|
||||
text: "Project admins will receive an email of your request"
|
||||
});
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<FormControl label="Note">
|
||||
<Input {...form.register("note")} />
|
||||
</FormControl>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button className="mr-4" size="sm" type="submit" isLoading={form.formState.isSubmitting}>
|
||||
Submit Request
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
type RequestProjectAccessModalProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
project?: Workspace;
|
||||
onComplete?: () => void;
|
||||
};
|
||||
|
||||
export const RequestProjectAccessModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
project,
|
||||
onComplete
|
||||
}: RequestProjectAccessModalProps) => {
|
||||
if (!project) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Confirm Access Request"
|
||||
subTitle={`Requesting access to project ${project?.name}. You may include an optional note for project admins to review your request.`}
|
||||
>
|
||||
<Content
|
||||
onComplete={() => {
|
||||
onOpenChange(false);
|
||||
if (onComplete) onComplete();
|
||||
}}
|
||||
projectId={project?.id}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { NewProjectModal } from "./NewProjectModal";
|
||||
export * from "./RequestProjectAccessModal";
|
||||
|
||||
@@ -186,6 +186,7 @@ export type TSearchProjectsDTO = {
|
||||
name?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
projectIds?: string[];
|
||||
type?: ProjectType;
|
||||
options?: { enabled?: boolean };
|
||||
orderBy?: ProjectIdentityOrderBy;
|
||||
|
||||
@@ -38,11 +38,7 @@ import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { getUserTablePreference, PreferenceKey, setUserTablePreference } from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
@@ -311,38 +307,46 @@ export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuContent
|
||||
sideOffset={2}
|
||||
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel>Filter by Apps</DropdownMenuLabel>
|
||||
{appConnections.length ? (
|
||||
[...new Set(appConnections.map(({ app }) => app))].map((app) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
apps: prev.apps.includes(app)
|
||||
? prev.apps.filter((a) => a !== app)
|
||||
: [...prev.apps, app]
|
||||
}));
|
||||
}}
|
||||
key={app}
|
||||
icon={
|
||||
filters.apps.includes(app) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${APP_CONNECTION_MAP[app].name} integration`}
|
||||
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span>{APP_CONNECTION_MAP[app].name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
[...new Set(appConnections.map(({ app }) => app))]
|
||||
.sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
})
|
||||
.map((app) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
apps: prev.apps.includes(app)
|
||||
? prev.apps.filter((a) => a !== app)
|
||||
: [...prev.apps, app]
|
||||
}));
|
||||
}}
|
||||
key={app}
|
||||
icon={
|
||||
filters.apps.includes(app) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${APP_CONNECTION_MAP[app].name} integration`}
|
||||
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span>{APP_CONNECTION_MAP[app].name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem isDisabled>No Connections Configured</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
faArrowDownAZ,
|
||||
faBorderAll,
|
||||
@@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { RequestProjectAccessModal } from "@app/components/projects/RequestProjectAccessModal";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -24,12 +24,9 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Lottie,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Skeleton,
|
||||
Tooltip
|
||||
@@ -43,11 +40,7 @@ import {
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useOrgAdminAccessProject,
|
||||
useRequestProjectAccess,
|
||||
useSearchProjects
|
||||
} from "@app/hooks/api";
|
||||
import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api";
|
||||
import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||
import {
|
||||
ProjectListToggle,
|
||||
@@ -62,53 +55,6 @@ type Props = {
|
||||
onProjectListViewChange: (value: ProjectListView) => void;
|
||||
};
|
||||
|
||||
type RequestAccessModalProps = {
|
||||
projectId: string;
|
||||
onPopUpToggle: () => void;
|
||||
};
|
||||
|
||||
const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProps) => {
|
||||
const form = useForm<{ note: string }>();
|
||||
|
||||
const requestProjectAccess = useRequestProjectAccess();
|
||||
|
||||
const onFormSubmit = ({ note }: { note: string }) => {
|
||||
if (requestProjectAccess.isPending) return;
|
||||
requestProjectAccess.mutate(
|
||||
{
|
||||
comment: note,
|
||||
projectId
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
title: "Project Access Request Sent",
|
||||
text: "Project admins will receive an email of your request"
|
||||
});
|
||||
onPopUpToggle();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<FormControl label="Note">
|
||||
<Input {...form.register("note")} />
|
||||
</FormControl>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button className="mr-4" size="sm" type="submit" isLoading={form.formState.isSubmitting}>
|
||||
Submit Request
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={() => onPopUpToggle()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const AllProjectView = ({
|
||||
onAddNewProject,
|
||||
onUpgradePlan,
|
||||
@@ -419,20 +365,11 @@ export const AllProjectView = ({
|
||||
<div className="text-center font-light">No Projects Found</div>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
<RequestProjectAccessModal
|
||||
isOpen={popUp.requestAccessConfirmation.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("requestAccessConfirmation", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Confirm Access Request"
|
||||
subTitle={`Requesting access to project ${requestedWorkspaceDetails?.name}. You may include an optional note for project admins to review your request.`}
|
||||
>
|
||||
<RequestAccessModal
|
||||
onPopUpToggle={() => handlePopUpToggle("requestAccessConfirmation")}
|
||||
projectId={requestedWorkspaceDetails?.id}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
project={requestedWorkspaceDetails}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,17 @@ import { AxiosError } from "axios";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
|
||||
import { ProjectAccessError } from "./components";
|
||||
|
||||
export const ErrorPage = ({ error }: ErrorComponentProps) => {
|
||||
if (
|
||||
error instanceof AxiosError &&
|
||||
error.status === 403 &&
|
||||
error.response?.data?.error === "User not a part of the specified project"
|
||||
) {
|
||||
return <ProjectAccessError />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-mineshaft-900">
|
||||
<div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200">
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { RequestProjectAccessModal } from "@app/components/projects";
|
||||
import { AccessRestrictedBanner, Button } from "@app/components/v2";
|
||||
import { OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionAdminConsoleAction } from "@app/context/OrgPermissionContext/types";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api";
|
||||
|
||||
export const ProjectAccessError = () => {
|
||||
const orgAdminAccessProject = useOrgAdminAccessProject();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
|
||||
"requestAccessConfirmation"
|
||||
] as const);
|
||||
|
||||
const { projectId } = useParams({
|
||||
strict: false
|
||||
});
|
||||
|
||||
const { data, isPending: isProjectLoading } = useSearchProjects({
|
||||
projectIds: projectId ? [projectId] : [],
|
||||
options: {
|
||||
enabled: Boolean(projectId)
|
||||
}
|
||||
});
|
||||
|
||||
const [project] = data?.projects ?? [];
|
||||
|
||||
const handleAccessProject = async () => {
|
||||
if (!project) return;
|
||||
try {
|
||||
await orgAdminAccessProject.mutateAsync({
|
||||
projectId: project.id
|
||||
});
|
||||
await navigate({
|
||||
to: getProjectHomePage(project.type, project.environments),
|
||||
params: {
|
||||
projectId: project.id
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to access project",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<AccessRestrictedBanner
|
||||
body={
|
||||
<>
|
||||
You are not currently a member of this project. Request access to join project.
|
||||
<div className="mt-4 flex w-full justify-center gap-2">
|
||||
<Link to="/organization/projects">
|
||||
<Button variant="outline_bg">
|
||||
<FontAwesomeIcon icon={faHome} className="mr-2" />
|
||||
Back To Home
|
||||
</Button>
|
||||
</Link>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAdminConsoleAction.AccessAllProjects}
|
||||
an={OrgPermissionSubjects.AdminConsole}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleAccessProject();
|
||||
}}
|
||||
disabled={orgAdminAccessProject.isPending}
|
||||
isLoading={isProjectLoading || orgAdminAccessProject.isPending}
|
||||
>
|
||||
Join Project as Admin
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("requestAccessConfirmation")}
|
||||
isLoading={isProjectLoading}
|
||||
>
|
||||
Request Access to Project
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<RequestProjectAccessModal
|
||||
isOpen={popUp.requestAccessConfirmation.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("requestAccessConfirmation", isOpen)}
|
||||
project={project}
|
||||
onComplete={() => {
|
||||
navigate({
|
||||
to: "/organization/projects"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
frontend/src/pages/public/ErrorPage/components/index.ts
Normal file
1
frontend/src/pages/public/ErrorPage/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./ProjectAccessError";
|
||||
Reference in New Issue
Block a user