Merge remote-tracking branch 'origin' into aws-iam-auth

This commit is contained in:
Tuan Dang
2024-05-06 22:58:47 -07:00
128 changed files with 3062 additions and 670 deletions
+1 -3
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { isLoggedIn } from "@app/reactQuery";
import { InitialStep, LDAPStep, MFAStep, SAMLSSOStep } from "./components";
import { InitialStep, MFAStep, SAMLSSOStep } from "./components";
import { navigateUserToSelectOrg } from "./Login.utils";
export const Login = () => {
@@ -58,8 +58,6 @@ export const Login = () => {
);
case 2:
return <SAMLSSOStep setStep={setStep} />;
case 3:
return <LDAPStep setStep={setStep} />;
default:
return <div />;
}
@@ -1,24 +1,23 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications";
import { Button, Input } from "@app/components/v2";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
type Props = {
setStep: (step: number) => void;
};
export const LoginLDAP = () => {
const router = useRouter();
const queryParams = new URLSearchParams(window.location.search);
const passedOrgSlug = queryParams.get("organizationSlug");
const passedUsername = queryParams.get("username");
export const LDAPStep = ({ setStep }: Props) => {
const [organizationSlug, setOrganizationSlug] = useState("");
const [username, setUsername] = useState("");
const [organizationSlug, setOrganizationSlug] = useState(passedOrgSlug || "");
const [username, setUsername] = useState(passedUsername || "");
const [password, setPassword] = useState("");
const { t } = useTranslation();
// const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = async (e: React.FormEvent) => {
e.preventDefault();
try {
@@ -42,7 +41,6 @@ export const LDAPStep = ({ setStep }: Props) => {
type: "success"
});
// redirects either to /login/sso or /signup/sso
window.open(nextUrl);
window.close();
} catch (err) {
@@ -76,6 +74,7 @@ export const LDAPStep = ({ setStep }: Props) => {
autoComplete="email"
id="email"
className="h-12"
isDisabled={passedOrgSlug !== null}
/>
</div>
</div>
@@ -90,6 +89,7 @@ export const LDAPStep = ({ setStep }: Props) => {
autoComplete="email"
id="email"
className="h-12"
isDisabled={passedUsername !== null}
/>
</div>
</div>
@@ -122,7 +122,7 @@ export const LDAPStep = ({ setStep }: Props) => {
<div className="mt-4 flex flex-row items-center justify-center">
<button
onClick={() => {
setStep(0);
router.push("/login");
}}
type="button"
className="mt-2 cursor-pointer text-sm text-bunker-300 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"
@@ -25,7 +25,7 @@ type Props = {
export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
const router = useRouter();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
@@ -33,7 +33,10 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const queryParams = new URLSearchParams(window.location.search);
useEffect(() => {
if (process.env.NEXT_PUBLIC_SAML_ORG_SLUG && process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default") {
if (
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
) {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
@@ -42,7 +45,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
);
window.close();
}
}, [])
}, []);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@@ -196,7 +199,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
router.push("/login/ldap");
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
@@ -1 +0,0 @@
export { LDAPStep } from "./LDAPStep";
@@ -1,5 +1,4 @@
export { InitialStep } from "./InitialStep";
export { LDAPStep } from "./LDAPStep";
export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep";
+1
View File
@@ -1,2 +1,3 @@
export { Login } from "./Login";
export { LoginLDAP } from "./LoginLDAP";
export { LoginSSO } from "./LoginSSO";
@@ -49,7 +49,6 @@ type Props = {
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { user } = useUser();
@@ -218,7 +217,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
variant="outline_bg"
onClick={() => onResendInvite(email)}
>
Resend Invite
Resend invite
</Button>
)}
</>
@@ -142,7 +142,7 @@ const SpecificPrivilegeSecretForm = ({
.filter(({ allowed }) => allowed)
.map(({ action }) => ({
action,
subject: [ProjectPermissionSub.Secrets],
subject: ProjectPermissionSub.Secrets,
conditions
}))
},
@@ -477,7 +477,7 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
permissions: [
{
action: ProjectPermissionActions.Read,
subject: [ProjectPermissionSub.Secrets],
subject: ProjectPermissionSub.Secrets,
conditions: {
environment: currentWorkspace?.environments?.[0].slug
}
@@ -512,6 +512,7 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
?.filter(({ permissions }) =>
permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets)
)
.sort((a, b) => a.id.localeCompare(b.id))
?.map((privilege) => (
<SpecificPrivilegeSecretForm
privilege={privilege as TProjectUserPrivilege}
@@ -495,6 +495,7 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => {
?.filter(({ permissions }) =>
permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets)
)
.sort((a, b) => a.id.localeCompare(b.id))
?.map((privilege) => (
<SpecificPrivilegeSecretForm
privilege={privilege as TProjectUserPrivilege}
@@ -1,3 +1,4 @@
import { useState } from "react";
import { subject } from "@casl/ability";
import {
faAngleDown,
@@ -45,6 +46,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from
import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { DecryptedSecret, TImportedSecrets, WsTag } from "@app/hooks/api/types";
import { debounce } from "@app/lib/fn/debounce";
import {
PopUpNames,
@@ -106,6 +108,7 @@ export const ActionBar = ({
] as const);
const { subscription } = useSubscription();
const { openPopUp } = usePopUpAction();
const [search, setSearch] = useState(filter.searchFilter);
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
@@ -114,6 +117,8 @@ export const ActionBar = ({
const { reset: resetSelectedSecret } = useSelectedSecretActions();
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
const debouncedOnSearch = debounce(onSearchChange, 500);
const handleFolderCreate = async (folderName: string) => {
try {
await createFolder({
@@ -199,8 +204,11 @@ export const ActionBar = ({
className="bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by folder name, key name, comment..."
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
value={filter.searchFilter}
onChange={(evt) => onSearchChange(evt.target.value)}
value={search}
onChange={(evt) => {
setSearch(evt.target.value);
debouncedOnSearch(evt.target.value);
}}
/>
</div>
<div>
@@ -14,6 +14,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
SecretInput,
Spinner,
TextArea,
Tooltip
@@ -48,7 +49,6 @@ import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -263,12 +263,10 @@ export const SecretItem = memo(
key="value-overriden"
control={control}
render={({ field }) => (
<InfisicalSecretInput
<SecretInput
key="value-overriden"
isVisible={isVisible}
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>
@@ -280,12 +278,10 @@ export const SecretItem = memo(
key="secret-value"
control={control}
render={({ field }) => (
<InfisicalSecretInput
<SecretInput
isReadOnly={isReadOnly}
key="secret-value"
isVisible={isVisible}
environment={environment}
secretPath={secretPath}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>
@@ -0,0 +1,206 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateAuditLogStream,
useGetAuditLogStreamDetails,
useUpdateAuditLogStream
} from "@app/hooks/api";
type Props = {
id?: string;
onClose: () => void;
};
const formSchema = z.object({
url: z.string().url().min(1),
headers: z
.object({
key: z.string(),
value: z.string()
})
.array()
.optional()
});
type TForm = z.infer<typeof formSchema>;
export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
const isEdit = Boolean(id);
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const auditLogStream = useGetAuditLogStreamDetails(id);
const createAuditLogStream = useCreateAuditLogStream();
const updateAuditLogStream = useUpdateAuditLogStream();
const {
handleSubmit,
control,
setValue,
getValues,
formState: { isSubmitting }
} = useForm<TForm>({
values: auditLogStream?.data,
defaultValues: {
headers: [{ key: "", value: "" }]
}
});
const headerFields = useFieldArray({
control,
name: "headers"
});
const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => {
if (!id) return;
try {
await updateAuditLogStream.mutateAsync({
id,
orgId,
headers,
url
});
createNotification({
type: "success",
text: "Successfully updated stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update stream"
});
}
};
const handleFormSubmit = async ({ headers = [], url }: TForm) => {
if (isSubmitting) return;
const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value));
const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined;
if (isEdit) {
await handleAuditLogStreamEdit({ headers: streamHeaders, url });
return;
}
try {
await createAuditLogStream.mutateAsync({
orgId,
headers: streamHeaders,
url
});
createNotification({
type: "success",
text: "Successfully created stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to create stream"
});
}
};
if (isEdit && auditLogStream.isLoading) {
return (
<div className="flex items-center justify-center p-8">
<Spinner size="lg" />
</div>
);
}
return (
<form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
<div>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Endpoint URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<FormLabel label="Headers" isOptional />
{headerFields.fields.map(({ id: headerFieldId }, i) => (
<div key={headerFieldId} className="flex space-x-2">
<Controller
control={control}
name={`headers.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-1/3"
>
<Input {...field} placeholder="Authorization" />
</FormControl>
)}
/>
<Controller
control={control}
name={`headers.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="flex-grow"
>
<Input
{...field}
type="password"
placeholder="Bearer <token>"
autoComplete="new-password"
/>
</FormControl>
)}
/>
<IconButton
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
const header = getValues("headers");
if (header && header?.length > 1) {
headerFields.remove(i);
} else {
setValue("headers", [{ key: "", value: "" }]);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => headerFields.append({ value: "", key: "" })}
>
Add Key
</Button>
</div>
</div>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isLoading={isSubmitting}>
{isEdit ? "Save" : "Create"}
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,197 @@
import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api";
import { AuditLogStreamForm } from "./AuditLogStreamForm";
export const AuditLogStreamsTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"auditLogStreamForm",
"deleteAuditLogStream",
"upgradePlan"
] as const);
const { subscription } = useSubscription();
const { data: auditLogStreams, isLoading: isAuditLogStreamsLoading } =
useGetAuditLogStreams(orgId);
// mutation
const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream();
const handleAuditLogStreamDelete = async () => {
try {
const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string;
await deleteAuditLogStream({
id: auditLogStreamId,
orgId
});
handlePopUpClose("deleteAuditLogStream");
createNotification({
type: "success",
text: "Successfully deleted stream"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete stream"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Audit Log Streams</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.auditLogStreams) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("auditLogStreamForm");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Create
</Button>
)}
</OrgPermissionCan>
</div>
<p className="mb-8 text-gray-400">
Send audit logs from Infisical to external logging providers via HTTP
</p>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>URL</Td>
<Td className="text-right">Action</Td>
</Tr>
</THead>
<TBody>
{isAuditLogStreamsLoading && (
<TableSkeleton columns={2} innerKey="stream-loading" />
)}
{!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No audit log streams found" icon={faPlug} />
</Td>
</Tr>
)}
{!isAuditLogStreamsLoading &&
auditLogStreams?.map(({ id, url }) => (
<Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url}
</Td>
<Td>
<div className="flex items-center justify-end space-x-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("auditLogStreamForm", id)}
>
Edit
</Button>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
colorSchema="danger"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("deleteAuditLogStream", id)}
>
Delete
</Button>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</div>
<Modal
isOpen={popUp.auditLogStreamForm.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle("auditLogStreamForm", isModalOpen);
}}
>
<ModalContent
title={`${popUp?.auditLogStreamForm?.data ? "Update" : "Create"} Audit Log Stream `}
subTitle="Continuously stream logs from Infisical to third-party logging providers."
>
<AuditLogStreamForm
id={popUp?.auditLogStreamForm?.data as string}
onClose={() => handlePopUpToggle("auditLogStreamForm")}
/>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
/>
<DeleteActionModal
isOpen={popUp.deleteAuditLogStream.isOpen}
deleteKey="delete"
title="Are you sure you want to remove this stream?"
onChange={(isOpen) => handlePopUpToggle("deleteAuditLogStream", isOpen)}
onClose={() => handlePopUpClose("deleteAuditLogStream")}
onDeleteApproved={handleAuditLogStreamDelete}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Settings }
);
@@ -0,0 +1 @@
export { AuditLogStreamsTab } from "./AuditLogStreamTab";
@@ -1,12 +1,14 @@
import { Fragment } from "react";
import { Tab } from "@headlessui/react";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Security", key: "tab-org-security" }
{ name: "Security", key: "tab-org-security" },
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
];
export const OrgTabGroup = () => {
return (
@@ -17,9 +19,8 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -34,6 +35,9 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
<Tab.Panel>
<AuditLogStreamsTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
@@ -7,7 +7,7 @@ import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Webhooks", key: "tab-project-webhooks" }
{ name: "Webhooks", key: "tab-project-webhooks" },
];
export const ProjectSettingsPage = () => {
@@ -25,9 +25,8 @@ export const ProjectSettingsPage = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
+31 -4
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import jwt_decode from "jwt-decode";
import { BackupPDFStep, UserInfoSSOStep } from "./components";
import { BackupPDFStep, EmailConfirmationStep, UserInfoSSOStep } from "./components";
type Props = {
providerAuthToken: string;
@@ -11,11 +11,38 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const { username, organizationName, firstName, lastName } = jwt_decode(providerAuthToken) as any;
const {
username,
email,
organizationName,
organizationSlug,
firstName,
lastName,
authType,
isEmailVerified
} = jwt_decode(providerAuthToken) as any;
useEffect(() => {
if (!isEmailVerified) {
setStep(0);
} else {
setStep(1);
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return (
<EmailConfirmationStep
authType={authType}
username={username}
email={email}
organizationSlug={organizationSlug}
setStep={setStep}
/>
);
case 1:
return (
<UserInfoSSOStep
username={username}
@@ -27,7 +54,7 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
providerAuthToken={providerAuthToken}
/>
);
case 1:
case 2:
return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
);
@@ -0,0 +1,185 @@
// confirm email
// if same email exists, then trigger fn to merge automatically
import { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useRouter } from "next/router";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "@app/hooks/api";
import { UserAliasType } from "@app/hooks/api/users/types";
type Props = {
authType?: UserAliasType;
username: string;
email: string;
organizationSlug: string;
setStep: (step: number) => void;
};
// The style for the verification code input
const props = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "55px",
borderRadius: "5px",
fontSize: "24px",
height: "55px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
const propsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
export const EmailConfirmationStep = ({
authType,
username,
email,
organizationSlug,
setStep
}: Props) => {
const router = useRouter();
const [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false);
const [isResendingVerificationEmail] = useState(false);
const [isLoading] = useState(false);
const { mutateAsync: sendEmailVerificationCode } = useSendEmailVerificationCode();
const { mutateAsync: verifyEmailVerificationCode } = useVerifyEmailVerificationCode();
const checkCode = async () => {
try {
await verifyEmailVerificationCode({ username, code });
setCodeError(false);
createNotification({
text: "Successfully verified code",
type: "success"
});
switch (authType) {
case UserAliasType.SAML: {
window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`);
window.close();
break;
}
case UserAliasType.LDAP: {
router.push(`/login/ldap?organizationSlug=${organizationSlug}`);
break;
}
default: {
setStep(1);
break;
}
}
} catch (err) {
createNotification({
text: "Failed to verify code",
type: "error"
});
}
setCode("");
};
const resendCode = async () => {
try {
await sendEmailVerificationCode(username);
createNotification({
text: "Successfully resent code",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed to resend code",
type: "error"
});
}
};
return (
<div className="mx-auto h-full w-full pb-4 md:px-8">
<p className="text-md flex justify-center text-bunker-200">
We&apos;ve sent a verification code to {email}
</p>
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...props}
className="mt-6 mb-2"
/>
</div>
<div className="mx-auto mt-4 block w-max md:hidden">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...propsPhone}
className="mt-2 mb-2"
/>
</div>
{codeError && <Error text="Oops. Your code is wrong. Please try again." />}
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
<div className="text-l w-full py-1 text-lg">
<Button
type="submit"
onClick={checkCode}
size="sm"
isFullWidth
className="h-14"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
>
{" "}
Verify
</Button>
</div>
</div>
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">Don&apos;t see the code?</span>
<div className="text-md mt-2 flex flex-row text-bunker-400">
<button disabled={isLoading} onClick={resendCode} type="button">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{isResendingVerificationEmail ? "Resending..." : "Resend"}
</span>
</button>
</div>
</div>
<p className="pb-2 text-sm text-bunker-400">Make sure to check your spam inbox.</p>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { EmailConfirmationStep } from "./EmailConfirmationStep";
@@ -201,7 +201,7 @@ export const UserInfoSSOStep = ({
localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project.id);
setStep(1);
setStep(2);
} catch (error) {
setIsLoading(false);
console.error(error);
@@ -1,2 +1,3 @@
export { BackupPDFStep } from "./BackupPDFStep";
export { EmailConfirmationStep } from "./EmailConfirmationStep";
export { UserInfoSSOStep } from "./UserInfoSSOStep";
@@ -14,11 +14,11 @@ import {
Input,
Select,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
Tabs} from "@app/components/v2";
import { useOrganization, useServerConfig, useUser } from "@app/context";
import { useUpdateServerConfig } from "@app/hooks/api";
@@ -33,7 +33,9 @@ enum SignUpModes {
const formSchema = z.object({
signUpMode: z.nativeEnum(SignUpModes),
allowedSignUpDomain: z.string().optional().nullable()
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean(),
trustLdapEmails: z.boolean()
});
type TDashboardForm = z.infer<typeof formSchema>;
@@ -52,7 +54,9 @@ export const AdminDashboardPage = () => {
values: {
// eslint-disable-next-line
signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled,
allowedSignUpDomain: config.allowedSignUpDomain
allowedSignUpDomain: config.allowedSignUpDomain,
trustSamlEmails: config.trustSamlEmails,
trustLdapEmails: config.trustLdapEmails
}
});
@@ -62,8 +66,6 @@ export const AdminDashboardPage = () => {
const { orgs } = useOrganization();
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
const isNotAllowed = !user?.superAdmin;
// TODO(akhilmhdh): on nextjs 14 roadmap this will be properly addressed with context split
@@ -78,10 +80,13 @@ export const AdminDashboardPage = () => {
const onFormSubmit = async (formData: TDashboardForm) => {
try {
const { signUpMode, allowedSignUpDomain } = formData;
const { signUpMode, allowedSignUpDomain, trustSamlEmails, trustLdapEmails } = formData;
await updateServerConfig({
allowSignUp: signUpMode !== SignUpModes.Disabled,
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
trustLdapEmails
});
createNotification({
text: "Successfully changed sign up setting.",
@@ -123,8 +128,9 @@ export const AdminDashboardPage = () => {
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
Allow user signups
</div>
<div className="mb-4 text-sm max-w-sm text-mineshaft-400">
Select if you want users to be able to signup freely into your Infisical instance.
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want users to be able to signup freely into your Infisical
instance.
</div>
<Controller
control={control}
@@ -176,6 +182,48 @@ export const AdminDashboardPage = () => {
/>
</div>
)}
<div className="mt-8 mb-8 flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Trust emails</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want Infisical to trust external emails from SAML/LDAP identity
providers. If set to false, then Infisical will prompt SAML/LDAP provisioned
users to verify their email upon their first login.
</div>
<Controller
control={control}
name="trustSamlEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-saml-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust SAML emails</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="trustLdapEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-ldap-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust LDAP emails</p>
</Switch>
</FormControl>
);
}}
/>
</div>
<Button
type="submit"
isLoading={isSubmitting}