Fix lint errors, merge conflicts

This commit is contained in:
Tuan Dang
2023-07-28 02:02:26 +07:00
261 changed files with 9623 additions and 3319 deletions
@@ -127,7 +127,7 @@ const AddProjectMemberDialog = ({
</div>
) : (
<Button
onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
onButtonPressed={() => router.push(`/org/${localStorage.getItem("orgData.id")}/members`)}
color="mineshaft"
text={t("section.members.add-dialog.add-user-to-org") as string}
size="md"
@@ -1,188 +0,0 @@
import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios"
import attemptLogin from "@app/components/utilities/attemptLogin";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import Error from "../basic/Error";
import attemptCliLogin from "../utilities/attemptCliLogin";
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Input } from "../v2";
export default function InitialLoginStep({
setStep,
email,
setEmail,
password,
setPassword,
}: {
setStep: (step: number) => void;
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (password: string) => void;
}) {
const router = useRouter();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint
const instance = axios.create()
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
console.log(cliResp)
// cli page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
}
return <form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open('/api/v1/auth/redirect/google')
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t('login.continue-with-google')}
</Button>
</div> */}
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-12"
/>
</div>
</div>
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12 select:-webkit-autofill:focus"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
type="submit"
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="solid"
isLoading={isLoading}
> Login </Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
<div className='w-1/2 border-t border-mineshaft-500' />
<span className='px-4 text-sm text-bunker-400'>or</span>
<div className='w-1/2 border-t border-mineshaft-500' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
isFullWidth
className="h-14 w-full mx-0"
>
Continue with SAML SSO
</Button>
</div>
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span>
</Link>
</div>
<div className="text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link>
</div>
</form>
}
@@ -1,122 +0,0 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import Error from "@app/components/basic/Error";
import attemptLogin from "@app/components/utilities/attemptLogin";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2";
export default function PasswordInputStep({
email,
password,
providerAuthToken,
setPassword,
setProviderAuthToken,
setStep
}: {
email: string;
password: string;
providerAuthToken: string;
setPassword: (password: string) => void;
setProviderAuthToken: (value: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
setIsLoading(true);
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken,
});
if (loginAttempt && loginAttempt.success) {
// case: login was successful
if (loginAttempt.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0]._id;
router.push(`/org/${userOrg?._id}/overview`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
};
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="h-full mx-auto w-full max-w-md px-6 pt-8">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What’s your Infisical Password?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={async () => handleLogin()}
isFullWidth
isLoading={isLoading}
className="h-14"
>
{t("login.login")}
</Button>
</div>
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
<span className='duration-200 max-w-sm text-center px-4'>
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
</span>
<Link href="/verify-email">
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
</Link>
</div>
<div className="flex flex-row items-center justify-center">
<button
onClick={(e) => {
e.preventDefault();
SecurityClient.setProviderAuthToken("");
setProviderAuthToken("");
}}
type="button"
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
</form>
);
}
@@ -1,9 +1,9 @@
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from "../v2";
export default function InitialSignupStep({
@@ -16,23 +16,24 @@ export default function InitialSignupStep({
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >{t("signup.initial-title")}</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open('/api/v1/auth/redirect/google')
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-14 w-full mx-0"
>
{t('signup.continue-with-google')}
{t("signup.continue-with-google")}
</Button>
</div> */}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="solid"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
+4 -2
View File
@@ -4,6 +4,7 @@ export const publicPaths = [
"/signupinvite",
"/pricing",
"/signup",
"/signup/sso",
"/login",
"/blog",
"/docs",
@@ -18,8 +19,9 @@ export const publicPaths = [
"/verify-email",
"/password-reset",
"/saml-sso",
"/login/provider/success",
"/login/provider/error"
"/login/provider/success", // TODO: change
"/login/provider/error", // TODO: change
"/login/sso"
];
export const languageMap = {
+2
View File
@@ -11,8 +11,10 @@ export * from "./secrets";
export * from "./secretSnapshots";
export * from "./serviceAccounts";
export * from "./serviceTokens";
export * from "./ssoConfig";
export * from "./subscriptions";
export * from "./tags";
export * from "./trustedIps";
export * from "./users";
export * from "./webhooks";
export * from "./workspace";
@@ -1,9 +1,10 @@
export {
useDeleteIntegrationAuth,
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches,
} from "./queries";
useGetIntegrationAuthVercelBranches} from "./queries";
@@ -3,13 +3,21 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
import {
App,
BitBucketWorkspace,
Environment,
IntegrationAuth,
NorthflankSecretGroup,
Service,
Team
} from "./types";
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string, workspaceSlug?: string) =>
[{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthTeams"] as const,
getIntegrationAuthVercelBranches: ({
@@ -19,7 +27,6 @@ const integrationAuthKeys = {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
@@ -33,7 +40,16 @@ const integrationAuthKeys = {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
getIntegrationAuthNorthflankSecretGroups: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const,
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -45,12 +61,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
teamId,
workspaceSlug
}: {
integrationAuthId: string;
teamId?: string;
workspaceSlug?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const params: Record<string, string> = {}
if (teamId) {
params.teamId = teamId
}
if (workspaceSlug) {
params.workspaceSlug = workspaceSlug
}
const searchParams = new URLSearchParams(params);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
@@ -129,6 +155,34 @@ const fetchIntegrationAuthRailwayServices = async ({
return services;
};
const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => {
const { data: { workspaces } } = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces`
);
return workspaces;
};
const fetchIntegrationAuthNorthflankSecretGroups = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const {
data: { secretGroups }
} = await apiRequest.get<{ secretGroups: NorthflankSecretGroup[] }>(
`/api/v1/integration-auth/${integrationAuthId}/northflank/secret-groups`,
{
params: {
appId
}
}
);
return secretGroups;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -139,17 +193,20 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId
teamId,
workspaceSlug,
}: {
integrationAuthId: string;
teamId?: string;
workspaceSlug?: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
queryFn: () =>
fetchIntegrationAuthApps({
integrationAuthId,
teamId
teamId,
workspaceSlug
}),
enabled: true
});
@@ -226,6 +283,34 @@ export const useGetIntegrationAuthRailwayServices = ({
});
};
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId),
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId),
enabled: true
});
};
export const useGetIntegrationAuthNorthflankSecretGroups = ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthNorthflankSecretGroups({
integrationAuthId,
appId
}),
queryFn: () =>
fetchIntegrationAuthNorthflankSecretGroups({
integrationAuthId,
appId
}),
enabled: true
});
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();
@@ -13,6 +13,7 @@ export type App = {
name: string;
appId?: string;
owner?: string;
secretGroups?: string[];
};
export type Team = {
@@ -29,3 +30,14 @@ export type Service = {
name: string;
serviceId: string;
};
export type BitBucketWorkspace = {
uuid: string;
name: string;
slug: string;
}
export type NorthflankSecretGroup = {
name: string;
groupId: string;
}
@@ -22,7 +22,7 @@ const organizationKeys = {
getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const,
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
};
export const useGetOrganization = () => {
@@ -0,0 +1,5 @@
export {
useCreateSSOConfig,
useGetSSOConfig,
useUpdateSSOConfig
} from "./queries";
@@ -0,0 +1,103 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
const ssoConfigKeys = {
getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const,
}
export const useGetSSOConfig = (organizationId: string) => {
return useQuery({
queryKey: ssoConfigKeys.getSSOConfig(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get(
`/api/v1/sso/config?organizationId=${organizationId}`
);
return data;
},
enabled: true
});
}
export const useCreateSSOConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
}: {
organizationId: string;
authProvider: string;
isActive: boolean;
entryPoint: string;
issuer: string;
cert: string;
audience: string;
}) => {
const { data } = await apiRequest.post(
"/api/v1/sso/config",
{
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
}
});
};
export const useUpdateSSOConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
authProvider,
isActive,
entryPoint,
issuer,
cert,
audience
}: {
organizationId: string;
authProvider?: string;
isActive?: boolean;
entryPoint?: string;
issuer?: string;
cert?: string;
audience?: string;
}) => {
const { data } = await apiRequest.patch(
"/api/v1/sso/config",
{
organizationId,
...(authProvider !== undefined ? { authProvider } : {}),
...(isActive !== undefined ? { isActive } : {}),
...(entryPoint !== undefined ? { entryPoint } : {}),
...(issuer !== undefined ? { issuer } : {}),
...(cert !== undefined ? { cert } : {}),
...(audience !== undefined ? { audience } : {})
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
}
});
};
@@ -6,6 +6,7 @@ export type SubscriptionPlan = {
customAlerts: boolean;
customRateLimits: boolean;
pitRecovery: boolean;
ipAllowlisting: boolean;
rbac: boolean;
secretVersioning: boolean;
slug: string;
@@ -13,6 +14,7 @@ export type SubscriptionPlan = {
workspaceLimit: number;
workspacesUsed: number;
environmentLimit: number;
samlSSO: boolean;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
has_used_trial: boolean;
@@ -0,0 +1,5 @@
export {
useAddTrustedIp,
useDeleteTrustedIp,
useGetTrustedIps,
useUpdateTrustedIp} from "./queries";
@@ -0,0 +1,108 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TrustedIp
} from "./types";
const trustedIps = {
getTrustedIps: (workspaceId: string) => [{ workspaceId }, "trusted-ips"] as const
}
export const useGetTrustedIps = (workspaceId: string) => {
return useQuery({
queryKey: trustedIps.getTrustedIps(workspaceId),
queryFn: async () => {
const { data } = await apiRequest.get<{ trustedIps: TrustedIp[] }>(`/api/v1/workspace/${workspaceId}/trusted-ips`);
return data.trustedIps;
}
});
}
export const useAddTrustedIp = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
workspaceId,
ipAddress,
comment,
isActive
}: {
workspaceId: string;
ipAddress: string;
comment?: string;
isActive: boolean;
}) => {
const { data } = await apiRequest.post(
`/api/v1/workspace/${workspaceId}/trusted-ips`,
{
ipAddress,
...(comment ? { comment } : {}),
isActive
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(trustedIps.getTrustedIps(dto.workspaceId));
}
});
};
export const useUpdateTrustedIp = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
workspaceId,
trustedIpId,
ipAddress,
comment,
isActive
}: {
workspaceId: string;
trustedIpId: string;
ipAddress: string;
comment?: string;
isActive: boolean;
}) => {
const { data } = await apiRequest.patch(
`/api/v1/workspace/${workspaceId}/trusted-ips/${trustedIpId}`,
{
ipAddress,
...(comment ? { comment } : {}),
isActive
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(trustedIps.getTrustedIps(dto.workspaceId));
}
});
};
export const useDeleteTrustedIp = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
workspaceId,
trustedIpId,
}: {
workspaceId: string;
trustedIpId: string;
}) => {
const { data } = await apiRequest.delete(
`/api/v1/workspace/${workspaceId}/trusted-ips/${trustedIpId}`
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(trustedIps.getTrustedIps(dto.workspaceId));
}
});
};
@@ -0,0 +1,9 @@
export type TrustedIp = {
_id: string;
workspace: string;
ipAddress: string;
type: "ipv4" | "ipv6";
isActive: boolean;
comment: string;
prefix?: number;
};
+4 -1
View File
@@ -6,6 +6,7 @@ export {
useDeleteAPIKey,
useDeleteOrgMembership,
useGetMyAPIKeys,
useGetMyIp,
useGetMySessions,
useGetOrgUsers,
useGetUser,
@@ -13,4 +14,6 @@ export {
useLogoutUser,
useRegisterUserAction,
useRevokeMySessions,
useUpdateOrgUserRole} from "./queries";
useUpdateOrgUserRole,
useUpdateUserAuthProvider
} from "./queries";
+35
View File
@@ -25,6 +25,7 @@ const userKeys = {
getUser: ["user"] as const,
userAction: ["user-action"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
mySessions: ["sessions"] as const
};
@@ -58,6 +59,27 @@ export const useRenameUser = () => {
});
};
export const useUpdateUserAuthProvider = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
authProvider
}: {
authProvider: string;
}) => {
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
authProvider
});
return user;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useGetUserAction = (action: string) =>
useQuery({
queryKey: userKeys.userAction,
@@ -185,6 +207,19 @@ export const useLogoutUser = () =>
}
});
export const useGetMyIp = () => {
return useQuery({
queryKey: userKeys.myIp,
queryFn: async () => {
const { data } = await apiRequest.get<{ ip: string; }>(
"/api/v1/users/me/ip"
);
return data.ip;
},
enabled: true
});
}
export const useGetMyAPIKeys = () => {
return useQuery({
queryKey: userKeys.myAPIKeys,
+7
View File
@@ -1,11 +1,18 @@
import { UserWsKeyPair } from "../keys/types";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
}
export type User = {
createdAt: Date;
updatedAt: Date;
email?: string;
firstName?: string;
lastName?: string;
authProvider?: AuthProvider;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
-99
View File
@@ -1,99 +0,0 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import jwt_decode from "jwt-decode";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import SecurityClient, { PROVIDER_AUTH_TOKEN_KEY } from "@app/components/utilities/SecurityClient";
export const useProviderAuth = () => {
const router = useRouter();
const { providerAuthToken: redirectedProviderAuthToken } = router.query;
const [email, setEmail] = useState<string>("");
const [userId, setUserId] = useState<string>("");
const [providerAuthToken, setProviderAuthToken] = useState<string>(
redirectedProviderAuthToken as string || ""
);
const [isProviderUserCompleted, setIsProviderUserCompleted] = useState<boolean>();
const { createNotification } = useNotificationContext();
const AUTH_ERROR_KEY = "PROVIDER_AUTH_ERROR"
const handleRedirectWithToken = () => {
if (providerAuthToken) {
const {
userId: resultUserId,
email: resultEmail,
isUserCompleted: resultIsUserCompleted,
} = jwt_decode(providerAuthToken) as any;
setEmail(resultEmail);
setUserId(resultUserId);
setIsProviderUserCompleted(resultIsUserCompleted);
}
}
useEffect(() => {
handleRedirectWithToken();
// reset when there is no redirect auth token
if (!providerAuthToken) {
SecurityClient.setProviderAuthToken("");
}
window.localStorage.removeItem(AUTH_ERROR_KEY);
const handleStorageChange = (event: StorageEvent) => {
if (event.storageArea !== localStorage) {
return;
}
if (event.key === PROVIDER_AUTH_TOKEN_KEY) {
if (event.newValue) {
const token = event.newValue;
const {
userId: resultUserId,
email: resultEmail,
isUserCompleted: resultIsUserCompleted,
} = jwt_decode(token) as any;
setIsProviderUserCompleted(resultIsUserCompleted);
setProviderAuthToken(token);
setEmail(resultEmail);
setUserId(resultUserId);
} else {
setProviderAuthToken("");
setEmail("");
setUserId("");
setIsProviderUserCompleted(false);
}
setProviderAuthToken(event.newValue || "");
}
if (event.key === AUTH_ERROR_KEY) {
if (event.newValue) {
createNotification({
text: "An error has occured during login.",
type: "error",
timeoutMs: 6000,
})
window.localStorage.removeItem(AUTH_ERROR_KEY);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange);
};
}, []);
return {
email,
isProviderUserCompleted,
providerAuthToken,
userId,
setEmail,
setProviderAuthToken,
setUserId,
};
};
@@ -330,6 +330,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
onValueChange={(value) => {
router.push(`/project/${value}/secrets`);
localStorage.setItem("projectData.id", value);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
@@ -404,6 +405,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
}
icon="system-outline-126-verified"
>
IP Allowlist
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<a>
<MenuItem
@@ -70,7 +70,7 @@ const completeAccountInformationSignup = async ({
verifier,
organizationName,
providerAuthToken,
attributionSource,
...(attributionSource ? { attributionSource } : {})
});
return data;
@@ -34,7 +34,7 @@ const createIntegration = ({
owner,
path,
region,
secretPath
secretPath,
}: Props) =>
SecurityClient.fetchCall("/api/v1/integration", {
method: "POST",
@@ -54,7 +54,7 @@ const createIntegration = ({
owner,
path,
region,
secretPath
secretPath,
})
}).then(async (res) => {
if (res && res.status === 200) {
@@ -0,0 +1,198 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function BitBucketCreateIntegrationPage() {
const router = useRouter();
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: targetEnvironments } = useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: targetEnvironmentId
});
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
useEffect(() => {
if (targetEnvironments) {
if (targetEnvironments.length > 0) {
setTargetEnvironmentId(targetEnvironments[0].slug);
} else {
setTargetEnvironmentId("none");
}
}
}, [targetEnvironments]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
const targetApp = integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
);
const targetEnvironment = targetEnvironments?.find(
(environment) => environment.slug === targetEnvironmentId
);
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp.name,
appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.slug,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetEnvironments ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">BitBucket Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="BitBucket Workspace">
<Select
value={targetEnvironmentId}
onValueChange={(val) => setTargetEnvironmentId(val)}
className="w-full border border-mineshaft-500"
isDisabled={targetEnvironments.length === 0}
>
{targetEnvironments.length > 0 ? (
targetEnvironments.map((targetEnvironment) => (
<SelectItem
value={targetEnvironment.slug as string}
key={`target-environment-${targetEnvironment.slug as string}`}
>
{targetEnvironment.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-environment-none">
No workspaces found
</SelectItem>
)}
</Select>
</FormControl>
<FormControl label="BitBucket Repo">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId as string}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No repositories found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
BitBucketCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,34 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function BitBucketOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split("?")[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem("latestCSRFToken")) return;
localStorage.removeItem("latestCSRFToken");
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem("projectData.id") as string,
code: code as string,
integration: "bitbucket"
});
router.push(`/integrations/bitbucket/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />;
}
BitBucketOAuth2CallbackPage.requireAuth = true;
@@ -0,0 +1,64 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function Cloud66CreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("Access token cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "cloud-66",
accessId: null,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/cloud-66/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Cloud 66 Integration</CardTitle>
<FormControl
label="Cloud 66 Personal Access Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Cloud 66
</Button>
</Card>
</div>
);
}
Cloud66CreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function Cloud66CreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Cloud 66 Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Cloud 66 Application" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No applications found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
Cloud66CreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,64 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function DigitalOceanAppPlatformCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "digital-ocean-app-platform",
accessId: null,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/digital-ocean-app-platform/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Digital Ocean App Platform Integration</CardTitle>
<FormControl
label="Digital Ocean API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Digital Ocean App Platform
</Button>
</Card>
</div>
);
}
DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function DigitalOceanAppPlatformCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Digital Ocean App Platform Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Digital Ocean App Platform Service" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No services found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,64 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function NorthflankCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "northflank",
accessToken: apiKey,
accessId: null,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/northflank/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Northflank Integration</CardTitle>
<FormControl
label="Northflank API Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Northflank
</Button>
</Card>
</div>
);
}
NorthflankCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,202 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
useGetIntegrationAuthNorthflankSecretGroups
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function NorthflankCreateIntegrationPage() {
const router = useRouter();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [targetAppId, setTargetAppId] = useState("");
const [targetSecretGroupId, setTargetSecretGroupId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const { data: integrationAuthSecretGroups } = useGetIntegrationAuthNorthflankSecretGroups({
integrationAuthId: (integrationAuthId as string) ?? "",
appId: targetAppId
});
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
// setTargetApp(integrationAuthApps[0].name);
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
// setTargetApp("none");
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
useEffect(() => {
if (integrationAuthSecretGroups) {
if (integrationAuthSecretGroups.length > 0) {
// case: project has at least 1 secret group in Northflank
setTargetSecretGroupId(integrationAuthSecretGroups[0].groupId);
} else {
// case: project has no secret groups in Northflank
setTargetSecretGroupId("none");
}
}
}, [integrationAuthSecretGroups]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
)?.name ?? null,
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: targetSecretGroupId,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetAppId ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Northflank Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Northflank Project" className="mt-4">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-environment-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
{targetSecretGroupId && integrationAuthSecretGroups && (
<FormControl label="Secret Group" className="mt-4">
<Select
value={targetSecretGroupId}
onValueChange={(val) => setTargetSecretGroupId(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthSecretGroups.length === 0}
>
{integrationAuthSecretGroups.length > 0 ? (
integrationAuthSecretGroups.map((secretGroup: any) => (
<SelectItem
value={secretGroup.groupId}
key={`target-secret-group-${secretGroup.groupId}`}
>
{secretGroup.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-secret-group-none">
No secret groups found
</SelectItem>
)}
</Select>
</FormControl>
)}
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0 || integrationAuthSecretGroups?.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
NorthflankCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,80 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function TerraformCloudCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [workspacesId, setWorkSpacesId] = useState("");
const [workspacesIdErrorText, setWorkspacesIdErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
setWorkspacesIdErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Token cannot be blank");
return;
}
if (workspacesId.length === 0) {
setWorkspacesIdErrorText("Workspace Id cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "terraform-cloud",
accessId: workspacesId,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/terraform-cloud/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
<FormControl
label="Terraform Cloud API Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="API Token" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<FormControl
label="Terraform Cloud Workspace ID"
errorText={workspacesIdErrorText}
isError={workspacesIdErrorText !== "" ?? false}
>
<Input placeholder="Workspace Id" value={workspacesId} onChange={(e) => setWorkSpacesId(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Terraform Cloud
</Button>
</Card>
</div>
);
}
TerraformCloudCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,189 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
const variableTypes = [
{ name: "env" },
{ name: "terraform" }
];
export default function TerraformCloudCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [variableType, setVariableType] = useState("");
const [variableTypeErrorText, setVariableTypeErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setVariableType(variableTypes[0].name);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setVariableTypeErrorText("");
if (variableType.length === 0 ) {
setVariableTypeErrorText("Variable Type cannot be blank!")
return;
}
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: variableType,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Category" className="mt-4" errorText={variableTypeErrorText}
isError={variableTypeErrorText !== "" ?? false}>
<Select
value={variableType}
onValueChange={(val) => setVariableType(val)}
className="w-full border border-mineshaft-500"
>
{
variableTypes.map((variable) => (
<SelectItem
value={variable.name}
key={`target-app-${variable.name}`}
>
{variable.name}
</SelectItem>
))
}
</Select>
</FormControl>
<FormControl label="Terraform Cloud Project" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No project found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
TerraformCloudCreateIntegrationPage.requireAuth = true;
-139
View File
@@ -1,139 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios"
// import ListBox from '@app/components/basic/Listbox';
import InitialLoginStep from "@app/components/login/InitialLoginStep";
import MFAStep from "@app/components/login/MFAStep";
import PasswordInputStep from "@app/components/login/PasswordInputStep";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { useProviderAuth } from "@app/hooks/useProviderAuth";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import getOrganizations from "./api/organization/getOrgs";
export default function Login() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [step, setStep] = useState(1);
const { t } = useTranslation();
// const lang = router.locale ?? 'en';
const {
providerAuthToken,
email: providerEmail,
setProviderAuthToken,
isProviderUserCompleted
} = useProviderAuth();
if (providerAuthToken && isProviderUserCompleted === false) {
router.push(`/signup?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
}
// const setLanguage = async (to: string) => {
// router.push('/login', '/login', { locale: to });
// localStorage.setItem('lang', to);
// };
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
try {
const userOrgs = await getOrganizations();
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrg = userOrgs[0] && userOrgs[0]._id;
// user details
const userDetails = await fetchUserDetails()
// send details back to client
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// send post request to cli with details
const cliUrl = `http://localhost:${callbackPort}`
const instance = axios.create()
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
}
router.push(`/org/${userOrg}/overview`);
} catch (error) {
console.log("Error - Not logged in yet");
}
};
if (isLoggedIn()) {
redirectToDashboard();
}
}, []);
const renderView = (loginStep: number) => {
if (providerAuthToken && step === 1) {
return (
<PasswordInputStep
email={providerEmail}
password={password}
providerAuthToken={providerAuthToken}
setPassword={setPassword}
setProviderAuthToken={setProviderAuthToken}
setStep={setStep}
/>
);
}
if (loginStep === 1) {
return <InitialLoginStep
setStep={setStep}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
/>;
}
if (step === 2) {
return (
<MFAStep
email={email || providerEmail}
password={password}
providerAuthToken={providerAuthToken}
/>
);
}
return <div />;
};
return (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("login.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("login.og-title") ?? ""} />
<meta name="og:description" content={t("login.og-description") ?? ""} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
{renderView(step)}
{/* <div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="mx-auto w-48">
<ListBox
isSelected={lang}
onChange={setLanguage}
data={['en', 'ko', 'fr', 'pt-BR']}
isFull
text={`${t('common.language')}: `}
/>
</div>
</div> */}
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { Login } from "@app/views/Login";
export default function LoginPage() {
const { t } = useTranslation();
return (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("login.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("login.og-title") ?? ""} />
<meta name="og:description" content={t("login.og-description") ?? ""} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<Login />
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router"
import { LoginSSO } from "@app/views/Login";
export default function LoginSSOPage() {
const { t } = useTranslation();
const router = useRouter();
const token = router.query.token as string;
return (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("login.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("login.og-title") ?? ""} />
<meta name="og:description" content={t("login.og-description") ?? ""} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<LoginSSO providerAuthToken={token} />
</div>
);
}
@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSlack } from "@fortawesome/free-brands-svg-icons";
@@ -325,9 +324,12 @@ export default function Organization() {
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div>
<Link href={`/project/${workspace._id}/secrets`}>
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
</Link>
<button type="button" onClick={() => {
router.push(`/project/${workspace._id}/secrets`);
localStorage.setItem("projectData.id", workspace._id);
}}>
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
</button>
</div>)}
</div>
{orgWorkspaces.length === 0 && (
@@ -0,0 +1,21 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage";
const ProjectAllowlist = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<IPAllowlistPage />
</>
);
}
export default ProjectAllowlist;
ProjectAllowlist.requireAuth = true;
@@ -183,7 +183,9 @@ export default function Users() {
<div className="ml-2 flex min-w-max flex-row items-start justify-start">
<Button
text={String(t("section.members.add-member"))}
onButtonPressed={openAddModal}
onButtonPressed={() => {
openAddModal();
}}
color="mineshaft"
size="md"
icon={faPlus}
-90
View File
@@ -1,90 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { Button, Input } from "@app/components/v2";
import { isLoggedIn } from "@app/reactQuery";
import getOrganizations from "./api/organization/getOrgs";
export default function Login() {
const router = useRouter();
const [password, setPassword] = useState("");
const { t } = useTranslation();
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
let userOrg;
try {
const userOrgs = await getOrganizations();
userOrg = userOrgs[0] && userOrgs[0]._id;
router.push(`/org/${userOrg}/overview`);
} catch (error) {
console.log("Error - Not logged in yet");
}
};
if (isLoggedIn()) {
redirectToDashboard();
}
}, []);
return (
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("login.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("login.og-title") ?? ""} />
<meta name="og:description" content={t("login.og-description") ?? ""} />
</Head>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What’s your email?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {}}
isFullWidth
className="h-14"
>
{t("login.login")}
</Button>
</div>
<div className="flex flex-row items-center justify-center mt-4">
<button
onClick={() => {router.push("/login")}}
type="button"
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
</div>
);
}
@@ -13,10 +13,8 @@ import TeamInviteStep from "@app/components/signup/TeamInviteStep";
import UserInfoStep from "@app/components/signup/UserInfoStep";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { useProviderAuth } from "@app/hooks/useProviderAuth";
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
import getOrganizations from "./api/organization/getOrgs";
import checkEmailVerificationCode from "@app/pages/api/auth/CheckEmailVerificationCode";
import getOrganizations from "@app/pages/api/organization/getOrgs";
/**
* @returns the signup page
@@ -35,15 +33,6 @@ export default function SignUp() {
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
const { t } = useTranslation();
const { email: providerEmail, providerAuthToken, isProviderUserCompleted } = useProviderAuth();
if (providerAuthToken && isProviderUserCompleted) {
router.push(`/login?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
}
if (providerAuthToken && step < 3) {
setStep(3);
}
useEffect(() => {
const tryAuth = async () => {
@@ -121,7 +110,7 @@ export default function SignUp() {
return (
<UserInfoStep
incrementStep={incrementStep}
email={email || providerEmail}
email={email}
password={password}
setPassword={setPassword}
name={name}
@@ -130,7 +119,7 @@ export default function SignUp() {
setOrganizationName={setOrganizationName}
attributionSource={attributionSource}
setAttributionSource={setAttributionSource}
providerAuthToken={providerAuthToken}
providerAuthToken={undefined}
/>
);
}
@@ -139,7 +128,7 @@ export default function SignUp() {
return (
<DownloadBackupPDF
incrementStep={incrementStep}
email={email || providerEmail}
email={email}
password={password}
name={name}
/>
+28
View File
@@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router"
import { SignupSSO } from "@app/views/Signup";
export default function SignupSSOPage() {
const { t } = useTranslation();
const router = useRouter();
const token = router.query.token as string;
return (
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
<Head>
<title>{t("common.head-title", { title: t("signup.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("signup.og-title") as string} />
<meta name="og:description" content={t("signup.og-description") as string} />
</Head>
<div className="mb-4 mt-20 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
<SignupSSO providerAuthToken={token} />
</div>
);
}
@@ -35,7 +35,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
@@ -86,15 +86,30 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "terraform-cloud":
link = `${window.location.origin}/integrations/terraform-cloud/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
case "bitbucket":
link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`;
break;
case "codefresh":
link = `${window.location.origin}/integrations/codefresh/authorize`;
break;
case "digital-ocean-app-platform":
link = `${window.location.origin}/integrations/digital-ocean-app-platform/authorize`;
break;
case "cloud-66":
link = `${window.location.origin}/integrations/cloud-66/authorize`;
break;
case "northflank":
link = `${window.location.origin}/integrations/northflank/authorize`;
break;
case "windmill":
link = `${window.location.origin}/integrations/windmill/authorize`;
break;
@@ -32,7 +32,7 @@ export const CloudIntegrationSection = ({
const isEmpty = !isLoading && !cloudIntegrations?.length;
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
return (
<div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
@@ -67,18 +67,9 @@ export const CloudIntegrationSection = ({
width={70}
alt="integration logo"
/>
{cloudIntegration.name.split(" ").length > 2 ? (
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegration.name.split(" ")[0]}</div>
<div className="text-base">
{cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]}
</div>
</div>
) : (
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
)}
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
{cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-40 h-full">
@@ -105,7 +105,8 @@ export const IntegrationsSection = ({
{(integration.integration === "vercel" ||
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab") && (
integration.integration === "gitlab" ||
integration.integration === "bitbucket") && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
+89
View File
@@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import axios from "axios"
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import {
InitialStep,
MFAStep,
SAMLSSOStep
} from "./components";
export const Login = () => {
const router = useRouter();
const [step, setStep] = useState(0);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const queryParams = new URLSearchParams(window.location.search)
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
try {
const userOrgs = await getOrganizations();
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrg = userOrgs[0] && userOrgs[0]._id;
// user details
const userDetails = await fetchUserDetails()
// send details back to client
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// send post request to cli with details
const cliUrl = `http://localhost:${callbackPort}`
const instance = axios.create()
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
}
router.push(`/org/${userOrg}/overview`);
} catch (error) {
console.log("Error - Not logged in yet");
}
};
if (isLoggedIn()) {
redirectToDashboard();
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return (
<InitialStep
setStep={setStep}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
/>
);
case 1:
return (
<MFAStep
email={email}
password={password}
providerAuthToken={undefined}
callbackPort={queryParams.get("callback_port")}
/>
);
case 2:
return (
<SAMLSSOStep setStep={setStep} />
);
default:
return <div />;
}
}
return (
<div>
{renderView()}
</div>
);
}
+65
View File
@@ -0,0 +1,65 @@
import { useEffect, useState } from "react";
import jwt_decode from "jwt-decode";
import {
MFAStep,
PasswordStep
} from "./components";
type Props = {
providerAuthToken: string;
}
export const LoginSSO = ({ providerAuthToken }: Props) => {
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const {
email,
isUserCompleted,
callbackPort
} = jwt_decode(providerAuthToken) as any;
useEffect(() => {
if (isUserCompleted) {
setStep(1);
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return (
<div />
);
case 1:
return (
<PasswordStep
providerAuthToken={providerAuthToken}
callbackPort={callbackPort}
email={email}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
return (
<MFAStep
providerAuthToken={providerAuthToken}
callbackPort={callbackPort}
email={email}
password={password}
/>
);
default:
return <div />;
}
}
return (
<div>
{renderView()}
</div>
);
}
@@ -0,0 +1,217 @@
import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios"
import Error from "@app/components/basic/Error";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import getOrganizations from "@app/pages/api/organization/getOrgs";
type Props = {
setStep: (step: number) => void;
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (email: string) => void;
}
export const InitialStep = ({
setStep,
email,
setEmail,
password,
setPassword
}: Props) => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const [loginEmailChosen, setLoginEmailChosen] = useState(false);
const queryParams = new URLSearchParams(window.location.search);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
try {
if (!email || !password) {
return;
}
setIsLoading(true);
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint
const instance = axios.create()
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
console.log(cliResp)
// cli page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(1);
setIsLoading(false);
return;
}
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
setIsLoading(false);
}
return (
<form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant={loginEmailChosen ? "outline_bg" : "solid"}
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
className="h-12 w-full mx-0"
>
{t("login.continue-with-google")}
</Button>
</div>
{loginEmailChosen && <>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
<div className='w-full border-t border-mineshaft-500' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-12"
/>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12 select:-webkit-autofill:focus"
/>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
type="submit"
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="solid"
isLoading={isLoading}
> Continue with Email </Button>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
<div className='w-full border-t border-mineshaft-500' />
</div></>}
{!loginEmailChosen && <div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
onClick={() => {
setLoginEmailChosen(true);
}}
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="outline_bg"
> Continue with Email </Button>
</div>}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(2);
}}
isFullWidth
className="h-12 w-full mx-0"
>
Continue with SAML SSO
</Button>
</div>
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Don&apos;t have an acount yet?</span>
<Link href="/signup">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span>
</Link>
</div>
<div className="text-bunker-400 text-sm flex flex-row">
<span className="mr-1">Forgot password?</span>
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link>
</div>
</form>
);
}
@@ -0,0 +1 @@
export { InitialStep } from "./InitialStep";
@@ -1,18 +1,17 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import axios from "axios"
import Error from "@app/components/basic/Error"; // which to notification
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import { Button } from "@app/components/v2";
import { useSendMfaToken } from "@app/hooks/api/auth";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import Error from "../basic/Error";
import { Button } from "../v2";
// The style for the verification code input
const props = {
inputStyle: {
@@ -33,6 +32,13 @@ const props = {
}
} as const;
type Props = {
email: string;
password: string;
providerAuthToken?: string;
callbackPort?: string | null;
}
interface VerifyMfaTokenError {
response: {
data: {
@@ -45,23 +51,13 @@ interface VerifyMfaTokenError {
};
}
/**
* 2nd step of login - users enter their MFA code
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.password - password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function MFAStep({
export const MFAStep = ({
email,
password,
providerAuthToken,
}: {
email: string;
password: string;
providerAuthToken?: string;
}): JSX.Element {
callbackPort
}: Props) => {
const { createNotification } = useNotificationContext();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingResend, setIsLoadingResend] = useState(false);
@@ -75,13 +71,15 @@ export default function MFAStep({
const handleLoginMfa = async () => {
try {
if (mfaCode.length !== 6) {
createNotification({
text: "Please enter a 6-digit MFA code and try again",
type: "error"
});
return;
}
setIsLoading(true);
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")){
const callbackPort = queryParams.get("callback_port")
if (callbackPort){
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLoginMfa({
@@ -116,12 +114,25 @@ export default function MFAStep({
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
} else {
createNotification({
text: "Failed to log in",
type: "error"
});
}
}
} catch (err) {
const error = err as VerifyMfaTokenError;
createNotification({
text: "Failed to log in",
type: "error"
});
if (error?.response?.status === 500) {
window.location.reload();
@@ -147,8 +158,8 @@ export default function MFAStep({
}
};
return (
<form className="mx-auto w-max md:px-8 pb-4 pt-4 md:mb-16">
return (
<form className="mx-auto w-max md:px-8 pb-4 pt-4 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">{t("mfa.step2-message")}</p>
<p className="text-l my-1 flex justify-center font-semibold text-bunker-300">{email} </p>
<div className="hidden md:block w-max min-w-[20rem] mx-auto">
@@ -204,6 +215,6 @@ export default function MFAStep({
</div>
<p className="text-sm text-bunker-400 pb-2">{t("signup.step2-spam-alert")}</p>
</div>
</form>
);
}
</form>
);
}
@@ -0,0 +1 @@
export { MFAStep } from "./MFAStep";
@@ -0,0 +1,161 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router"
import axios from "axios"
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import getOrganizations from "@app/pages/api/organization/getOrgs";
type Props = {
providerAuthToken: string;
callbackPort?: string;
email: string;
password: string;
setPassword: (password: string) => void;
setStep: (step: number) => void;
}
export const PasswordStep = ({
providerAuthToken,
callbackPort,
email,
password,
setPassword,
setStep
}: Props) => {
const { createNotification } = useNotificationContext();
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const router = useRouter();
const handleLogin = async () => {
try {
setIsLoading(true);
if (callbackPort) {
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
providerAuthToken
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint
const instance = axios.create()
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
// cli page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken,
});
if (loginAttempt && loginAttempt.success) {
// case: login was successful
if (loginAttempt.mfaEnabled) {
// TODO: deal with MFA
// case: login requires MFA step
setIsLoading(false);
setStep(2);
return;
}
// case: login does not require MFA step
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0]._id;
setIsLoading(false);
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg?._id}/overview`);
}
}
} catch (err) {
setIsLoading(false);
createNotification({
text: "Login unsuccessful. Double-check your master password and try again.",
type: "error"
});
console.error(err);
}
};
return (
<form
onSubmit={(e) => e.preventDefault()}
className="h-full mx-auto w-full max-w-md px-6 pt-8"
>
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What’s your Infisical Password?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={async () => handleLogin()}
isFullWidth
isLoading={isLoading}
className="h-14"
>
{t("login.login")}
</Button>
</div>
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
<span className='duration-200 max-w-sm text-center px-4'>
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
</span>
<Link href="/verify-email">
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
</Link>
</div>
<div className="flex flex-row items-center justify-center">
<button
onClick={() => {
router.push("/login");
}}
type="button"
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</form>
);
}
@@ -0,0 +1 @@
export { PasswordStep } from "./PasswordStep";
@@ -0,0 +1,65 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Input } from "@app/components/v2";
type Props = {
setStep: (step: number) => void;
}
export const SAMLSSOStep = ({
setStep
}: Props) => {
const [ssoIdentifier, setSSOIdentifier] = useState("");
const { t } = useTranslation();
const queryParams = new URLSearchParams(window.location.search);
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What&apos;s your SSO Identifier?
</p>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={ssoIdentifier}
onChange={(e) => setSSOIdentifier(e.target.value)}
type="text"
placeholder="Enter your SSO identifier..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
window.close();
}}
isFullWidth
className="h-14"
>
{t("login.login")}
</Button>
</div>
<div className="flex flex-row items-center justify-center mt-4">
<button
onClick={() => {
setStep(0);
}}
type="button"
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
);
}
@@ -0,0 +1 @@
export { SAMLSSOStep } from "./SAMLSSOStep";
@@ -0,0 +1,6 @@
export { InitialStep } from "./InitialStep";
export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep";
// SSO-specific step
export { PasswordStep } from "./PasswordStep";
+2
View File
@@ -0,0 +1,2 @@
export { Login } from "./Login";
export { LoginSSO } from "./LoginSSO";
@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal,
@@ -26,9 +27,11 @@ import {
Th,
THead,
Tr,
UpgradePlanModal} from "@app/components/v2";
import { useWorkspace } from "@app/context";
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization , useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetSSOConfig } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { OrgUser, Workspace } from "@app/hooks/api/types";
@@ -69,6 +72,9 @@ export const OrgMembersTable = ({
setCompleteInviteLink
}: Props) => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const {data: serverDetails } = useFetchServerStatus()
const { workspaces } = useWorkspace();
@@ -79,7 +85,7 @@ export const OrgMembersTable = ({
"upgradePlan",
"setUpEmail"
] as const);
useEffect(() => {
if (router.query.action === "invite") {
handlePopUpOpen("addMember");
@@ -152,6 +158,15 @@ export const OrgMembersTable = ({
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
createNotification({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
return;
}
if (isMoreUserNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
@@ -0,0 +1,15 @@
import { IPAllowlistSection } from "./components";
export const IPAllowlistPage = () => {
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">IP Allowlist</p>
<div />
</div>
<IPAllowlistSection />
</div>
</div>
);
}
@@ -0,0 +1,193 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useAddTrustedIp,
useGetMyIp,
useUpdateTrustedIp
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
ipAddress: yup.string().required("IP address is required"),
comment: yup.string()
}).required();
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["trustedIp"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["trustedIp"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["trustedIp"]>, state?: boolean) => void;
};
export const IPAllowlistModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetMyIp();
const { currentWorkspace } = useWorkspace();
const addTrustedIp = useAddTrustedIp();
const updateTrustedIp = useUpdateTrustedIp();
const {
control,
setValue,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
useEffect(() => {
const trustedIpData = popUp?.trustedIp?.data as {
ipAddress: string;
comment: string;
prefix: number;
};
if (popUp?.trustedIp?.data) {
reset({
ipAddress: `${trustedIpData.ipAddress}${trustedIpData.prefix !== undefined ? `/${trustedIpData.prefix}` : ""}`,
comment: trustedIpData.comment
});
} else {
reset({
ipAddress: "",
comment: ""
});
}
}, [popUp?.trustedIp?.data]);
const onIPAllowlistModalSubmit = async ({
ipAddress,
comment
}: FormData) => {
try {
if (!currentWorkspace?._id) return;
if (popUp?.trustedIp?.data) {
await updateTrustedIp.mutateAsync({
workspaceId: currentWorkspace._id,
trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId,
ipAddress,
comment,
isActive: true
});
} else {
await addTrustedIp.mutateAsync({
workspaceId: currentWorkspace._id,
ipAddress,
comment,
isActive: true
});
}
createNotification({
text: `Successfully ${popUp?.trustedIp?.data ? "updated" : "added"} trusted IP`,
type: "success"
});
reset();
handlePopUpClose("trustedIp");
} catch (err) {
createNotification({
text: `Failed to ${popUp?.trustedIp?.data ? "update" : "add"} trusted IP`,
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.trustedIp?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("trustedIp", isOpen);
reset();
}}
>
<ModalContent title={popUp?.trustedIp?.data ? "Update IP" : "Add IP"}>
<form onSubmit={handleSubmit(onIPAllowlistModalSubmit)}>
<Controller
control={control}
defaultValue=""
name="ipAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="IPv4/IPv6 Address / CIDR Notation"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="123.456.789.0"
/>
</FormControl>
)}
/>
{!isLoading && data && (
<Button
colorSchema="secondary"
type="button"
onClick={() => setValue("ipAddress", data)}
className="mb-8"
>
Add current IP address
</Button>
)}
<Controller
control={control}
defaultValue=""
name="comment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Comment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My IP address"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.trustedIp?.data ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("trustedIp")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,105 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription,useWorkspace } from "@app/context";
import {
useDeleteTrustedIp
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { IPAllowlistModal } from "./IPAllowlistModal";
import { IPAllowlistTable } from "./IPAllowlistTable";
export const IPAllowlistSection = () => {
const { createNotification } = useNotificationContext();
const { mutateAsync } = useDeleteTrustedIp();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"trustedIp",
"deleteTrustedIp",
"upgradePlan"
] as const);
const onDeleteTrustedIpSubmit = async (trustedIpId: string) => {
try {
if (!currentWorkspace?._id) return;
await mutateAsync({
workspaceId: currentWorkspace._id,
trustedIpId
});
createNotification({
text: "Successfully deleted IP access range",
type: "success"
});
handlePopUpClose("deleteTrustedIp");
} catch (err) {
console.log(err);
createNotification({
text: "Failed to delete IP access range",
type: "error"
});
}
}
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
IP Allowlist
</h2>
<Button
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("trustedIp")
} else {
handlePopUpOpen("upgradePlan");
}
}}
colorSchema="secondary"
isLoading={false}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add IP
</Button>
</div>
<IPAllowlistTable
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IPAllowlistModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteTrustedIp.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteTrustedIp?.data as { name: string })?.name || " "
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteTrustedIp", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteTrustedIpSubmit((popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</div>
);
}
@@ -0,0 +1,162 @@
import { faGlobe, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context";
import {
useGetTrustedIps
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["upgradePlan"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["trustedIp", "deleteTrustedIp", "upgradePlan"]>,
data?: {
trustedIpId: string;
ipAddress?: string;
comment?: string;
isActive?: boolean;
prefix?: number;
},
) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["upgradePlan"]>, state?: boolean) => void;
};
export const IPAllowlistTable = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetTrustedIps(currentWorkspace?._id ?? "");
const formatType = (type: string, prefix?: number) => {
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${(prefix !== undefined) ? "CIDR" : ""}`;
}
return (
<div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">IP Address / Range</Th>
<Th className="flex-1">Format</Th>
<Th className="flex-1">Comment</Th>
{/* <Th className="flex-1">Status</Th> */}
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data
.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress))
.map(({
_id,
ipAddress,
comment,
type,
prefix,
isActive
}) => {
return (
<Tr
key={`ip-access-range-${_id}`}
className="h-10"
>
<Td>
{`${ipAddress}${(prefix !== undefined) ? `/${prefix}` : ""}`}
</Td>
<Td>
{formatType(type, prefix)}
</Td>
<Td>
{comment}
</Td>
{/* <Td>
<div className="flex items-center">
<FontAwesomeIcon
icon={faCircle}
color="#2ecc71"
/>
<p className="ml-4">Active</p>
</div>
</Td> */}
<Td className="flex items-center">
<IconButton
className="mr-3 py-2"
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("trustedIp", {
trustedIpId: _id,
ipAddress,
comment,
prefix,
isActive
});
} else {
handlePopUpOpen("upgradePlan");
}
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("deleteTrustedIp", {
trustedIpId: _id
});
} else {
handlePopUpOpen("upgradePlan");
}
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={4} key="ip-access-ranges" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No IP addresses added"
icon={faGlobe}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</div>
);
}
@@ -0,0 +1 @@
export { IPAllowlistSection } from "./IPAllowlistSection";
@@ -0,0 +1 @@
export { IPAllowlistPage } from "./IPAllowlistPage";
@@ -7,7 +7,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import {
Button,
FormControl,
Input} from "@app/components/v2";
Input
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
@@ -29,7 +29,7 @@ export const PmtMethodsSection = () => {
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Payment Methods
Payment methods
</h2>
<Button
onClick={handleAddPmtMethodBtnClick}
@@ -11,7 +11,8 @@ import {
Td,
Th,
THead,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteOrgPmtMethod,
@@ -10,7 +10,8 @@ import {
Modal,
ModalContent,
Select,
SelectItem} from "@app/components/v2";
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useAddOrgTaxId } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -138,22 +139,22 @@ export const TaxIDModal = ({
defaultValue="eu_vat"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Type"
errorText={error?.message}
isError={Boolean(error)}
label="Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{taxIDTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{taxIDTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
@@ -163,9 +164,9 @@ export const TaxIDModal = ({
name="value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Value"
isError={Boolean(error)}
errorText={error?.message}
label="Value"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
@@ -11,7 +11,8 @@ import {
Td,
Th,
THead,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgInvoices
@@ -1,10 +1,6 @@
import { useTranslation } from "react-i18next";
import {
OrgIncidentContactsSection,
OrgNameChangeSection,
OrgServiceAccountsTable
} from "./components";
import { OrgTabGroup } from "./components";
export const OrgSettingsPage = () => {
const { t } = useTranslation();
@@ -15,11 +11,7 @@ export const OrgSettingsPage = () => {
<div className="mb-4">
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
</div>
<OrgNameChangeSection />
<OrgIncidentContactsSection />
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<OrgServiceAccountsTable />
</div>
<OrgTabGroup />
</div>
</div>
);
@@ -0,0 +1,9 @@
import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = () => {
return (
<div>
<OrgSSOSection />
</div>
);
}
@@ -0,0 +1,119 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useGetSSOConfig,
useUpdateSSOConfig
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { SSOModal } from "./SSOModal";
const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML 2.0"
}
export const OrgSSOSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
const { mutateAsync } = useUpdateSSOConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"upgradePlan",
"addSSO"
] as const);
const handleSamlSSOToggle = async (value: boolean) => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
organizationId: currentOrg?._id,
isActive: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${value ? "enable" : "disable"} SAML SSO`,
type: "error"
});
}
}
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
SAML SSO Configuration
</h2>
{!isLoading && (
<Button
onClick={() => {
if (subscription?.samlSSO) {
handlePopUpOpen("addSSO");
} else {
handlePopUpOpen("upgradePlan");
}
}}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
{data ? "Update SAML SSO" : "Set up SAML SSO"}
</Button>
)}
</div>
{!isLoading && data && (
<>
<div className="mb-4">
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data.isActive}
>
Enable SAML SSO
</Switch>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{data._id}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">{ssoAuthProviderMap[data.authProvider]}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Audience</h3>
<p className="text-gray-400 text-md">{data.audience}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">{data.entryPoint}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{data.issuer}</p>
</div>
</>
)}
<SSOModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SAML SSO if you switch to Infisical's Pro plan."
/>
</div>
);
};
@@ -0,0 +1,253 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
TextArea} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateSSOConfig,
useGetSSOConfig,
useUpdateSSOConfig
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const ssoAuthProviders = [
{ label: "Okta SAML 2.0", value: "okta-saml" }
];
const schema = yup.object({
authProvider: yup.string().required("SSO Type is required"),
entryPoint: yup.string().required("IDP entrypoint is required"),
issuer: yup.string().required("Issuer string is required"),
cert: yup.string().required("IDP's public signing certificate is required"),
audience: yup.string().required("Expected SAML response audience is required"),
}).required();
export type AddSSOFormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["addSSO"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addSSO"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addSSO"]>, state?: boolean) => void;
};
export const SSOModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateSSOConfig();
const { data } = useGetSSOConfig(currentOrg?._id ?? "");
const {
control,
handleSubmit,
reset,
watch,
} = useForm<AddSSOFormData>({
defaultValues: {
authProvider: "okta-saml"
},
resolver: yupResolver(schema)
});
useEffect(() => {
if (data) {
reset({
authProvider: data?.authProvider ?? "",
entryPoint: data?.entryPoint ?? "",
issuer: data?.issuer ?? "",
cert: data?.cert ?? "",
audience: data?.audience ?? ""
});
}
}, [data]);
const onSSOModalSubmit = async ({
authProvider,
entryPoint,
issuer,
cert,
audience
}: AddSSOFormData) => {
try {
if (!currentOrg) return;
if (!data) {
await createMutateAsync({
organizationId: currentOrg._id,
authProvider,
isActive: false,
entryPoint,
issuer,
cert,
audience
});
} else {
await updateMutateAsync({
organizationId: currentOrg._id,
authProvider,
isActive: false,
entryPoint,
issuer,
cert,
audience
});
}
handlePopUpClose("addSSO");
createNotification({
text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} SAML SSO configuration`,
type: "error"
});
}
}
const authProvider = watch("authProvider");
return (
<Modal
isOpen={popUp?.addSSO?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addSSO", isOpen);
reset();
}}
>
<ModalContent title="Add SSO">
<form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller
control={control}
name="authProvider"
defaultValue="okta-saml"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{ssoAuthProviders.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{authProvider && authProvider === "okta-saml" && (
<>
<Controller
control={control}
name="audience"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Audience"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://your-domain.com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="entryPoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Entrypoint"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="https://your-domain.okta.com/app/app-name/xxx/sso/saml"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Issuer"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="http://www.okta.com/xxx"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="cert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
/>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addSSO")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1 @@
export { OrgAuthTab } from "./OrgAuthTab";
@@ -0,0 +1,15 @@
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable";
export const OrgGeneralTab = () => {
return (
<div>
<OrgNameChangeSection />
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<OrgServiceAccountsTable />
</div>
<OrgIncidentContactsSection />
</div>
);
}
@@ -0,0 +1 @@
export { OrgGeneralTab } from "./OrgGeneralTab";
@@ -17,7 +17,7 @@ export const OrgIncidentContactsSection = () => {
] as const);
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")}
@@ -0,0 +1,59 @@
import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { useOrganization,useUser } from "@app/context";
import {
useGetOrgUsers
} from "@app/hooks/api";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
export const OrgTabGroup = () => {
const { currentOrg } = useOrganization();
const { user } = useUser();
const { data } = useGetOrgUsers(currentOrg?._id ?? "");
const isRoleSufficient = data?.some((orgUser) => {
return orgUser.role !== "member" && orgUser.user._id === user._id;
});
const tabs = [
{ name: "General", key: "tab-org-general" },
];
if (isRoleSufficient) {
tabs.push(
{ name: "Authentication", key: "tab-org-auth" }
);
}
return (
<Tab.Group>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<OrgGeneralTab />
</Tab.Panel>
{isRoleSufficient && (
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
)}
</Tab.Panels>
</Tab.Group>
);
}
@@ -0,0 +1 @@
export { OrgTabGroup } from "./OrgTabGroup";
@@ -1,3 +1 @@
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
export { OrgNameChangeSection } from "./OrgNameChangeSection";
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
export { OrgTabGroup } from "./OrgTabGroup";
@@ -0,0 +1,132 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Select,
SelectItem} from "@app/components/v2";
import { useUser } from "@app/context";
import {
useUpdateUserAuthProvider
} from "@app/hooks/api";
const authMethods = [
{ label: "Email", value: "email" },
{ label: "Google SSO", value: "google" },
{ label: "Okta SAML 2.0", value: "okta-saml" },
];
const schema = yup.object({
authMethod: yup.string().required("Auth method is required")
});
export type FormData = yup.InferType<typeof schema>;
export const AuthMethodSection = () => {
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { mutateAsync, isLoading } = useUpdateUserAuthProvider();
const {
reset,
control,
handleSubmit
} = useForm<FormData>({
defaultValues: {
authMethod: user?.authProvider ?? "email"
},
resolver: yupResolver(schema)
});
useEffect(() => {
if (user) {
reset({
authMethod: user?.authProvider ?? "email"
});
}
}, [user]);
const onFormSubmit = async ({
authMethod
}: FormData) => {
try {
if (authMethod === "okta-saml") {
createNotification({
text: "Okta SAML 2.0 can only be configured in your organization settings",
type: "error"
});
return;
}
await mutateAsync({
authProvider: authMethod
});
createNotification({
text: "Successfully updated authentication method",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update authentication method",
type: "error"
});
}
}
return (
<form
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
onSubmit={handleSubmit(onFormSubmit)}
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Authentication Method
</h2>
<div className="max-w-md mb-4">
<Controller
control={control}
name="authMethod"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-800 border border-mineshaft-600"
>
{authMethods.map((authMethod) => {
return (
<SelectItem
value={authMethod.value}
key={`auth-method-${authMethod.value}`}
>
{authMethod.label}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
</div>
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading}
>
Save
</Button>
</form>
);
}
@@ -0,0 +1 @@
export { AuthMethodSection } from "./AuthMethodSection";
@@ -0,0 +1,13 @@
import { AuthMethodSection } from "../AuthMethodSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { MFASection } from "../SecuritySection";
export const PersonalAuthTab = () => {
return (
<div>
<MFASection />
<AuthMethodSection />
<ChangePasswordSection />
</div>
);
}
@@ -0,0 +1 @@
export { PersonalAuthTab } from "./PersonalAuthTab";
@@ -1,18 +1,14 @@
import { ChangeLanguageSection } from "../ChangeLanguageSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalSecurityTab = () => {
export const PersonalGeneralTab = () => {
return (
<div>
<UserNameSection />
<ChangeLanguageSection />
<SecuritySection />
<SessionsSection />
<ChangePasswordSection />
<EmergencyKitSection />
</div>
);
@@ -0,0 +1 @@
export { PersonalGeneralTab } from "./PersonalGeneralTab";
@@ -1 +0,0 @@
export { PersonalSecurityTab } from "./PersonalSecurityTab";
@@ -2,10 +2,12 @@ import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
import { PersonalSecurityTab } from "../PersonalSecurityTab";
import { PersonalAuthTab } from "../PersonalAuthTab";
import { PersonalGeneralTab } from "../PersonalGeneralTab";
const tabs = [
{ name: "General", key: "tab-account-security" },
{ name: "General", key: "tab-account-general" },
{ name: "Authentication", key: "tab-account-auth" },
{ name: "API Keys", key: "tab-account-api-keys" }
];
@@ -28,7 +30,10 @@ export const PersonalTabGroup = () => {
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<PersonalSecurityTab />
<PersonalGeneralTab />
</Tab.Panel>
<Tab.Panel>
<PersonalAuthTab />
</Tab.Panel>
<Tab.Panel>
<PersonalAPIKeyTab />
@@ -9,7 +9,7 @@ import { useGetUser } from "../../../../hooks/api";
import { User } from "../../../../hooks/api/types";
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
export const SecuritySection = () => {
export const MFASection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser();
const { createNotification } = useNotificationContext();
@@ -1 +1 @@
export { SecuritySection } from "./SecuritySection";
export { MFASection } from "./MFASection";
@@ -55,7 +55,7 @@ export const UserNameSection = (): JSX.Element => {
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
User name
Name
</p>
<div className="mb-2 max-w-md">
<Controller
@@ -18,7 +18,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "deleteEnv", "upgradePlan"]>,
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
{
name,
slug
+57
View File
@@ -0,0 +1,57 @@
import { useState } from "react";
import jwt_decode from "jwt-decode";
import {
BackupPDFStep,
UserInfoSSOStep} from "./components";
type Props = {
providerAuthToken: string;
}
export const SignupSSO = ({
providerAuthToken
}: Props) => {
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const {
email,
organizationName,
firstName,
lastName
} = jwt_decode(providerAuthToken) as any;
const renderView = () => {
switch (step) {
case 0:
return (
<UserInfoSSOStep
email={email}
name={`${firstName} ${lastName}`}
providerOrganizationName={organizationName}
password={password}
setPassword={setPassword}
setStep={setStep}
providerAuthToken={providerAuthToken}
/>
);
case 1:
return (
<BackupPDFStep
email={email}
password={password}
name={`${firstName} ${lastName}`}
/>
);
default:
return <div />
}
}
return (
<div>
{renderView()}
</div>
);
}
@@ -0,0 +1,66 @@
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router"
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import { Button } from "@app/components/v2";
interface DownloadBackupPDFStepProps {
email: string;
password: string;
name: string;
}
/**
* This is the step of the signup flow where the user downloads the backup pdf
* @param {object} obj
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
* @param {string} obj.email - user's email
* @param {string} obj.password - user's password
* @param {string} obj.name - user's name
* @returns
*/
export const BackupPDFStep = ({
email,
password,
name
}: DownloadBackupPDFStepProps) => {
const { t } = useTranslation();
const router = useRouter();
return (
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />{t("signup.step4-message")}
</p>
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
<span className='mb-2'>{t("signup.step4-description1")} {t("signup.step4-description3")}</span>
</div>
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: () => { },
setBackupKeyIssued: () => { }
});
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
}}
size="sm"
isFullWidth
className='h-12'
colorSchema="primary"
variant="outline_bg"
> Download PDF </Button>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1 @@
export { BackupPDFStep } from "./BackupPDFStep";
@@ -0,0 +1,320 @@
import crypto from "crypto";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useGetCommonPasswords } from "@app/hooks/api";
import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Props = {
setStep: (step: number) => void;
email: string;
password: string;
setPassword: (value: string) => void;
name: string;
providerOrganizationName: string;
providerAuthToken?: string;
}
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
/**
* This is the step of the sign up flow where people provife their name/surname and password
* @param {object} obj
* @param {string} obj.verificationToken - the token which we use to verify the legitness of a user
* @param {string} obj.incrementStep - a function to move to the next signup step
* @param {string} obj.email - email of a user who is signing up
* @param {string} obj.password - user's password
* @param {string} obj.setPassword - function managing the state of user's password
* @param {string} obj.firstName - user's first name
* @param {string} obj.setFirstName - function managing the state of user's first name
* @param {string} obj.lastName - user's lastName
* @param {string} obj.setLastName - function managing the state of user's last name
*/
export const UserInfoSSOStep = ({
email,
name,
providerOrganizationName,
password,
setPassword,
setStep,
providerAuthToken,
}: Props) => {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false);
const [organizationName, setOrganizationName] = useState("");
const [organizationNameError, setOrganizationNameError] = useState(false);
const [attributionSource, setAttributionSource] = useState("");
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
useEffect(() => {
if (providerOrganizationName !== undefined) {
setOrganizationName(providerOrganizationName);
}
}, []);
// Verifies if the information that the users entered (name, workspace)
// is there, and if the password matches the criteria.
const signupErrorCheck = async () => {
setIsLoading(true);
let errorCheck = false;
if (!name) {
setNameError(true);
errorCheck = true;
} else {
setNameError(false);
}
if (!organizationName) {
setOrganizationNameError(true);
errorCheck = true;
} else {
setOrganizationNameError(false);
}
errorCheck = checkPassword({
password,
commonPasswords,
setErrors
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
localStorage.setItem("PRIVATE_KEY", privateKey);
client.init(
{
username: email,
password
},
async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
try {
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountInformationSignup({
email,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]?._id;
const project = await ProjectService.initProject({
organizationId: orgId,
projectName: "Example Project"
});
localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project._id);
setStep(1);
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}
);
} else {
setIsLoading(false);
}
};
return (
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16">
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
{t("signup.step3-message")}
</p>
<div className="h-full mx-auto mb-36 w-max rounded-xl py-6 md:px-8 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800">
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p>
<Input
placeholder="Jane Doe"
value={name}
disabled
isRequired
autoComplete="given-name"
className="h-12"
/>
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
</div>
{providerOrganizationName === undefined && (
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
<Input
placeholder="Infisical"
value={organizationName}
onChange={(e) => setOrganizationName(e.target.value)}
isRequired
className="h-12"
disabled
/>
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
</div>
)}
{providerOrganizationName === undefined && (
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Where did you hear about us? <span className="font-light">(optional)</span></p>
<Input
placeholder=""
onChange={(e) => setAttributionSource(e.target.value)}
value={attributionSource}
className="h-12"
/>
</div>
)}
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
<InputField
label={t("section.password.password")}
onChangeHandler={(pass: string) => {
setPassword(pass);
checkPassword({
password: pass,
commonPasswords,
setErrors
});
}}
type="password"
value={password}
isRequired
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div
className="ml-1 flex flex-row items-top justify-start"
key={key}
>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div>
)}
</div>
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
type="submit"
onClick={signupErrorCheck}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t("signup.signup"))} </Button>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1 @@
export { UserInfoSSOStep } from "./UserInfoSSOStep";
@@ -0,0 +1,2 @@
export { BackupPDFStep } from "./BackupPDFStep";
export { UserInfoSSOStep } from "./UserInfoSSOStep";
+1
View File
@@ -0,0 +1 @@
export { SignupSSO } from "./SignupSSO";