mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 13:28:11 +00:00
Add ssh host endpoint for issuing ssh host cert
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { SshCaSection } from "./components";
|
||||
|
||||
export const SshCasPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="SSH Certificate Authorities"
|
||||
description="Manage the SSH certificate authorities used to sign user and host certificates, including custom and default CAs."
|
||||
/>
|
||||
<SshCaSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
|
||||
import {
|
||||
SshCaKeySource,
|
||||
SshCertKeyAlgorithm,
|
||||
sshCertKeyAlgorithms
|
||||
} from "@app/hooks/api/sshCa/constants";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["sshCa"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const sshCaKeySources = [
|
||||
{ label: "Internal", value: SshCaKeySource.INTERNAL },
|
||||
{ label: "External", value: SshCaKeySource.EXTERNAL }
|
||||
];
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
friendlyName: z.string(),
|
||||
keySource: z.nativeEnum(SshCaKeySource),
|
||||
publicKey: z.string().optional(),
|
||||
privateKey: z.string().optional(),
|
||||
keyAlgorithm: z.enum([
|
||||
SshCertKeyAlgorithm.RSA_2048,
|
||||
SshCertKeyAlgorithm.RSA_4096,
|
||||
SshCertKeyAlgorithm.ECDSA_P256,
|
||||
SshCertKeyAlgorithm.ECDSA_P384,
|
||||
SshCertKeyAlgorithm.ED25519
|
||||
])
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || "");
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateSshCa();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateSshCa();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
friendlyName: "",
|
||||
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
|
||||
keySource: SshCaKeySource.INTERNAL,
|
||||
publicKey: "",
|
||||
privateKey: ""
|
||||
}
|
||||
});
|
||||
|
||||
const caKeySource = watch("keySource");
|
||||
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
reset({
|
||||
friendlyName: ca.friendlyName,
|
||||
keyAlgorithm: ca.keyAlgorithm,
|
||||
keySource: ca.keySource,
|
||||
publicKey: ca.publicKey
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
friendlyName: "",
|
||||
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
|
||||
keySource: SshCaKeySource.INTERNAL,
|
||||
publicKey: "",
|
||||
privateKey: ""
|
||||
});
|
||||
}
|
||||
}, [ca]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
friendlyName,
|
||||
keySource,
|
||||
keyAlgorithm,
|
||||
publicKey,
|
||||
privateKey
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
if (ca) {
|
||||
await updateMutateAsync({
|
||||
caId: ca.id,
|
||||
friendlyName
|
||||
});
|
||||
} else {
|
||||
const { id: newCaId } = await createMutateAsync({
|
||||
projectId,
|
||||
friendlyName,
|
||||
keySource,
|
||||
keyAlgorithm,
|
||||
publicKey,
|
||||
privateKey
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
|
||||
params: {
|
||||
projectId,
|
||||
caId: newCaId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("sshCa", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${ca ? "updated" : "created"} SSH CA`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create SSH CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.sshCa?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
reset();
|
||||
handlePopUpToggle("sshCa", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${ca ? "View" : "Create"} SSH CA`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{ca && (
|
||||
<FormControl label="CA ID">
|
||||
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="My SSH CA" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caKeySource && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="keySource"
|
||||
defaultValue={SshCaKeySource.INTERNAL}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Key Source"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{sshCaKeySources.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{caKeySource === SshCaKeySource.INTERNAL && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
defaultValue={SshCertKeyAlgorithm.ED25519}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Key Algorithm"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired={caKeySource === SshCaKeySource.INTERNAL}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{sshCertKeyAlgorithms.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{caKeySource === SshCaKeySource.EXTERNAL && !ca && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="publicKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Public Key"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired={caKeySource === SshCaKeySource.EXTERNAL}
|
||||
>
|
||||
<Input {...field} placeholder="ssh-rsa AAA..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="privateKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Private Key"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired={caKeySource === SshCaKeySource.EXTERNAL}
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.sshCa?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("sshCa", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { SshCaStatus, useDeleteSshCa, useUpdateSshCa } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SshCaModal } from "./SshCaModal";
|
||||
import { SshCaTable } from "./SshCaTable";
|
||||
|
||||
export const SshCaSection = () => {
|
||||
const { mutateAsync: deleteSshCa } = useDeleteSshCa();
|
||||
const { mutateAsync: updateSshCa } = useUpdateSshCa();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"sshCa",
|
||||
"deleteSshCa",
|
||||
"sshCaStatus", // enable / disable
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onRemoveSshCaSubmit = async (caId: string) => {
|
||||
try {
|
||||
await deleteSshCa({ caId });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted SSH CA",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteSshCa");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete SSH CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateSshCaStatus = async ({ caId, status }: { caId: string; status: SshCaStatus }) => {
|
||||
try {
|
||||
await updateSshCa({ caId, status });
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("sshCaStatus");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("sshCa")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create SSH CA
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<SshCaTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<SshCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSshCa.isOpen}
|
||||
title="Are you sure want to remove the SSH CA?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSshCa", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveSshCaSubmit((popUp?.deleteSshCa?.data as { caId: string })?.caId)
|
||||
}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.sshCaStatus.isOpen}
|
||||
title={`Are you sure want to ${
|
||||
(popUp?.sshCaStatus?.data as { status: string })?.status === SshCaStatus.ACTIVE
|
||||
? "enable"
|
||||
: "disable"
|
||||
} the CA?`}
|
||||
subTitle={
|
||||
(popUp?.sshCaStatus?.data as { status: string })?.status === SshCaStatus.ACTIVE
|
||||
? "This action will allow the SSH CA to start issuing certificates again."
|
||||
: "This action will prevent the SSH CA from issuing new certificates."
|
||||
}
|
||||
onChange={(isOpen) => handlePopUpToggle("sshCaStatus", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onUpdateSshCaStatus(popUp?.sshCaStatus?.data as { caId: string; status: SshCaStatus })
|
||||
}
|
||||
/>
|
||||
{/* <UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { SshCaStatus, useListWorkspaceSshCas } from "@app/hooks/api";
|
||||
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteSshCa", "sshCaStatus"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SshCaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useListWorkspaceSshCas(currentWorkspace?.id || "");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="org-ssh-cas" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((ca) => {
|
||||
return (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`ca-${ca.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
caId: ca.id
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{ca.friendlyName}</Td>
|
||||
<Td>
|
||||
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
|
||||
{caStatusToNameMap[ca.status]}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{(ca.status === SshCaStatus.ACTIVE ||
|
||||
ca.status === SshCaStatus.DISABLED) && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("sshCaStatus", {
|
||||
caId: ca.id,
|
||||
status:
|
||||
ca.status === SshCaStatus.ACTIVE
|
||||
? SshCaStatus.DISABLED
|
||||
: SshCaStatus.ACTIVE
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
{`${
|
||||
ca.status === SshCaStatus.ACTIVE ? "Disable" : "Enable"
|
||||
} SSH CA`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteSshCa", {
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete SSH CA
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data?.length === 0 && (
|
||||
<EmptyState
|
||||
title="No SSH certificate authorities have been created"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SshCaSection } from "./SshCaSection";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { SshCasPage } from "./SshCasPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas"
|
||||
)({
|
||||
component: SshCasPage
|
||||
});
|
||||
Reference in New Issue
Block a user