feat: base for signup pages completed

This commit is contained in:
=
2024-12-14 20:10:01 +05:30
parent 3d03fece74
commit 18460e0678
25 changed files with 1243 additions and 96 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNavigate } from "@tanstack/react-router";
import { useAddUsersToOrg } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@@ -13,7 +13,7 @@ import { Button, EmailServiceSetupModal } from "../v2";
*/
export default function TeamInviteStep(): JSX.Element {
const { t } = useTranslation();
const router = useRouter();
const navigate = useNavigate();
const [emails, setEmails] = useState("");
const { data: serverDetails } = useFetchServerStatus();
@@ -22,7 +22,7 @@ export default function TeamInviteStep(): JSX.Element {
// Redirect user to the getting started page
const redirectToHome = async () => {
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
navigate({ to: `/org/${localStorage.getItem("orgData.id")}/overview` });
};
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
@@ -48,19 +48,19 @@ export default function TeamInviteStep(): JSX.Element {
<p className="mb-6 mt-4 flex justify-center text-center text-bunker-400 md:mx-8">
{t("signup.step5-subtitle")}
</p>
<div className="mx-auto mb-6 w-max rounded-xl border border-mineshaft-500 bg-mineshaft-800 px-8 pt-6 pb-4 drop-shadow-xl">
<div className="mx-auto mb-6 w-max rounded-xl border border-mineshaft-500 bg-mineshaft-800 px-8 pb-4 pt-6 drop-shadow-xl">
<div>
<div className="pl-1 pb-1 text-sm font-medium text-bunker-300">
<div className="pb-1 pl-1 text-sm font-medium text-bunker-300">
<span>Emails</span>
</div>
<textarea
className="h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2"
className="h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 px-2 py-1 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 placeholder:text-bunker-400 focus:ring-2"
value={emails}
onChange={(e) => setEmails(e.target.value)}
placeholder="email@example.com, email2@example.com..."
/>
</div>
<div className="mx-auto mt-2 flex w-full flex-row items-end justify-end text-sm md:mt-4 md:mb-2 md:min-w-[30rem] md:max-w-md">
<div className="mx-auto mt-2 flex w-full flex-row items-end justify-end text-sm md:mb-2 md:mt-4 md:min-w-[30rem] md:max-w-md">
<Button
onClick={() => {
if (serverDetails?.emailConfigured) {
@@ -84,7 +84,7 @@ export default function TeamInviteStep(): JSX.Element {
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/>
</div>
<div className="min-w-28 mx-auto mt-4 mb-2 flex max-h-24 min-w-[20rem] max-w-max flex-row items-center justify-center px-4 text-lg md:p-2">
<div className="mx-auto mb-2 mt-4 flex max-h-24 min-w-[20rem] max-w-max flex-row items-center justify-center px-4 text-lg md:p-2">
<Button
onClick={redirectToHome}
size="sm"

View File

@@ -121,7 +121,7 @@ export default function UserInfoStep({
password
},
async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
try {
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
@@ -224,7 +224,7 @@ export default function UserInfoStep({
{t("signup.step3-message")}
</p>
<div className="mx-auto mb-36 h-full w-max rounded-xl py-6 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800 md:px-8">
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Your Name
</p>
@@ -242,7 +242,7 @@ export default function UserInfoStep({
</p>
)}
</div>
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
</p>
@@ -260,7 +260,7 @@ export default function UserInfoStep({
</p>
)}
</div>
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Where did you hear about us? <span className="font-light">(optional)</span>
</p>
@@ -271,7 +271,7 @@ export default function UserInfoStep({
className="h-12"
/>
</div>
<div className="mt-2 flex max-h-60 w-1/4 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
<div className="mt-2 flex max-h-60 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
<InputField
label={t("section.password.password")}
onChangeHandler={async (pass: string) => {

View File

@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/hooks/api/reactQuery";
import { setAuthToken } from "../reactQuery";
import { organizationKeys } from "../organization/queries";
import { workspaceKeys } from "../workspace";
@@ -148,7 +148,7 @@ export const useCompleteAccountSignup = () => {
};
export const useSendMfaToken = () => {
return useMutation<object, object, SendMfaTokenDTO>({
return useMutation<{}, {}, SendMfaTokenDTO>({
mutationFn: async ({ email }) => {
const { data } = await apiRequest.post("/api/v2/auth/mfa/send", { email });
return data;
@@ -175,7 +175,7 @@ export const verifyMfaToken = async ({
};
export const useVerifyMfaToken = () => {
return useMutation<VerifyMfaTokenRes, object, VerifyMfaTokenDTO>({
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
mutationFn: async ({ email, mfaCode, mfaMethod }) => {
return verifyMfaToken({
email,

View File

@@ -7,5 +7,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth"
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
};

View File

@@ -5,5 +5,11 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth"
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth"
}
export enum IdentityJwtConfigurationType {
JWKS = "jwks",
STATIC = "static"
}

View File

@@ -4,6 +4,7 @@ export {
useAddIdentityAwsAuth,
useAddIdentityAzureAuth,
useAddIdentityGcpAuth,
useAddIdentityJwtAuth,
useAddIdentityKubernetesAuth,
useAddIdentityOidcAuth,
useAddIdentityTokenAuth,
@@ -15,6 +16,7 @@ export {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityJwtAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
@@ -25,20 +27,24 @@ export {
useUpdateIdentityAwsAuth,
useUpdateIdentityAzureAuth,
useUpdateIdentityGcpAuth,
useUpdateIdentityJwtAuth,
useUpdateIdentityKubernetesAuth,
useUpdateIdentityOidcAuth,
useUpdateIdentityTokenAuth,
useUpdateIdentityTokenAuthToken,
useUpdateIdentityUniversalAuth} from "./mutations";
useUpdateIdentityUniversalAuth
} from "./mutations";
export {
useGetIdentityAwsAuth,
useGetIdentityAzureAuth,
useGetIdentityById,
useGetIdentityGcpAuth,
useGetIdentityJwtAuth,
useGetIdentityKubernetesAuth,
useGetIdentityOidcAuth,
useGetIdentityProjectMemberships,
useGetIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets} from "./queries";
useGetIdentityUniversalAuthClientSecrets
} from "./queries";

View File

@@ -8,6 +8,7 @@ import {
AddIdentityAwsAuthDTO,
AddIdentityAzureAuthDTO,
AddIdentityGcpAuthDTO,
AddIdentityJwtAuthDTO,
AddIdentityKubernetesAuthDTO,
AddIdentityOidcAuthDTO,
AddIdentityTokenAuthDTO,
@@ -22,6 +23,7 @@ import {
DeleteIdentityAzureAuthDTO,
DeleteIdentityDTO,
DeleteIdentityGcpAuthDTO,
DeleteIdentityJwtAuthDTO,
DeleteIdentityKubernetesAuthDTO,
DeleteIdentityOidcAuthDTO,
DeleteIdentityTokenAuthDTO,
@@ -32,6 +34,7 @@ import {
IdentityAwsAuth,
IdentityAzureAuth,
IdentityGcpAuth,
IdentityJwtAuth,
IdentityKubernetesAuth,
IdentityOidcAuth,
IdentityTokenAuth,
@@ -42,6 +45,7 @@ import {
UpdateIdentityAzureAuthDTO,
UpdateIdentityDTO,
UpdateIdentityGcpAuthDTO,
UpdateIdentityJwtAuthDTO,
UpdateIdentityKubernetesAuthDTO,
UpdateIdentityOidcAuthDTO,
UpdateIdentityTokenAuthDTO,
@@ -518,6 +522,118 @@ export const useDeleteIdentityOidcAuth = () => {
}
});
};
export const useUpdateIdentityJwtAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityJwtAuth, {}, UpdateIdentityJwtAuthDTO>({
mutationFn: async ({
identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject
}) => {
const {
data: { identityJwtAuth }
} = await apiRequest.patch<{ identityJwtAuth: IdentityJwtAuth }>(
`/api/v1/auth/jwt-auth/identities/${identityId}`,
{
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityJwtAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId));
}
});
};
export const useAddIdentityJwtAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityJwtAuth, {}, AddIdentityJwtAuthDTO>({
mutationFn: async ({
identityId,
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityJwtAuth }
} = await apiRequest.post<{ identityJwtAuth: IdentityJwtAuth }>(
`/api/v1/auth/jwt-auth/identities/${identityId}`,
{
configurationType,
jwksUrl,
jwksCaCert,
publicKeys,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityJwtAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId));
}
});
};
export const useDeleteIdentityJwtAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, DeleteIdentityJwtAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityJwtAuth }
} = await apiRequest.delete(`/api/v1/auth/jwt-auth/identities/${identityId}`);
return identityJwtAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId));
}
});
};
export const useAddIdentityAzureAuth = () => {
const queryClient = useQueryClient();

View File

@@ -8,6 +8,7 @@ import {
IdentityAwsAuth,
IdentityAzureAuth,
IdentityGcpAuth,
IdentityJwtAuth,
IdentityKubernetesAuth,
IdentityMembership,
IdentityMembershipOrg,
@@ -29,6 +30,7 @@ export const identitiesKeys = {
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const,
getIdentityTokensTokenAuth: (identityId: string) =>
[{ identityId }, "identity-tokens-token-auth"] as const,
getIdentityProjectMemberships: (identityId: string) =>
@@ -276,3 +278,30 @@ export const useGetIdentityOidcAuth = (
enabled: Boolean(identityId) && (options?.enabled ?? true)
});
};
export const useGetIdentityJwtAuth = (
identityId: string,
options?: UseQueryOptions<
IdentityJwtAuth,
unknown,
IdentityJwtAuth,
ReturnType<typeof identitiesKeys.getIdentityJwtAuth>
>
) => {
return useQuery({
queryKey: identitiesKeys.getIdentityJwtAuth(identityId),
queryFn: async () => {
const {
data: { identityJwtAuth }
} = await apiRequest.get<{ identityJwtAuth: IdentityJwtAuth }>(
`/api/v1/auth/jwt-auth/identities/${identityId}`
);
return identityJwtAuth;
},
staleTime: 0,
cacheTime: 0,
...options,
enabled: Boolean(identityId) && (options?.enabled ?? true)
});
};

View File

@@ -1,6 +1,6 @@
import { TOrgRole } from "../roles/types";
import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types";
import { IdentityAuthMethod } from "./enums";
import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums";
export type IdentityTrustedIp = {
id: string;
@@ -47,7 +47,7 @@ export type IdentityMembershipOrg = {
export type IdentityMembership = {
id: string;
identity: Identity;
project: Pick<Workspace, "id" | "name">;
project: Pick<Workspace, "id" | "name" | "type">;
roles: Array<
{
id: string;
@@ -446,6 +446,65 @@ export type DeleteIdentityTokenAuthDTO = {
identityId: string;
};
export type IdentityJwtAuth = {
identityId: string;
configurationType: IdentityJwtConfigurationType;
jwksUrl: string;
jwksCaCert: string;
publicKeys: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityJwtAuthDTO = {
organizationId: string;
identityId: string;
configurationType: string;
jwksUrl?: string;
jwksCaCert: string;
publicKeys?: string[];
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityJwtAuthDTO = {
organizationId: string;
identityId: string;
configurationType?: string;
jwksUrl?: string;
jwksCaCert?: string;
publicKeys?: string[];
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type DeleteIdentityJwtAuthDTO = {
organizationId: string;
identityId: string;
};
export type CreateTokenIdentityTokenAuthDTO = {
identityId: string;
name: string;

View File

@@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { ProjectType } from "../workspace/types";
export const useImportEnvKey = () => {
const queryClient = useQueryClient();
@@ -20,9 +21,7 @@ export const useImportEnvKey = () => {
"Content-Type": "multipart/form-data"
},
onUploadProgress: (progressEvent) => {
const percentCompleted = progressEvent?.total
? Math.round((progressEvent.loaded * 100) / progressEvent.total)
: 0;
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`Upload Progress: ${percentCompleted}%`);
}
});
@@ -33,7 +32,7 @@ export const useImportEnvKey = () => {
}
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
}
});
};

View File

@@ -3,11 +3,11 @@ import { AxiosError } from "axios";
import { apiRequest } from "@app/config/request";
import { SessionStorageKeys } from "@app/const";
import { setAuthToken } from "@app/hooks/api/reactQuery";
import { APIKeyDataV2 } from "../apiKeys/types";
import { MfaMethod } from "../auth/types";
import { TGroupWithProjectMemberships } from "../groups/types";
import { setAuthToken } from "../reactQuery";
import { workspaceKeys } from "../workspace";
import { userKeys } from "./query-keys";
import {
@@ -80,7 +80,7 @@ export const fetchUserProjectFavorites = async (orgId: string) => {
export const useRenameUser = () => {
const queryClient = useQueryClient();
return useMutation<object, object, RenameUserDTO>({
return useMutation<{}, {}, RenameUserDTO>({
mutationFn: ({ newName }) =>
apiRequest.patch("/api/v2/users/me/name", {
firstName: newName?.split(" ")[0],
@@ -152,7 +152,7 @@ export const useAddUsersToOrg = () => {
};
};
return useMutation<Response, object, AddUserToOrgDTO>({
return useMutation<Response, {}, AddUserToOrgDTO>({
mutationFn: (dto) => {
return apiRequest.post("/api/v1/invite-org/signup", dto);
},
@@ -207,7 +207,7 @@ export const useGetOrgMembershipProjectMemberships = (
export const useDeleteOrgMembership = () => {
const queryClient = useQueryClient();
return useMutation<object, object, DeletOrgMembershipDTO>({
return useMutation<{}, {}, DeletOrgMembershipDTO>({
mutationFn: ({ membershipId, orgId }) => {
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`);
},
@@ -220,7 +220,7 @@ export const useDeleteOrgMembership = () => {
export const useDeactivateOrgMembership = () => {
const queryClient = useQueryClient();
return useMutation<object, object, DeletOrgMembershipDTO>({
return useMutation<{}, {}, DeletOrgMembershipDTO>({
mutationFn: ({ membershipId, orgId }) => {
return apiRequest.post(
`/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate`
@@ -236,7 +236,7 @@ export const useDeactivateOrgMembership = () => {
export const useUpdateOrgMembership = () => {
const queryClient = useQueryClient();
return useMutation<object, object, UpdateOrgMembershipDTO>({
return useMutation<{}, {}, UpdateOrgMembershipDTO>({
mutationFn: ({ organizationId, membershipId, role, isActive, metadata }) => {
return apiRequest.patch(
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
@@ -261,7 +261,7 @@ export const useUpdateOrgMembership = () => {
export const useRegisterUserAction = () => {
const queryClient = useQueryClient();
return useMutation<object, object, string>({
return useMutation<{}, {}, string>({
mutationFn: (action) => apiRequest.post("/api/v1/user-action", { action }),
onSuccess: () => {
queryClient.invalidateQueries(userKeys.userAction);

View File

@@ -1,6 +1,6 @@
import { MfaMethod } from "../auth/types";
import { UserWsKeyPair } from "../keys/types";
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
import { ProjectType, ProjectUserMembershipTemporaryMode } from "../workspace/types";
export enum AuthMethod {
EMAIL = "email",
@@ -95,6 +95,7 @@ export type TWorkspaceUser = {
project: {
id: string;
name: string;
type: ProjectType;
};
inviteEmail: string;
organization: string;

View File

@@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request";
import { userKeys } from "../users/query-keys";
import { workspaceKeys } from "./query-keys";
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
import { ProjectType, TUpdateWorkspaceGroupRoleDTO } from "./types";
export const useAddGroupToWorkspace = () => {
const queryClient = useQueryClient();
@@ -83,7 +83,7 @@ export const useLeaveProject = () => {
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/leave`);
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace());
}
});
};
@@ -95,7 +95,7 @@ export const useMigrateProjectToV3 = () => {
return apiRequest.post(`/api/v1/workspace/${workspaceId}/migrate-v3`);
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
}
});
};

View File

@@ -26,6 +26,7 @@ import {
DeleteWorkspaceDTO,
NameWorkspaceSecretsDTO,
ProjectIdentityOrderBy,
ProjectType,
TGetUpgradeProjectStatusDTO,
TListProjectIdentitiesDTO,
ToggleAutoCapitalizationDTO,
@@ -82,7 +83,7 @@ export const useUpgradeProject = () => {
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
}
});
};
@@ -102,10 +103,11 @@ export const useGetUpgradeProjectStatus = ({
});
};
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
const fetchUserWorkspaces = async (includeRoles?: boolean, type?: ProjectType | "all") => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
params: {
includeRoles
includeRoles,
type
}
});
return data.workspaces;
@@ -139,8 +141,16 @@ export const useGetWorkspaceById = (
});
};
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
export const useGetUserWorkspaces = ({
includeRoles,
type = "all"
}: {
includeRoles?: boolean;
type?: ProjectType | "all";
} = {}) =>
useQuery(workspaceKeys.getAllUserWorkspace(type || ""), () =>
fetchUserWorkspaces(includeRoles, type)
);
const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
@@ -206,33 +216,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
refetchInterval: 4000
});
export const createWorkspace = ({
projectName,
projectDescription,
kmsKeyId,
template
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", {
projectName,
projectDescription,
kmsKeyId,
template
});
export const createWorkspace = (
dto: CreateWorkspaceDTO
): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", dto);
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) =>
createWorkspace({
projectName,
projectDescription,
kmsKeyId,
template
template,
type
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.data.project.type));
}
});
};
@@ -240,15 +243,19 @@ export const useCreateWorkspace = () => {
export const useUpdateProject = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateProjectDTO>({
mutationFn: ({ projectID, newProjectName, newProjectDescription }) => {
return apiRequest.patch(`/api/v1/workspace/${projectID}`, {
name: newProjectName,
description: newProjectDescription
});
return useMutation<Workspace, {}, UpdateProjectDTO>({
mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`,
{
name: newProjectName,
description: newProjectDescription
}
);
return data.workspace;
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
}
});
};
@@ -256,13 +263,18 @@ export const useUpdateProject = () => {
export const useToggleAutoCapitalization = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
mutationFn: ({ workspaceID, state }) =>
apiRequest.post(`/api/v1/workspace/${workspaceID}/auto-capitalization`, {
autoCapitalization: state
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
return useMutation<Workspace, {}, ToggleAutoCapitalizationDTO>({
mutationFn: async ({ workspaceID, state }) => {
const { data } = await apiRequest.post<{ workspace: Workspace }>(
`/api/v1/workspace/${workspaceID}/auto-capitalization`,
{
autoCapitalization: state
}
);
return data.workspace;
},
onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
}
});
};
@@ -270,14 +282,15 @@ export const useToggleAutoCapitalization = () => {
export const useUpdateWorkspaceVersionLimit = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdatePitVersionLimitDTO>({
mutationFn: ({ projectSlug, pitVersionLimit }) => {
return apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, {
return useMutation<Workspace, {}, UpdatePitVersionLimitDTO>({
mutationFn: async ({ projectSlug, pitVersionLimit }) => {
const { data } = await apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, {
pitVersionLimit
});
return data.workspace;
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
}
});
};
@@ -285,14 +298,18 @@ export const useUpdateWorkspaceVersionLimit = () => {
export const useUpdateWorkspaceAuditLogsRetention = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateAuditLogsRetentionDTO>({
mutationFn: ({ projectSlug, auditLogsRetentionDays }) => {
return apiRequest.put(`/api/v1/workspace/${projectSlug}/audit-logs-retention`, {
auditLogsRetentionDays
});
return useMutation<Workspace, {}, UpdateAuditLogsRetentionDTO>({
mutationFn: async ({ projectSlug, auditLogsRetentionDays }) => {
const { data } = await apiRequest.put(
`/api/v1/workspace/${projectSlug}/audit-logs-retention`,
{
auditLogsRetentionDays
}
);
return data.workspace;
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
}
});
};
@@ -300,12 +317,13 @@ export const useUpdateWorkspaceAuditLogsRetention = () => {
export const useDeleteWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteWorkspaceDTO>({
mutationFn: ({ workspaceID }) => {
return apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
return useMutation<Workspace, {}, DeleteWorkspaceDTO>({
mutationFn: async ({ workspaceID }) => {
const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
return data.workspace;
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
queryClient.invalidateQueries(["org-admin-projects"]);
}
});
@@ -322,7 +340,7 @@ export const useCreateWsEnvironment = () => {
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
}
});
};
@@ -339,7 +357,7 @@ export const useUpdateWsEnvironment = () => {
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
}
});
};
@@ -352,7 +370,7 @@ export const useDeleteWsEnvironment = () => {
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/environments/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
}
});
};

View File

@@ -11,7 +11,8 @@ export const workspaceKeys = {
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const,
getAllUserWorkspace: (type?: string) =>
type ? ["workspaces", { type }] : (["workspace"] as const),
getWorkspaceAuditLogs: (workspaceId: string) =>
[{ workspaceId }, "workspace-audit-logs"] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,

View File

@@ -8,6 +8,12 @@ export enum ProjectVersion {
V3 = 3
}
export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms"
}
export enum ProjectUserMembershipTemporaryMode {
Relative = "relative"
}
@@ -16,6 +22,7 @@ export type Workspace = {
__v: number;
id: string;
name: string;
type: ProjectType;
description?: string;
orgId: string;
version: ProjectVersion;
@@ -59,6 +66,7 @@ export type CreateWorkspaceDTO = {
projectDescription?: string;
kmsKeyId?: string;
template?: string;
type: ProjectType;
};
export type UpdateProjectDTO = {

View File

@@ -12,7 +12,9 @@
import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as SignupIndexImport } from './routes/signup/index'
import { Route as LoginIndexImport } from './routes/login/index'
import { Route as SignupSsoIndexImport } from './routes/signup/sso/index'
import { Route as LoginSsoIndexImport } from './routes/login/sso/index'
import { Route as LoginSelectOrganizationIndexImport } from './routes/login/select-organization/index'
import { Route as LoginLdapIndexImport } from './routes/login/ldap/index'
@@ -27,12 +29,24 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const SignupIndexRoute = SignupIndexImport.update({
id: '/signup/',
path: '/signup/',
getParentRoute: () => rootRoute,
} as any)
const LoginIndexRoute = LoginIndexImport.update({
id: '/login/',
path: '/login/',
getParentRoute: () => rootRoute,
} as any)
const SignupSsoIndexRoute = SignupSsoIndexImport.update({
id: '/signup/sso/',
path: '/signup/sso/',
getParentRoute: () => rootRoute,
} as any)
const LoginSsoIndexRoute = LoginSsoIndexImport.update({
id: '/login/sso/',
path: '/login/sso/',
@@ -82,6 +96,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginIndexImport
parentRoute: typeof rootRoute
}
'/signup/': {
id: '/signup/'
path: '/signup'
fullPath: '/signup'
preLoaderRoute: typeof SignupIndexImport
parentRoute: typeof rootRoute
}
'/login/provider/error': {
id: '/login/provider/error'
path: '/login/provider/error'
@@ -117,6 +138,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginSsoIndexImport
parentRoute: typeof rootRoute
}
'/signup/sso/': {
id: '/signup/sso/'
path: '/signup/sso'
fullPath: '/signup/sso'
preLoaderRoute: typeof SignupSsoIndexImport
parentRoute: typeof rootRoute
}
}
}
@@ -125,32 +153,38 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginIndexRoute
'/signup': typeof SignupIndexRoute
'/login/provider/error': typeof LoginProviderErrorRoute
'/login/provider/success': typeof LoginProviderSuccessRoute
'/login/ldap': typeof LoginLdapIndexRoute
'/login/select-organization': typeof LoginSelectOrganizationIndexRoute
'/login/sso': typeof LoginSsoIndexRoute
'/signup/sso': typeof SignupSsoIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginIndexRoute
'/signup': typeof SignupIndexRoute
'/login/provider/error': typeof LoginProviderErrorRoute
'/login/provider/success': typeof LoginProviderSuccessRoute
'/login/ldap': typeof LoginLdapIndexRoute
'/login/select-organization': typeof LoginSelectOrganizationIndexRoute
'/login/sso': typeof LoginSsoIndexRoute
'/signup/sso': typeof SignupSsoIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/login/': typeof LoginIndexRoute
'/signup/': typeof SignupIndexRoute
'/login/provider/error': typeof LoginProviderErrorRoute
'/login/provider/success': typeof LoginProviderSuccessRoute
'/login/ldap/': typeof LoginLdapIndexRoute
'/login/select-organization/': typeof LoginSelectOrganizationIndexRoute
'/login/sso/': typeof LoginSsoIndexRoute
'/signup/sso/': typeof SignupSsoIndexRoute
}
export interface FileRouteTypes {
@@ -158,50 +192,60 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/login'
| '/signup'
| '/login/provider/error'
| '/login/provider/success'
| '/login/ldap'
| '/login/select-organization'
| '/login/sso'
| '/signup/sso'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/login'
| '/signup'
| '/login/provider/error'
| '/login/provider/success'
| '/login/ldap'
| '/login/select-organization'
| '/login/sso'
| '/signup/sso'
id:
| '__root__'
| '/'
| '/login/'
| '/signup/'
| '/login/provider/error'
| '/login/provider/success'
| '/login/ldap/'
| '/login/select-organization/'
| '/login/sso/'
| '/signup/sso/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LoginIndexRoute: typeof LoginIndexRoute
SignupIndexRoute: typeof SignupIndexRoute
LoginProviderErrorRoute: typeof LoginProviderErrorRoute
LoginProviderSuccessRoute: typeof LoginProviderSuccessRoute
LoginLdapIndexRoute: typeof LoginLdapIndexRoute
LoginSelectOrganizationIndexRoute: typeof LoginSelectOrganizationIndexRoute
LoginSsoIndexRoute: typeof LoginSsoIndexRoute
SignupSsoIndexRoute: typeof SignupSsoIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LoginIndexRoute: LoginIndexRoute,
SignupIndexRoute: SignupIndexRoute,
LoginProviderErrorRoute: LoginProviderErrorRoute,
LoginProviderSuccessRoute: LoginProviderSuccessRoute,
LoginLdapIndexRoute: LoginLdapIndexRoute,
LoginSelectOrganizationIndexRoute: LoginSelectOrganizationIndexRoute,
LoginSsoIndexRoute: LoginSsoIndexRoute,
SignupSsoIndexRoute: SignupSsoIndexRoute,
}
export const routeTree = rootRoute
@@ -216,11 +260,13 @@ export const routeTree = rootRoute
"children": [
"/",
"/login/",
"/signup/",
"/login/provider/error",
"/login/provider/success",
"/login/ldap/",
"/login/select-organization/",
"/login/sso/"
"/login/sso/",
"/signup/sso/"
]
},
"/": {
@@ -229,6 +275,9 @@ export const routeTree = rootRoute
"/login/": {
"filePath": "login/index.tsx"
},
"/signup/": {
"filePath": "signup/index.tsx"
},
"/login/provider/error": {
"filePath": "login/provider/error.tsx"
},
@@ -243,6 +292,9 @@ export const routeTree = rootRoute
},
"/login/sso/": {
"filePath": "login/sso/index.tsx"
},
"/signup/sso/": {
"filePath": "signup/sso/index.tsx"
}
}
}

View File

@@ -0,0 +1,174 @@
/* eslint-disable no-nested-ternary */
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import CodeInputStep from "@app/components/signup/CodeInputStep";
import DownloadBackupPDF from "@app/components/signup/DonwloadBackupPDFStep";
import EnterEmailStep from "@app/components/signup/EnterEmailStep";
import InitialSignupStep from "@app/components/signup/InitialSignupStep";
import TeamInviteStep from "@app/components/signup/TeamInviteStep";
import UserInfoStep from "@app/components/signup/UserInfoStep";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { useServerConfig } from "@app/context";
import { useVerifySignupEmailVerificationCode } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { ProjectType } from "@app/hooks/api/workspace/types";
/**
* @returns the signup page
*/
const SignUpPage = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [organizationName, setOrganizationName] = useState("");
const [attributionSource, setAttributionSource] = useState("");
const [code, setCode] = useState("123456");
const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1);
const navigate = useNavigate();
const { data: serverDetails } = useFetchServerStatus();
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
const { t } = useTranslation();
const { mutateAsync } = useVerifySignupEmailVerificationCode();
const { config } = useServerConfig();
useEffect(() => {
if (!config.allowSignUp) {
navigate({ to: "/login" });
}
}, [config.allowSignUp]);
useEffect(() => {
const tryAuth = async () => {
try {
const userOrgs = await fetchOrganizations();
navigate({ to: `/org/${userOrgs[0].id}/${ProjectType.SecretManager}/overview` });
} catch {
console.log("Error - Not logged in yet");
}
};
tryAuth();
}, []);
/**
* Goes to the following step (out of 5) of the signup process.
* Step 1 is submitting your email
* Step 2 is Verifying your email with the code that you received
* Step 3 is asking the final info.
* Step 4 is downloading a backup pdf
* Step 5 is inviting users
*/
const incrementStep = async () => {
if (step === 1 || step === 3 || step === 4) {
setStep(step + 1);
} else if (step === 2) {
setIsCodeInputCheckLoading(true);
// Checking if the code matches the email.
try {
const { token } = await mutateAsync({ email, code });
SecurityClient.setSignupToken(token);
setStep(3);
} catch (err) {
console.error(err);
setCodeError(true);
}
setIsCodeInputCheckLoading(false);
}
};
// when email service is not configured, skip step 2 and 5
useEffect(() => {
(async () => {
if (!serverDetails?.emailConfigured && step === 2) {
incrementStep();
}
if (!serverDetails?.emailConfigured && step === 5) {
const userOrgs = await fetchOrganizations();
navigate({ to: `/org/${userOrgs[0].id}/${ProjectType.SecretManager}/overview` });
}
})();
}, [step]);
const renderView = (registerStep: number) => {
if (isSignupWithEmail && registerStep === 1) {
return <EnterEmailStep email={email} setEmail={setEmail} incrementStep={incrementStep} />;
}
if (!isSignupWithEmail && registerStep === 1) {
return <InitialSignupStep setIsSignupWithEmail={setIsSignupWithEmail} />;
}
if (registerStep === 2) {
return (
<CodeInputStep
email={email}
incrementStep={incrementStep}
setCode={setCode}
codeError={codeError}
isCodeInputCheckLoading={isCodeInputCheckLoading}
/>
);
}
if (registerStep === 3) {
return (
<UserInfoStep
incrementStep={incrementStep}
email={email}
password={password}
setPassword={setPassword}
name={name}
setName={setName}
organizationName={organizationName}
setOrganizationName={setOrganizationName}
attributionSource={attributionSource}
setAttributionSource={setAttributionSource}
providerAuthToken={undefined}
/>
);
}
if (registerStep === 4) {
return (
<DownloadBackupPDF
incrementStep={incrementStep}
email={email}
password={password}
name={name}
/>
);
}
if (serverDetails?.emailConfigured) {
return <TeamInviteStep />;
}
return "";
};
return (
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Helmet>
<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} />
</Helmet>
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
<form onSubmit={(e) => e.preventDefault()}>{renderView(step)}</form>
</div>
);
};
export const Route = createFileRoute("/signup/")({
component: SignUpPage
});

View File

@@ -0,0 +1,71 @@
import { useTranslation } from "react-i18next";
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";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { useNavigate } from "@tanstack/react-router";
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 navigate = useNavigate();
return (
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
<p className="flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
{t("signup.step4-message")}
</p>
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
<span className="mb-2">
{t("signup.step4-description1")} {t("signup.step4-description3")}
</span>
</div>
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
<div className="text-l w-full py-1 text-lg">
<Button
onClick={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: () => {},
setBackupKeyIssued: () => {}
});
navigate({
to: `/org/${localStorage.getItem("orgData.id")}/${ProjectType.SecretManager}/overview`
});
}}
size="sm"
isFullWidth
className="h-12"
colorSchema="primary"
variant="outline_bg"
>
{" "}
Download PDF{" "}
</Button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { BackupPDFStep } from "./BackupPDFStep";

View File

@@ -0,0 +1,189 @@
// confirm email
// if same email exists, then trigger fn to merge automatically
import { useState } from "react";
import ReactCodeInput from "react-code-input";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "@app/hooks/api";
import { UserAliasType } from "@app/hooks/api/users/types";
import { useNavigate } from "@tanstack/react-router";
type Props = {
authType?: UserAliasType;
username: string;
email: string;
organizationSlug: string;
setStep: (step: number) => void;
};
// The style for the verification code input
const props = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "55px",
borderRadius: "5px",
fontSize: "24px",
height: "55px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
const propsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
export const EmailConfirmationStep = ({
authType,
username,
email,
organizationSlug,
setStep
}: Props) => {
const navigate = useNavigate();
const [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false);
const [isResendingVerificationEmail] = useState(false);
const [isLoading] = useState(false);
const { mutateAsync: sendEmailVerificationCode } = useSendEmailVerificationCode();
const { mutateAsync: verifyEmailVerificationCode } = useVerifyEmailVerificationCode();
const checkCode = async () => {
try {
await verifyEmailVerificationCode({ username, code });
setCodeError(false);
createNotification({
text: "Successfully verified code",
type: "success"
});
switch (authType) {
case UserAliasType.SAML: {
window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`);
window.close();
break;
}
case UserAliasType.LDAP: {
navigate({ to: `/login/ldap?organizationSlug=${organizationSlug}` });
break;
}
case UserAliasType.OIDC: {
navigate({ to: `/api/v1/sso/oidc/login?orgSlug=${organizationSlug}` });
break;
}
default: {
setStep(1);
break;
}
}
} catch {
createNotification({
text: "Failed to verify code",
type: "error"
});
}
setCode("");
};
const resendCode = async () => {
try {
await sendEmailVerificationCode(username);
createNotification({
text: "Successfully resent code",
type: "success"
});
} catch {
createNotification({
text: "Failed to resend code",
type: "error"
});
}
};
return (
<div className="mx-auto h-full w-full pb-4 md:px-8">
<p className="text-md flex justify-center text-bunker-200">
We&apos;ve sent a verification code to {email}
</p>
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...props}
className="mb-2 mt-6"
/>
</div>
<div className="mx-auto mt-4 block w-max md:hidden">
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setCode}
{...propsPhone}
className="mb-2 mt-2"
/>
</div>
{codeError && <Error text="Oops. Your code is wrong. Please try again." />}
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
<div className="text-l w-full py-1 text-lg">
<Button
type="submit"
onClick={checkCode}
size="sm"
isFullWidth
className="h-14"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
>
{" "}
Verify
</Button>
</div>
</div>
<div className="mx-auto flex max-h-24 w-full max-w-md flex-col items-center justify-center pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">Don&apos;t see the code?</span>
<div className="text-md mt-2 flex flex-row text-bunker-400">
<button disabled={isLoading} onClick={resendCode} type="button">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{isResendingVerificationEmail ? "Resending..." : "Resend"}
</span>
</button>
</div>
</div>
<p className="pb-2 text-sm text-bunker-400">Make sure to check your spam inbox.</p>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { EmailConfirmationStep } from "./EmailConfirmationStep";

View File

@@ -0,0 +1,324 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
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 { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { Mfa } from "@app/routes/login/-components/Mfa";
import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Props = {
setStep: (step: number) => void;
username: string;
password: string;
setPassword: (value: string) => void;
name: string;
providerOrganizationName: string;
providerAuthToken?: 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 = ({
username,
name,
providerOrganizationName,
password,
setPassword,
setStep,
providerAuthToken
}: Props) => {
const [nameError, setNameError] = useState(false);
const [organizationName, setOrganizationName] = useState("");
const [organizationNameError, setOrganizationNameError] = useState(false);
const [attributionSource, setAttributionSource] = useState("");
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex");
setPassword(randomPassword);
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);
}
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,
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 completeAccountSignup({
email: username,
password,
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 fetchOrganizations();
const orgId = userOrgs[0]?.id;
const completeSignupFlow = async () => {
try {
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
// only create example project if not joining existing org
if (!providerOrganizationName) {
const project = await ProjectService.initProject({
projectName: "Example Project"
});
localStorage.setItem("projectData.id", project.id);
}
localStorage.setItem("orgData.id", orgId);
setStep(2);
} catch (error) {
setIsLoading(false);
console.error(error);
}
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}
);
} else {
setIsLoading(false);
}
};
useEffect(() => {
if (password && providerOrganizationName) {
signupErrorCheck();
}
}, [providerOrganizationName, password]);
if (shouldShowMfa) {
return (
<Mfa
hideLogo
email={username}
successCallback={mfaSuccessCallback}
method={requiredMfaMethod}
closeMfa={() => toggleShowMfa.off()}
/>
);
}
return (
<div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
<p className="text-medium mx-8 mb-6 flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-xl font-bold text-transparent md:mx-16">
{t("signup.step3-message")}
</p>
<div className="mx-auto mb-36 h-full w-max rounded-xl py-6 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800 md:px-8">
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Your Name
</p>
<Input
placeholder="Jane Doe"
value={name}
disabled
isRequired
autoComplete="given-name"
className="h-12"
/>
{nameError && (
<p className="ml-1 mt-1 w-full text-left text-xs text-red-600">
Please, specify your name
</p>
)}
</div>
{providerOrganizationName === undefined && (
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
</p>
<Input
placeholder="Infisical"
value={organizationName}
onChange={(e) => setOrganizationName(e.target.value)}
isRequired
className="h-12"
maxLength={64}
disabled
/>
{organizationNameError && (
<p className="ml-1 mt-1 w-full text-left text-xs text-red-600">
Please, specify your organization name
</p>
)}
</div>
)}
{providerOrganizationName === undefined && (
<div className="relative z-0 flex w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
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="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
<div className="text-l w-full py-1 text-lg">
<Button
type="submit"
onClick={signupErrorCheck}
size="sm"
isFullWidth
className="h-12"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
isDisabled={isLoading}
>
{" "}
{String(t("signup.signup"))}{" "}
</Button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { UserInfoSSOStep } from "./UserInfoSSOStep";

View File

@@ -0,0 +1,90 @@
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { createFileRoute, useSearch } from "@tanstack/react-router";
import { jwtDecode } from "jwt-decode";
import { BackupPDFStep } from "./-components/BackupPDFStep";
import { EmailConfirmationStep } from "./-components/EmailConfirmationStep";
import { UserInfoSSOStep } from "./-components/UserInfoSSOStep";
const SignupSSOPage = () => {
const { t } = useTranslation();
const search = useSearch({ from: "/signup/sso/" });
const token = search.token as string;
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const {
username,
email,
organizationName,
organizationSlug,
firstName,
lastName,
authType,
isEmailVerified
} = jwtDecode(token) as any;
useEffect(() => {
if (!isEmailVerified) {
setStep(0);
} else {
setStep(1);
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return (
<EmailConfirmationStep
authType={authType}
username={username}
email={email}
organizationSlug={organizationSlug}
setStep={setStep}
/>
);
case 1:
return (
<UserInfoSSOStep
username={username}
name={`${firstName} ${lastName}`}
providerOrganizationName={organizationName}
password={password}
setPassword={setPassword}
setStep={setStep}
providerAuthToken={token}
/>
);
case 2:
return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
);
default:
return <div />;
}
};
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">
<Helmet>
<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} />
</Helmet>
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
<div>{renderView()}</div>;
</div>
);
};
export const Route = createFileRoute("/signup/sso/")({
component: SignupSSOPage
});