feat: first login page base completed

This commit is contained in:
=
2024-12-14 19:22:50 +05:30
parent 234e7eb9be
commit 3d03fece74
58 changed files with 4268 additions and 47 deletions

View File

@@ -49,7 +49,7 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react-refresh/only-export-components": "off",
"@typescript-eslint/no-empty-function": "off",
quotes: ["error", "double", { avoidEscape: true }],
"comma-dangle": ["error", "only-multiline"],

File diff suppressed because it is too large Load Diff

View File

@@ -72,6 +72,7 @@
"react-code-input": "^3.10.1",
"react-day-picker": "^9.4.3",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.54.0",
"react-i18next": "^15.2.0",
"react-icons": "^5.4.0",
@@ -101,6 +102,7 @@
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
@@ -119,6 +121,10 @@
"tailwindcss": "^3.4.16",
"typescript": "~5.6.2",
"typescript-eslint": "^8.15.0",
"vite": "^6.0.1"
"vite": "^6.0.1",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0",
"vite-tsconfig-paths": "^5.1.4"
}
}

View File

@@ -1,4 +1,9 @@
import { getAuthToken, setAuthToken, setMfaTempToken, setSignupTempToken } from "@app/reactQuery";
import {
getAuthToken,
setAuthToken,
setMfaTempToken,
setSignupTempToken
} from "@app/hooks/api/reactQuery";
export const PROVIDER_AUTH_TOKEN_KEY = "infisical__provider-auth-token";

View File

@@ -1,10 +1,9 @@
import argon2 from "argon2-browser";
import argon2 from "argon2-browser/dist/argon2-bundled.min.js";
import nacl from "tweetnacl";
import { encodeBase64, decodeBase64, encodeUTF8, decodeUTF8 } from "tweetnacl-util";
import aes from "./aes-256-gcm";
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
/**
* Return new base64, NaCl, public-private key pair.
* @returns {Object} obj
@@ -15,8 +14,8 @@ const generateKeyPair = () => {
const pair = nacl.box.keyPair();
return {
publicKey: nacl.util.encodeBase64(pair.publicKey),
privateKey: nacl.util.encodeBase64(pair.secretKey)
publicKey: encodeBase64(pair.publicKey),
privateKey: encodeBase64(pair.secretKey)
};
};
@@ -34,8 +33,8 @@ type EncryptAsymmetricProps = {
* @param {String} - base64-encoded Nacl public key
*/
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
const derivedPublicKey = nacl.util.encodeBase64(
nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey
const derivedPublicKey = encodeBase64(
nacl.box.keyPair.fromSecretKey(decodeBase64(privateKey)).publicKey
);
if (derivedPublicKey !== publicKey) {
@@ -108,15 +107,15 @@ const encryptAssymmetric = ({
} => {
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
nacl.util.decodeUTF8(plaintext),
decodeUTF8(plaintext),
nonce,
nacl.util.decodeBase64(publicKey),
nacl.util.decodeBase64(privateKey)
decodeBase64(publicKey),
decodeBase64(privateKey)
);
return {
ciphertext: nacl.util.encodeBase64(ciphertext),
nonce: nacl.util.encodeBase64(nonce)
ciphertext: encodeBase64(ciphertext),
nonce: encodeBase64(nonce)
};
};
@@ -143,13 +142,13 @@ const decryptAssymmetric = ({
privateKey
}: DecryptAsymmetricProps): string => {
const plaintext = nacl.box.open(
nacl.util.decodeBase64(ciphertext),
nacl.util.decodeBase64(nonce),
nacl.util.decodeBase64(publicKey),
nacl.util.decodeBase64(privateKey)
decodeBase64(ciphertext),
decodeBase64(nonce),
decodeBase64(publicKey),
decodeBase64(privateKey)
);
return nacl.util.encodeUTF8(plaintext);
return encodeUTF8(plaintext);
};
type EncryptSymmetricProps = {

View File

@@ -71,7 +71,7 @@ export const MenuItem = <T extends ElementType = "button">({
lottieRef={iconRef}
style={{ width: 22, height: 22 }}
// eslint-disable-next-line import/no-dynamic-require
animationData={require(`../../../../public/lotties/${icon}.json`)}
// animationData={require(`../../../../public/lotties/${icon}.json`)}
loop={false}
autoplay={false}
className="my-auto ml-[0.1rem] mr-3"
@@ -121,7 +121,7 @@ export const SubMenuItem = <T extends ElementType = "button">({
lottieRef={iconRef}
style={{ width: 16, height: 16 }}
// eslint-disable-next-line import/no-dynamic-require
animationData={require(`../../../../public/lotties/${icon}.json`)}
// animationData={require(`../../../../public/lotties/${icon}.json`)}
loop={false}
autoplay={false}
className="my-auto ml-[0.1rem] mr-3"

View File

@@ -3,8 +3,9 @@ import axios from "axios";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { getAuthToken, getMfaTempToken, getSignupTempToken } from "@app/hooks/api/reactQuery";
// TODO(rbr): update this later
export const apiRequest = axios.create({
baseURL: "/",
baseURL: "http://localhost:8080",
headers: {
"Content-Type": "application/json"
}

View File

@@ -0,0 +1,33 @@
import { ReactNode } from "react";
import { useGetAuthToken } from "@app/hooks/api";
type Props = {
children: ReactNode;
};
// TODO(akhilmhdh): Using react-simple-animate from hard dom offloading
// smoother dom offloading needs to be done
// Authentication controller
// Does route checking
// Provide a context for whole app to notify user is authorized or not
export const AuthProvider = ({ children }: Props): JSX.Element => {
const { isLoading } = useGetAuthToken();
// wait for app to load the auth state
if (isLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="infisical loading indicator"
/>
</div>
);
}
return children as JSX.Element;
};

View File

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

View File

@@ -0,0 +1,58 @@
import { createContext, ReactNode, useContext } from "react";
import { useGetUserOrgPermissions } from "@app/hooks/api";
import { OrgUser } from "@app/hooks/api/types";
import { useOrganization } from "../OrganizationContext";
import { TOrgPermission } from "./types";
type Props = {
children: ReactNode;
};
const OrgPermissionContext = createContext<null | {
permission: TOrgPermission;
membership: OrgUser | null;
}>(null);
export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: permission, isLoading } = useGetUserOrgPermissions({ orgId });
if (isLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="infisical loading indicator"
/>
</div>
);
}
if (!permission) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
Failed to load user permissions
</div>
);
}
return (
<OrgPermissionContext.Provider value={permission}>{children}</OrgPermissionContext.Provider>
);
};
export const useOrgPermission = () => {
const ctx = useContext(OrgPermissionContext);
if (!ctx) {
throw new Error("useOrgPermission to be used within <OrgPermissionProvider>");
}
return ctx;
};

View File

@@ -0,0 +1,3 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export { OrgPermissionActions, OrgPermissionSubjects } from "./types";

View File

@@ -0,0 +1,52 @@
import { MongoAbility } from "@casl/ability";
export enum OrgPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
Member = "member",
Settings = "settings",
IncidentAccount = "incident-contact",
Scim = "scim",
Sso = "sso",
Ldap = "ldap",
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;

View File

@@ -0,0 +1,44 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useGetOrganizations } from "@app/hooks/api";
import { Organization } from "@app/hooks/api/types";
type TOrgContext = {
orgs?: Organization[];
currentOrg?: Organization;
isLoading: boolean;
};
const OrgContext = createContext<TOrgContext | null>(null);
type Props = {
children: ReactNode;
};
export const OrgProvider = ({ children }: Props): JSX.Element => {
const { data: userOrgs, isLoading } = useGetOrganizations();
// const currentWsOrgID = currentWorkspace?.organization;
const currentWsOrgID = localStorage.getItem("orgData.id");
// memorize the workspace details for the context
const value = useMemo<TOrgContext>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID),
isLoading
}),
[currentWsOrgID, userOrgs, isLoading]
);
return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>;
};
export const useOrganization = () => {
const ctx = useContext(OrgContext);
if (!ctx) {
throw new Error("useOrganization to be used within <OrgContext.Provider>");
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { OrgProvider, useOrganization } from "./OrganizationContext";

View File

@@ -0,0 +1,62 @@
import { createContext, ReactNode, useContext } from "react";
import { useGetUserProjectPermissions } from "@app/hooks/api";
import { TProjectMembership } from "@app/hooks/api/users/types";
import { useWorkspace } from "../WorkspaceContext";
import { TProjectPermission } from "./types";
type Props = {
children: ReactNode;
};
const ProjectPermissionContext = createContext<null | {
permission: TProjectPermission;
membership: TProjectMembership;
}>(null);
export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentWorkspace, isLoading: isWsLoading } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId });
if ((isLoading && currentWorkspace) || isWsLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="infisical loading indicator"
/>
</div>
);
}
if (!permission && currentWorkspace) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
Failed to load user permissions
</div>
);
}
return (
<ProjectPermissionContext.Provider value={permission!}>
{children}
</ProjectPermissionContext.Provider>
);
};
export const useProjectPermission = () => {
const ctx = useContext(ProjectPermissionContext);
if (!ctx) {
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
}
const hasProjectRole = (role: string) => ctx?.membership?.roles?.includes(role) || false;
return { ...ctx, hasProjectRole };
};

View File

@@ -0,0 +1,8 @@
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types";
export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub
} from "./types";

View File

@@ -0,0 +1,176 @@
import { ForcedSubject, MongoAbility } from "@casl/ability";
export enum ProjectPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential",
EditRootCredential = "edit-root-credential",
DeleteRootCredential = "delete-root-credential",
Lease = "lease"
}
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
Encrypt = "encrypt",
Decrypt = "decrypt"
}
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}
export type IdentityManagementSubjectFields = {
identityId: string;
};
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "contains",
[PermissionConditionOperators.$ALL]: "contains all",
[PermissionConditionOperators.$NEQ]: "not equal to",
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
[PermissionConditionOperators.$REGEX]: "matches regex pattern"
};
export type TPermissionConditionOperators = {
[PermissionConditionOperators.$IN]: string[];
[PermissionConditionOperators.$ALL]: string[];
[PermissionConditionOperators.$EQ]: string;
[PermissionConditionOperators.$NEQ]: string;
[PermissionConditionOperators.$REGEX]: string;
[PermissionConditionOperators.$GLOB]: string;
};
export type TPermissionCondition = Record<
string,
| string
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string }
>;
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Groups = "groups",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
ServiceTokens = "service-tokens",
Environments = "environments",
Tags = "tags",
AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist",
Project = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretImports = "secret-imports",
DynamicSecrets = "dynamic-secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
Cmek = "cmek"
}
export type SecretSubjectFields = {
environment: string;
secretPath: string;
secretName: string;
secretTags: string[];
};
export type SecretFolderSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = {
environment: string;
secretPath: string;
};
export type SecretImportSubjectFields = {
environment: string;
secretPath: string;
};
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretFolders
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
)
]
| [
ProjectPermissionDynamicSecretActions,
(
| ProjectPermissionSub.DynamicSecrets
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.Identity
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -0,0 +1,89 @@
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
import { ContentLoader } from "@app/components/v2/ContentLoader";
import { useGetServerConfig } from "@app/hooks/api";
import { TServerConfig } from "@app/hooks/api/admin/types";
import { Helmet } from "react-helmet";
import { useNavigate } from "@tanstack/react-router";
type TServerConfigContext = {
config: TServerConfig;
};
const ServerConfigContext = createContext<TServerConfigContext | null>(null);
type Props = {
children: ReactNode;
};
export const ServerConfigProvider = ({ children }: Props): JSX.Element => {
const navigate = useNavigate();
const { data, isLoading } = useGetServerConfig();
// memorize the workspace details for the context
const value = useMemo<TServerConfigContext>(() => {
return {
config: data!
};
}, [data]);
useEffect(() => {
if (!isLoading && data && !data.initialized && !data.isMigrationModeOn) {
navigate({ to: "/admin/signup" });
}
}, [isLoading, data]);
if (!isLoading && data?.isMigrationModeOn) {
return (
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center space-y-8 bg-bunker-800 px-8 text-mineshaft-50 dark:[color-scheme:dark]">
<Helmet>
<title>Infisical Maintenance Mode</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<img
src="/images/maintenance.png"
height={175}
width={300}
alt="maintenance mode"
className="w-[40rem]"
/>
<p className="mx-8 mb-4 flex justify-center bg-gradient-to-tr from-mineshaft-300 to-white bg-clip-text text-4xl font-bold text-transparent md:mx-16">
Scheduled Maintenance
</p>
<div className="mt-2 text-center text-lg text-bunker-300">
Infisical is undergoing planned maintenance. <br /> No action is required on your end —
your applications will continue to fetch secrets.
<br /> If you have questions, please{" "}
<a
className="text-bunker-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
>
join our Slack community
</a>
.
</div>
</div>
);
}
if (isLoading) {
return (
<div className="bg-bunker-800">
<ContentLoader text="Loading configurations" />
</div>
);
}
return <ServerConfigContext.Provider value={value}>{children}</ServerConfigContext.Provider>;
};
export const useServerConfig = () => {
const ctx = useContext(ServerConfigContext);
if (!ctx) {
throw new Error("useServerConfig has to be used within <UserContext.Provider>");
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { ServerConfigProvider, useServerConfig } from "./ServerConfigContext";

View File

@@ -0,0 +1,45 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useGetOrgSubscription } from "@app/hooks/api";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { useOrganization } from "../OrganizationContext";
type TSubscriptionContext = {
subscription?: SubscriptionPlan;
isLoading: boolean;
};
const SubscriptionContext = createContext<TSubscriptionContext | null>(null);
type Props = {
children: ReactNode;
};
export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgSubscription({
orgID: currentOrg?.id || ""
});
// memorize the workspace details for the context
const value = useMemo<TSubscriptionContext>(
() => ({
subscription: data,
isLoading
}),
[data, isLoading]
);
return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;
};
export const useSubscription = () => {
const ctx = useContext(SubscriptionContext);
if (!ctx) {
throw new Error("useSubscription has to be used within <SubscriptionContext.Provider>");
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";

View File

@@ -0,0 +1,53 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useGetUser } from "@app/hooks/api";
import { User, UserEnc } from "@app/hooks/api/types";
type TUserContext = {
user: User & UserEnc;
isLoading: boolean;
};
const UserContext = createContext<TUserContext | null>(null);
type Props = {
children: ReactNode;
};
export const UserProvider = ({ children }: Props): JSX.Element => {
const { data, isLoading } = useGetUser();
// memorize the workspace details for the context
const value = useMemo<TUserContext>(() => {
return {
user: data!,
isLoading
};
}, [data, isLoading]);
if (isLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="infisical loading indicator"
/>
</div>
);
}
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) {
throw new Error("useUser has to be used within <UserContext.Provider>");
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { UserProvider, useUser } from "./UserContext";

View File

@@ -0,0 +1,55 @@
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
import { createNotification } from "@app/components/notifications";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/workspace/types";
import { useParams } from "@tanstack/react-router";
type TWorkspaceContext = {
workspaces: Workspace[];
currentWorkspace?: Workspace;
isLoading: boolean;
};
const WorkspaceContext = createContext<TWorkspaceContext | null>(null);
type Props = {
children: ReactNode;
};
export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
const { data: ws, isLoading } = useGetUserWorkspaces();
const params = useParams({ strict: false });
const workspaceId = params.id;
// memorize the workspace details for the context
const value = useMemo<TWorkspaceContext>(() => {
const wsId = workspaceId || localStorage.getItem("projectData.id");
return {
workspaces: ws || [],
currentWorkspace: (ws || []).find(({ id }) => id === wsId),
isLoading
};
}, [ws, workspaceId, isLoading]);
const shouldTriggerNoProjectAccess = !value.isLoading && !value.currentWorkspace;
if (shouldTriggerNoProjectAccess) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800 text-primary-50">
You do not have sufficient access to this project.
</div>
);
}
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;
};
export const useWorkspace = () => {
const ctx = useContext(WorkspaceContext);
if (!ctx) {
throw new Error("useWorkspace has to be used within <WorkspaceContext.Provider>");
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";

View File

@@ -0,0 +1,22 @@
export { AuthProvider } from "./AuthContext";
export { OrgProvider, useOrganization } from "./OrganizationContext";
export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgPermissionActions,
OrgPermissionProvider,
OrgPermissionSubjects,
useOrgPermission
} from "./OrgPermissionContext";
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionProvider,
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";
export { ServerConfigProvider, useServerConfig } from "./ServerConfigContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
export { UserProvider, useUser } from "./UserContext";
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";

View File

@@ -0,0 +1,84 @@
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
/**
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
*/
const decryptPrivateKeyHelper = async ({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) => {
let privateKey;
try {
if (encryptionVersion === 1) {
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
});
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
const derivedKey = await deriveArgonKey({
password,
salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to generate derived key");
const key = Aes256Gcm.decrypt({
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag,
secret: Buffer.from(derivedKey.hash)
});
// decrypt back the private key
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: Buffer.from(key, "hex")
});
} else {
throw new Error("Insufficient details to decrypt private key");
}
} catch (err) {
throw new Error("Failed to decrypt private key");
}
return privateKey;
};
export { decryptPrivateKeyHelper };

View File

@@ -0,0 +1,12 @@
import { TWorkspaceUser } from "@app/hooks/api/users/types";
export const getMemberLabel = (member: TWorkspaceUser) => {
const {
inviteEmail,
user: { firstName, lastName, username, email }
} = member;
return firstName || lastName
? `${firstName ?? ""} ${lastName ?? ""}`.trim()
: username || email || inviteEmail;
};

View File

@@ -0,0 +1,31 @@
/** Extracts the key and value from a passed in env string based on the provided delimiters. */
export const getKeyValue = (pastedContent: string, delimiters: string[]) => {
if (!pastedContent) {
return { key: "", value: "" };
}
let firstDelimiterIndex = -1;
let foundDelimiter = "";
delimiters.forEach((delimiter) => {
const index = pastedContent.indexOf(delimiter);
if (index !== -1 && (firstDelimiterIndex === -1 || index < firstDelimiterIndex)) {
firstDelimiterIndex = index;
foundDelimiter = delimiter;
}
});
const hasValueAfterDelimiter = pastedContent.length > firstDelimiterIndex + foundDelimiter.length;
if (firstDelimiterIndex === -1 || !hasValueAfterDelimiter) {
return { key: pastedContent.trim(), value: "" };
}
const key = pastedContent.substring(0, firstDelimiterIndex);
const value = pastedContent.substring(firstDelimiterIndex + foundDelimiter.length);
return {
key: key.trim(),
value: value.trim()
};
};

View File

@@ -0,0 +1,12 @@
import { PolicyType } from "@app/hooks/api/policies/enums";
export const policyDetails: Record<PolicyType, { name: string; className: string }> = {
[PolicyType.AccessPolicy]: {
className: "bg-lime-900 text-lime-100",
name: "Access Policy"
},
[PolicyType.ChangePolicy]: {
className: "bg-indigo-900 text-indigo-100",
name: "Change Policy"
}
};

View File

@@ -0,0 +1,62 @@
import { apiRequest } from "@app/config/request";
import { createWorkspace } from "@app/hooks/api/workspace/queries";
const secretsToBeAdded = [
{
secretKey: "DATABASE_URL",
// eslint-disable-next-line no-template-curly-in-string
secretValue: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
secretComment: "Secret referencing example"
},
{
secretKey: "DB_USERNAME",
secretValue: "OVERRIDE_THIS",
secretComment: "Override secrets with personal value"
},
{
secretKey: "DB_PASSWORD",
secretValue: "OVERRIDE_THIS",
secretComment: "Another secret override"
},
{
secretKey: "DB_PASSWORD",
secretValue: "example_password"
},
{
secretKey: "TWILIO_AUTH_TOKEN",
secretValue: "example_twillio_token"
},
{
secretKey: "WEBSITE_URL",
secretValue: "http://localhost:3000"
}
];
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
*/
const initProjectHelper = async ({ projectName }: { projectName: string }) => {
// create new project
const {
data: { project }
} = await createWorkspace({
projectName
});
try {
const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", {
workspaceId: project.id,
environment: "dev",
secretPath: "/",
secrets: secretsToBeAdded
});
return data;
} catch (err) {
console.error("Failed to upload secrets", err);
}
return project;
};
export { initProjectHelper };

View File

@@ -0,0 +1,5 @@
export const reverseTruncate = (text: string, maxLength = 42) => {
if (text.length < maxLength) return text;
return `...${text.substring(text.length - maxLength + 3)}`;
};

View File

@@ -0,0 +1,30 @@
import { ProjectMembershipRole, TOrgRole } from "@app/hooks/api/roles/types";
enum OrgMembershipRole {
Admin = "admin",
Member = "member",
NoAccess = "no-access"
}
enum ProjectMemberRole {
Admin = "admin",
Member = "member",
Viewer = "viewer",
NoAccess = "no-access"
}
export const isCustomOrgRole = (slug: string) =>
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
export const formatProjectRoleName = (name: string) => {
if (name === ProjectMemberRole.Member) return "developer";
return name;
};
export const isCustomProjectRole = (slug: string) =>
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) =>
isCustomOrgRole(roleIdOrSlug)
? roles.find((r) => r.id === roleIdOrSlug)
: roles.find((r) => r.slug === roleIdOrSlug);

View File

@@ -0,0 +1,15 @@
export const removeTrailingSlash = (str: string) => {
if (str === "/") return str;
return str.endsWith("/") ? str.slice(0, -1) : str;
};
export const isValidPath = (val: string): boolean => {
if (val.length === 0) return false;
if (val === "/") return true;
// Check for valid characters and no consecutive slashes
const validPathRegex = /^[a-zA-Z0-9-_.:]+(?:\/[a-zA-Z0-9-_.:]+)*$/;
return validPathRegex.test(val);
};

View File

@@ -2,16 +2,16 @@ import { MutationCache, QueryClient } from "@tanstack/react-query";
import axios from "axios";
import { createNotification } from "@app/components/notifications";
// akhilmhdh: doing individual imports to avoid cyclic import error
import { Button } from "./components/v2/Button";
import { Modal, ModalContent, ModalTrigger } from "./components/v2/Modal";
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "./components/v2/Table";
import { Button } from "@app/components/v2/Button";
import { Modal, ModalContent, ModalTrigger } from "@app/components/v2/Modal";
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "@app/components/v2/Table";
import {
formatedConditionsOperatorNames,
PermissionConditionOperators
} from "./context/ProjectPermissionContext/types";
import { ApiErrorTypes, TApiErrors } from "./hooks/api/types";
} from "@app/context/ProjectPermissionContext/types";
import { ApiErrorTypes, TApiErrors } from "./types";
// this is saved in react-query cache
export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];

View File

@@ -12,6 +12,12 @@
import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as LoginIndexImport } from './routes/login/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'
import { Route as LoginProviderSuccessImport } from './routes/login/provider/success'
import { Route as LoginProviderErrorImport } from './routes/login/provider/error'
// Create/Update Routes
@@ -21,6 +27,43 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const LoginIndexRoute = LoginIndexImport.update({
id: '/login/',
path: '/login/',
getParentRoute: () => rootRoute,
} as any)
const LoginSsoIndexRoute = LoginSsoIndexImport.update({
id: '/login/sso/',
path: '/login/sso/',
getParentRoute: () => rootRoute,
} as any)
const LoginSelectOrganizationIndexRoute =
LoginSelectOrganizationIndexImport.update({
id: '/login/select-organization/',
path: '/login/select-organization/',
getParentRoute: () => rootRoute,
} as any)
const LoginLdapIndexRoute = LoginLdapIndexImport.update({
id: '/login/ldap/',
path: '/login/ldap/',
getParentRoute: () => rootRoute,
} as any)
const LoginProviderSuccessRoute = LoginProviderSuccessImport.update({
id: '/login/provider/success',
path: '/login/provider/success',
getParentRoute: () => rootRoute,
} as any)
const LoginProviderErrorRoute = LoginProviderErrorImport.update({
id: '/login/provider/error',
path: '/login/provider/error',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@@ -32,6 +75,48 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/login/': {
id: '/login/'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginIndexImport
parentRoute: typeof rootRoute
}
'/login/provider/error': {
id: '/login/provider/error'
path: '/login/provider/error'
fullPath: '/login/provider/error'
preLoaderRoute: typeof LoginProviderErrorImport
parentRoute: typeof rootRoute
}
'/login/provider/success': {
id: '/login/provider/success'
path: '/login/provider/success'
fullPath: '/login/provider/success'
preLoaderRoute: typeof LoginProviderSuccessImport
parentRoute: typeof rootRoute
}
'/login/ldap/': {
id: '/login/ldap/'
path: '/login/ldap'
fullPath: '/login/ldap'
preLoaderRoute: typeof LoginLdapIndexImport
parentRoute: typeof rootRoute
}
'/login/select-organization/': {
id: '/login/select-organization/'
path: '/login/select-organization'
fullPath: '/login/select-organization'
preLoaderRoute: typeof LoginSelectOrganizationIndexImport
parentRoute: typeof rootRoute
}
'/login/sso/': {
id: '/login/sso/'
path: '/login/sso'
fullPath: '/login/sso'
preLoaderRoute: typeof LoginSsoIndexImport
parentRoute: typeof rootRoute
}
}
}
@@ -39,32 +124,84 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof LoginIndexRoute
'/login/provider/error': typeof LoginProviderErrorRoute
'/login/provider/success': typeof LoginProviderSuccessRoute
'/login/ldap': typeof LoginLdapIndexRoute
'/login/select-organization': typeof LoginSelectOrganizationIndexRoute
'/login/sso': typeof LoginSsoIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginIndexRoute
'/login/provider/error': typeof LoginProviderErrorRoute
'/login/provider/success': typeof LoginProviderSuccessRoute
'/login/ldap': typeof LoginLdapIndexRoute
'/login/select-organization': typeof LoginSelectOrganizationIndexRoute
'/login/sso': typeof LoginSsoIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/login/': typeof LoginIndexRoute
'/login/provider/error': typeof LoginProviderErrorRoute
'/login/provider/success': typeof LoginProviderSuccessRoute
'/login/ldap/': typeof LoginLdapIndexRoute
'/login/select-organization/': typeof LoginSelectOrganizationIndexRoute
'/login/sso/': typeof LoginSsoIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fullPaths:
| '/'
| '/login'
| '/login/provider/error'
| '/login/provider/success'
| '/login/ldap'
| '/login/select-organization'
| '/login/sso'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
to:
| '/'
| '/login'
| '/login/provider/error'
| '/login/provider/success'
| '/login/ldap'
| '/login/select-organization'
| '/login/sso'
id:
| '__root__'
| '/'
| '/login/'
| '/login/provider/error'
| '/login/provider/success'
| '/login/ldap/'
| '/login/select-organization/'
| '/login/sso/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LoginIndexRoute: typeof LoginIndexRoute
LoginProviderErrorRoute: typeof LoginProviderErrorRoute
LoginProviderSuccessRoute: typeof LoginProviderSuccessRoute
LoginLdapIndexRoute: typeof LoginLdapIndexRoute
LoginSelectOrganizationIndexRoute: typeof LoginSelectOrganizationIndexRoute
LoginSsoIndexRoute: typeof LoginSsoIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LoginIndexRoute: LoginIndexRoute,
LoginProviderErrorRoute: LoginProviderErrorRoute,
LoginProviderSuccessRoute: LoginProviderSuccessRoute,
LoginLdapIndexRoute: LoginLdapIndexRoute,
LoginSelectOrganizationIndexRoute: LoginSelectOrganizationIndexRoute,
LoginSsoIndexRoute: LoginSsoIndexRoute,
}
export const routeTree = rootRoute
@@ -77,11 +214,35 @@ export const routeTree = rootRoute
"__root__": {
"filePath": "__root.tsx",
"children": [
"/"
"/",
"/login/",
"/login/provider/error",
"/login/provider/success",
"/login/ldap/",
"/login/select-organization/",
"/login/sso/"
]
},
"/": {
"filePath": "index.tsx"
},
"/login/": {
"filePath": "login/index.tsx"
},
"/login/provider/error": {
"filePath": "login/provider/error.tsx"
},
"/login/provider/success": {
"filePath": "login/provider/success.tsx"
},
"/login/ldap/": {
"filePath": "login/ldap/index.tsx"
},
"/login/select-organization/": {
"filePath": "login/select-organization/index.tsx"
},
"/login/sso/": {
"filePath": "login/sso/index.tsx"
}
}
}

View File

@@ -1,16 +1,21 @@
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { QueryClientProvider } from "@tanstack/react-query";
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { queryClient } from "@app/hooks/api/reactQuery";
import { ServerConfigProvider } from "@app/context";
import { TooltipProvider } from "@app/components/v2";
export const Route = createRootRoute({
component: () => (
<>
<div className="flex gap-2 p-2">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
</div>
<hr />
<Outlet />
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<ServerConfigProvider>
<Outlet />
</ServerConfigProvider>
</TooltipProvider>
</QueryClientProvider>
<TanStackRouterDevtools />
</>
)

View File

@@ -0,0 +1,408 @@
import { FormEvent, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "@tanstack/react-router";
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import Error from "@app/components/basic/Error";
import { RegionSelect } from "@app/components/navigation/RegionSelect";
import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import { Button, IconButton, Input, Tooltip } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api";
import { LoginMethod } from "@app/hooks/api/admin/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { useNavigateToSelectOrganization } from "../Login.utils";
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 navigate = useNavigate();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const [captchaToken, setCaptchaToken] = useState("");
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
const captchaRef = useRef<HCaptcha>(null);
const { data: serverDetails } = useFetchServerStatus();
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const redirectToSaml = (orgSlug: string) => {
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${orgSlug}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`;
navigate({ to: redirectUrl });
};
const redirectToOidc = (orgSlug: string) => {
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/oidc/login?orgSlug=${orgSlug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
navigate({ to: redirectUrl });
};
useEffect(() => {
if (serverDetails?.samlDefaultOrgSlug) redirectToSaml(serverDetails.samlDefaultOrgSlug);
}, [serverDetails?.samlDefaultOrgSlug]);
const handleSaml = () => {
if (config.defaultAuthOrgSlug) {
redirectToSaml(config.defaultAuthOrgSlug);
} else {
setStep(2);
}
};
const handleOidc = () => {
if (config.defaultAuthOrgSlug) {
redirectToOidc(config.defaultAuthOrgSlug);
} else {
setStep(3);
}
};
const shouldDisplayLoginMethod = (method: LoginMethod) =>
!config.enabledLoginMethods || config.enabledLoginMethods.includes(method);
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: email.toLowerCase(),
password,
captchaToken
});
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
navigateToSelectOrganization(callbackPort!);
} else {
setLoginError(true);
createNotification({
text: "CLI login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password,
captchaToken
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
navigateToSelectOrganization();
createNotification({
text: "Successfully logged in",
type: "success"
});
}
}
} catch (err: any) {
console.error(err);
if (err.response.data.error === "User Locked") {
createNotification({
title: err.response.data.error,
text: err.response.data.message,
type: "error"
});
setIsLoading(false);
return;
}
if (err.response.data.error === "Captcha Required") {
setShouldShowCaptcha(true);
setIsLoading(false);
return;
}
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
if (captchaRef.current) {
captchaRef.current.resetCaptcha();
}
setCaptchaToken("");
setIsLoading(false);
};
if (config.defaultAuthOrgAuthEnforced && config.defaultAuthOrgAuthMethod) {
return (
<form
onSubmit={handleLogin}
className="mx-auto flex w-full flex-col items-center justify-center"
>
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Login to Infisical
</h1>
<RegionSelect />
{config.defaultAuthOrgAuthMethod === AuthMethod.SAML && (
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={handleSaml}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with SAML
</Button>
</div>
)}
{config.defaultAuthOrgAuthMethod === AuthMethod.OIDC && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={handleOidc}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
)}
</form>
);
}
return (
<form
onSubmit={handleLogin}
className="mx-auto flex w-full flex-col items-center justify-center"
>
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Login to Infisical
</h1>
<RegionSelect />
{shouldDisplayLoginMethod(LoginMethod.SAML) && (
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={handleSaml}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with SAML
</Button>
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.OIDC) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={handleOidc}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.LDAP) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
navigate({ to: "/login/ldap" });
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with LDAP
</Button>
</div>
)}
<div className="mt-2 flex w-1/4 min-w-[21.2rem] gap-2 md:min-w-[20.1rem] lg:w-1/6">
{shouldDisplayLoginMethod(LoginMethod.GOOGLE) && (
<Tooltip position="bottom" content={t("login.continue-with-google")}>
<IconButton
ariaLabel={t("login.continue-with-google")}
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/google${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
window.close();
}}
className="h-10 w-full bg-mineshaft-600"
>
<FontAwesomeIcon icon={faGoogle} />
</IconButton>
</Tooltip>
)}
{shouldDisplayLoginMethod(LoginMethod.GITHUB) && (
<Tooltip position="bottom" content="Continue with GitHub">
<IconButton
ariaLabel="Login continue with GitHub"
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/github${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
window.close();
}}
className="h-10 w-full bg-mineshaft-600"
>
<FontAwesomeIcon icon={faGithub} />
</IconButton>
</Tooltip>
)}
{shouldDisplayLoginMethod(LoginMethod.GITLAB) && (
<Tooltip position="bottom" content="Continue with GitLab">
<IconButton
ariaLabel="Login continue with GitLab"
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/gitlab${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
window.close();
}}
className="h-10 w-full bg-mineshaft-600"
>
<FontAwesomeIcon icon={faGitlab} />
</IconButton>
</Tooltip>
)}
</div>
{(!config.enabledLoginMethods ||
(shouldDisplayLoginMethod(LoginMethod.EMAIL) && config.enabledLoginMethods.length > 1)) && (
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
<div className="w-full border-t border-mineshaft-400/60" />
<span className="mx-2 text-xs text-mineshaft-200">or</span>
<div className="w-full border-t border-mineshaft-400/60" />
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.EMAIL) && (
<>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-10"
/>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
{shouldShowCaptcha && (
<div className="mt-4">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
isLoading={isLoading}
>
{" "}
Continue with Email{" "}
</Button>
</div>
</>
)}
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{config.allowSignUp &&
(shouldDisplayLoginMethod(LoginMethod.EMAIL) ||
shouldDisplayLoginMethod(LoginMethod.GOOGLE) ||
shouldDisplayLoginMethod(LoginMethod.GITHUB) ||
shouldDisplayLoginMethod(LoginMethod.GITLAB)) ? (
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link href="/signup">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Don&apos;t have an account yet? {t("login.create-account")}
</span>
</Link>
</div>
) : (
<div className="mt-4" />
)}
{shouldDisplayLoginMethod(LoginMethod.EMAIL) && (
<div className="mt-2 flex flex-row text-sm text-bunker-400">
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Forgot password? Recover your account
</span>
</Link>
</div>
)}
</form>
);
};

View File

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

View File

@@ -0,0 +1,51 @@
import { NavigateFn, useNavigate } from "@tanstack/react-router";
import { useServerConfig } from "@app/context";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { queryClient } from "@app/hooks/api/reactQuery";
import { userKeys } from "@app/hooks/api/users";
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => {
const userOrgs = await fetchOrganizations();
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
if (organizationId) {
localStorage.setItem("orgData.id", organizationId);
navigate({ to: `/org/${organizationId}/overview` });
return;
}
if (nonAuthEnforcedOrgs.length > 0) {
// user is part of at least 1 non-auth enforced org
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
localStorage.setItem("orgData.id", userOrg);
navigate({ to: `/org/${userOrg}/overview` });
} else {
// user is not part of any non-auth enforced orgs
localStorage.removeItem("orgData.id");
navigate({ to: "/org/none" });
}
};
export const useNavigateToSelectOrganization = () => {
const { config } = useServerConfig();
const navigate = useNavigate();
const navigateToSelectOrganization = async (cliCallbackPort?: string) => {
let redirectTo = "/login/select-organization?";
if (config.defaultAuthOrgId) {
redirectTo += `org_id=${config.defaultAuthOrgId}&`;
} else {
queryClient.invalidateQueries(userKeys.getUser);
}
if (cliCallbackPort) {
redirectTo += `callback_port=${cliCallbackPort}`;
}
navigate({ to: redirectTo });
};
return { navigateToSelectOrganization };
};

View File

@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
import { jwtDecode } from "jwt-decode";
import { PasswordStep } from "./PasswordStep";
type Props = {
providerAuthToken: string;
};
export const LoginSSO = ({ providerAuthToken }: Props) => {
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const { username, isUserCompleted } = jwtDecode(providerAuthToken) as any;
useEffect(() => {
if (isUserCompleted) {
setStep(1);
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return <div />;
case 1:
return (
<PasswordStep
providerAuthToken={providerAuthToken}
email={username}
password={password}
setPassword={setPassword}
/>
);
default:
return <div />;
}
};
return <div>{renderView()}</div>;
};

View File

@@ -0,0 +1,222 @@
import React, { useEffect, useState } from "react";
import ReactCodeInput from "react-code-input";
import { Link, useNavigate } from "@tanstack/react-router";
import { t } from "i18next";
import Error from "@app/components/basic/Error";
import TotpRegistration from "@app/components/mfa/TotpRegistration";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useSendMfaToken } from "@app/hooks/api";
import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
// The style for the verification code input
const codeInputProps = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "48px",
borderRadius: "5px",
fontSize: "24px",
height: "48px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
}
} as const;
type Props = {
successCallback: () => void | Promise<void>;
closeMfa?: () => void;
hideLogo?: boolean;
email: string;
method: MfaMethod;
};
export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => {
const [mfaCode, setMfaCode] = useState("");
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingResend, setIsLoadingResend] = useState(false);
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false);
const sendMfaToken = useSendMfaToken();
useEffect(() => {
if (method === MfaMethod.TOTP) {
checkUserTotpMfa().then((isVerified) => {
if (!isVerified) {
SecurityClient.setMfaToken("");
setShouldShowTotpRegistration(true);
}
});
}
}, []);
const verifyMfa = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
try {
const { token } = await verifyMfaToken({
email,
mfaCode,
mfaMethod: method
});
SecurityClient.setMfaToken("");
SecurityClient.setToken(token);
await successCallback();
if (closeMfa) {
closeMfa();
}
} catch {
if (triesLeft) {
setTriesLeft((left) => {
if (triesLeft === 1) {
navigate({ to: "/" });
SecurityClient.setMfaToken("");
SecurityClient.setToken("");
}
return (left as number) - 1;
});
} else {
setTriesLeft(2);
}
} finally {
setIsLoading(false);
}
};
const handleResendMfaCode = async () => {
try {
setIsLoadingResend(true);
await sendMfaToken.mutateAsync({ email });
setIsLoadingResend(false);
} catch (err) {
console.error(err);
setIsLoadingResend(false);
}
};
if (shouldShowTotpRegistration) {
return (
<>
<div className="mb-6 text-center text-lg font-bold text-white">
Your organization requires mobile authentication to be configured.
</div>
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
<TotpRegistration
shouldCenterQr
onComplete={async () => {
setShouldShowTotpRegistration(false);
await successCallback();
}}
/>
</div>
</>
);
}
return (
<div className="mx-auto w-max pb-4 pt-4 md:mb-16 md:px-8">
{!hideLogo && (
<Link href="/">
<div className="mb-4 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
)}
{method === MfaMethod.EMAIL && (
<>
<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>
</>
)}
{method === MfaMethod.TOTP && (
<>
<p className="text-l mb-4 flex max-w-xs justify-center text-center font-bold text-bunker-100">
Authenticator MFA Required
</p>
<p className="text-l flex max-w-xs justify-center text-center text-bunker-300">
Open the authenticator app on your mobile device to get your verification code or enter
a recovery code.
</p>
</>
)}
<form onSubmit={verifyMfa}>
<div className="mx-auto hidden w-max min-w-[20rem] md:block">
{method === MfaMethod.EMAIL && (
<ReactCodeInput
name=""
inputMode="tel"
type="text"
fields={6}
onChange={setMfaCode}
className="mb-2 mt-6"
{...codeInputProps}
/>
)}
{method === MfaMethod.TOTP && (
<div className="mb-4 mt-6">
<Input value={mfaCode} onChange={(e) => setMfaCode(e.target.value)} />
</div>
)}
</div>
{typeof triesLeft === "number" && (
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
)}
<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
size="sm"
type="submit"
isFullWidth
className="h-14"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
>
{String(t("mfa.verify"))}
</Button>
</div>
</div>
</form>
{method === MfaMethod.TOTP && (
<div className="mt-2 flex flex-row justify-center text-sm text-bunker-400">
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Lost your recovery codes? Reset your account
</span>
</Link>
</div>
)}
{method === MfaMethod.EMAIL && (
<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">{t("signup.step2-resend-alert")}</span>
<div className="text-md mt-2 flex flex-row text-bunker-400">
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{isLoadingResend
? t("signup.step2-resend-progress")
: t("signup.step2-resend-submit")}
</span>
</button>
</div>
</div>
<p className="pb-2 text-sm text-bunker-400">{t("signup.step2-spam-alert")}</p>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,379 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "@tanstack/react-router";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import { jwtDecode } from "jwt-decode";
import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useToggle } from "@app/hooks";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey } from "@app/hooks/api/users/queries";
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../Login.utils";
import { Mfa } from "../Mfa";
type Props = {
providerAuthToken: string;
email: string;
password: string;
setPassword: (password: string) => void;
};
export const PasswordStep = ({ providerAuthToken, email, password, setPassword }: Props) => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const navigate = useNavigate();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange();
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
const { callbackPort, organizationId, hasExchangedPrivateKey } = jwtDecode(
providerAuthToken
) as any;
const handleExchange = async () => {
try {
setIsLoading(true);
const oauthLogin = await oauthTokenExchange({
email,
providerAuthToken
});
// attemptCliLogin
const cliUrl = `http://127.0.0.1:${callbackPort}/`;
// unset provider auth token in case it was used
SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(oauthLogin.token);
const privateKey = await fetchMyPrivateKey();
localStorage.setItem("PRIVATE_KEY", privateKey);
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
if (organizationId) {
const finishWithOrgWorkflow = async () => {
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
setMfaSuccessCallback(() => finishWithOrgWorkflow);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
return;
}
if (callbackPort) {
console.log("organization id was present. new JWT token to be used in CLI:", token);
const instance = axios.create();
const payload = {
privateKey,
email,
JTWToken: token
};
await instance.post(cliUrl, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
});
navigate({ to: "/cli-redirect" });
return;
}
await navigateUserToOrg(navigate, organizationId);
};
await finishWithOrgWorkflow();
}
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page
else {
const userOrgs = await fetchOrganizations();
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
else {
await navigateUserToOrg(navigate);
}
}
} catch (err: any) {
setIsLoading(false);
console.error(err);
if (err.response.data.error === "User Locked") {
createNotification({
title: err.response.data.error,
text: err.response.data.message,
type: "error"
});
return;
}
createNotification({
text: "Login unsuccessful. Double-check your master password and try again.",
type: "error"
});
}
};
useEffect(() => {
if (hasExchangedPrivateKey) {
handleExchange();
}
}, []);
const [captchaToken, setCaptchaToken] = useState("");
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
const captchaRef = useRef<HCaptcha>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
try {
setIsLoading(true);
if (callbackPort) {
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
providerAuthToken,
captchaToken
});
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
const cliUrl = `http://127.0.0.1:${callbackPort}/`;
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
if (organizationId) {
const finishWithOrgWorkflow = async () => {
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
organizationId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => finishWithOrgWorkflow);
return;
}
console.log("organization id was present. new JWT token to be used in CLI:", token);
const instance = axios.create();
const payload = {
...isCliLoginSuccessful.loginResponse,
JTWToken: token
};
await instance.post(cliUrl, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
});
navigate({ to: "/cli-redirect" });
};
await finishWithOrgWorkflow();
return;
}
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page
const userOrgs = await fetchOrganizations();
// case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort);
}
// case: no orgs found, so we navigate the user to create an org
else {
await navigateUserToOrg(navigate);
}
}
} else {
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken,
captchaToken
});
if (loginAttempt && loginAttempt.success) {
// case: login was successful
setIsLoading(false);
createNotification({
text: "Successfully logged in",
type: "success"
});
// case: organization ID is present from the provider auth token -- navigate directly to the org
if (organizationId) {
await navigateUserToOrg(navigate, organizationId);
}
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page
else {
const userOrgs = await fetchOrganizations();
if (userOrgs.length > 0) {
navigateToSelectOrganization();
} else {
await navigateUserToOrg(navigate);
}
}
}
}
} catch (err: any) {
setIsLoading(false);
console.error(err);
if (err.response.data.error === "User Locked") {
createNotification({
title: err.response.data.error,
text: err.response.data.message,
type: "error"
});
return;
}
if (err.response.data.error === "Captcha Required") {
setShouldShowCaptcha(true);
return;
}
createNotification({
text: "Login unsuccessful. Double-check your master password and try again.",
type: "error"
});
}
if (captchaRef.current) {
captchaRef.current.resetCaptcha();
}
setCaptchaToken("");
};
if (shouldShowMfa) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Mfa
email={email}
successCallback={mfaSuccessCallback}
method={requiredMfaMethod}
closeMfa={() => toggleShowMfa.off()}
/>
</div>
);
}
if (hasExchangedPrivateKey) {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Spinner />
<p className="text-white opacity-80">Loading, please wait</p>
</div>
);
}
return (
<form onSubmit={handleLogin} className="mx-auto h-full w-full max-w-md px-6 pt-8">
<div className="mb-8">
<p className="mx-auto mb-4 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
What&apos;s your Infisical password?
</p>
</div>
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[22rem] items-center justify-center rounded-lg md:max-h-28 lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg 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>
{shouldShowCaptcha && (
<div className="mx-auto mt-4 flex w-full min-w-[22rem] items-center justify-center lg:w-1/6">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
isLoading={isLoading}
className="h-14"
>
{t("login.login")}
</Button>
</div>
<div className="mx-auto mt-4 flex w-max flex-col items-center text-xs text-bunker-400">
<span className="max-w-sm px-4 text-center duration-200">
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="mt-2 cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
{t("login.forgot-password")}
</span>
</Link>
</div>
<div className="flex flex-row items-center justify-center">
<button
onClick={() => {
navigate({ to: "/login" });
}}
type="button"
className="mt-2 cursor-pointer text-xs text-bunker-400 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"
>
{t("login.other-option")}
</button>
</div>
</form>
);
};

View File

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

View File

@@ -0,0 +1,82 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Input } from "@app/components/v2";
type Props = {
setStep: (step: number) => void;
type: "SAML" | "OIDC";
};
export const SSOStep = ({ setStep, type }: Props) => {
const [ssoIdentifier, setSSOIdentifier] = useState("");
const { t } = useTranslation();
const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = (e: React.FormEvent) => {
e.preventDefault();
const callbackPort = queryParams.get("callback_port");
if (type === "SAML") {
window.open(
`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
} else {
window.open(
`/api/v1/sso/oidc/login?orgSlug=${ssoIdentifier}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`
);
}
window.close();
};
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-8 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
What&apos;s your organization slug?
</p>
<form onSubmit={handleSubmission}>
<div className="relative mx-auto flex max-h-24 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={ssoIdentifier}
onChange={(e) => setSSOIdentifier(e.target.value)}
type="text"
placeholder="acme-123"
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mx-auto mt-4 flex w-full min-w-[20rem] items-center justify-center rounded-md text-center md:min-w-[22rem] lg:w-1/6">
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="h-14"
>
Continue with {type}
</Button>
</div>
</form>
<div className="mt-4 flex flex-row items-center justify-center">
<button
onClick={() => {
setStep(0);
}}
type="button"
className="mt-2 cursor-pointer text-sm text-bunker-300 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"
>
{t("login.other-option")}
</button>
</div>
</div>
);
};

View File

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

View File

@@ -0,0 +1,5 @@
export { InitialStep } from "./InitialStep";
export { SSOStep } from "./SSOStep";
// SSO-specific step
export { PasswordStep } from "./PasswordStep";

View File

@@ -0,0 +1,81 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { createFileRoute, Link } from "@tanstack/react-router";
import { InitialStep, SSOStep } from "./-components";
const LoginPage = () => {
const { t } = useTranslation();
const [step, setStep] = useState(0);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// TODO(rbr): move this to beforeload
// const { navigateToSelectOrganization } = useNavigateToSelectOrganization();
//
// const queryParams = new URLSearchParams(window.location.search);
//
// useEffect(() => {
// // TODO(akhilmhdh): workspace will be controlled by a workspace context
// const handleRedirects = async () => {
// try {
// const callbackPort = queryParams?.get("callback_port");
// // case: a callback port is set, meaning it's a cli login request: redirect to select org with callback port
// if (callbackPort) {
// navigateToSelectOrganization(callbackPort);
// } else {
// // case: no callback port, meaning it's a regular login request: redirect to select org
// navigateToSelectOrganization();
// }
// } catch (error) {
// console.log("Error - Not logged in yet");
// }
// };
// if (isLoggedIn()) {
// handleRedirects();
// }
// }, []);
const renderView = () => {
switch (step) {
case 0:
return (
<InitialStep
setStep={setStep}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
/>
);
case 2:
return <SSOStep setStep={setStep} type="SAML" />;
case 3:
return <SSOStep setStep={setStep} type="OIDC" />;
default:
return <div />;
}
};
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">
<Helmet>
<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") ?? ""} />
</Helmet>
<Link to="/">
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<div className="pb-28">{renderView()}</div>;
</div>
);
};
export const Route = createFileRoute("/login/")({
component: LoginPage
});

View File

@@ -0,0 +1,157 @@
import { useTranslation } from "react-i18next";
import { Helmet } from "react-helmet";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useServerConfig } from "@app/context";
import { useState } from "react";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
import { createNotification } from "@app/components/notifications";
import { Input, Button } from "@app/components/v2";
const LoginLDAPPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const passedOrgSlug = queryParams.get("organizationSlug");
const passedUsername = queryParams.get("username");
const [organizationSlug, setOrganizationSlug] = useState(
config.defaultAuthOrgSlug || passedOrgSlug || ""
);
const [username, setUsername] = useState(passedUsername || "");
const [password, setPassword] = useState("");
const handleSubmission = async (e: React.FormEvent) => {
e.preventDefault();
try {
const { nextUrl } = await loginLDAPRedirect({
organizationSlug,
username,
password
});
if (!nextUrl) {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
return;
}
createNotification({
text: "Successfully logged in",
type: "success"
});
window.open(nextUrl);
window.close();
} catch {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
// TODO: add callback port support
// const callbackPort = queryParams.get("callback_port");
// window.open(`/api/v1/ldap/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
// window.close();
};
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">
<Helmet>
<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") ?? ""} />
</Helmet>
<Link to="/">
<div className="mb-4 mt-20 flex justify-center">
<img 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 mb-8 flex w-max justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
What&apos;s your LDAP Login?
</p>
<form onSubmit={handleSubmission}>
{!config.defaultAuthOrgSlug && !passedOrgSlug && (
<div className="relative mx-auto flex max-h-24 w-1/4 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={organizationSlug}
onChange={(e) => setOrganizationSlug(e.target.value)}
type="text"
placeholder="Enter your organization slug..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
)}
<div className="relative mx-auto mt-2 flex max-h-24 w-1/4 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
placeholder="Enter your LDAP username..."
isRequired
autoComplete="email"
id="email"
className="h-12"
isDisabled={passedUsername !== null}
/>
</div>
</div>
<div className="relative mx-auto mt-2 flex max-h-24 w-1/4 w-full min-w-[20rem] items-center justify-center rounded-lg md:max-h-28 md:min-w-[22rem] lg:w-1/6">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your LDAP password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
</div>
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[20rem] items-center justify-center rounded-md text-center md:min-w-[22rem] lg:w-1/6">
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="h-14"
>
{t("login.login")}
</Button>
</div>
</form>
<div className="mt-4 flex flex-row items-center justify-center">
<button
onClick={() => {
navigate({ to: "/login" });
}}
type="button"
className="mt-2 cursor-pointer text-sm text-bunker-300 duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4"
>
{t("login.other-option")}
</button>
</div>
</div>
</div>
);
};
export const Route = createFileRoute("/login/ldap/")({
component: LoginLDAPPage
});

View File

@@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
const LoginProviderError = () => {
useEffect(() => {
window.localStorage.setItem("PROVIDER_AUTH_ERROR", "err");
window.close();
}, []);
return <div />;
};
export const Route = createFileRoute("/login/provider/error")({
component: LoginProviderError
});

View File

@@ -0,0 +1,19 @@
import { useEffect } from "react";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { createFileRoute, useSearch } from "@tanstack/react-router";
const LoginProviderSuccess = () => {
const search = useSearch({ from: "/login/provider/success" });
useEffect(() => {
SecurityClient.setProviderAuthToken(search.token);
window.close();
}, []);
return <div />;
};
export const Route = createFileRoute("/login/provider/success")({
component: LoginProviderSuccess
});

View File

@@ -0,0 +1,281 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Helmet } from "react-helmet";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios";
import { addSeconds, formatISO } from "date-fns";
import { jwtDecode } from "jwt-decode";
import { createNotification } from "@app/components/notifications";
import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Spinner } from "@app/components/v2";
import { SessionStorageKeys } from "@app/const";
import { useToggle } from "@app/hooks";
import {
useGetOrganizations,
useGetUser,
useLogoutUser,
useSelectOrganization
} from "@app/hooks/api";
import { MfaMethod, UserAgentType } from "@app/hooks/api/auth/types";
import { Organization } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types";
import { getAuthToken, isLoggedIn } from "@app/hooks/api/reactQuery";
import { navigateUserToOrg } from "../-components/Login.utils";
import { Mfa } from "../-components/Mfa";
const LoadingScreen = () => {
return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Spinner />
<p className="text-white opacity-80">Loading, please wait</p>
</div>
);
};
const SelectOrganizationPage = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const organizations = useGetOrganizations();
const selectOrg = useSelectOrganization();
const { data: user, isLoading: userLoading } = useGetUser();
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const queryParams = new URLSearchParams(window.location.search);
const orgId = queryParams.get("org_id");
const callbackPort = queryParams.get("callback_port");
const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId);
const logout = useLogoutUser(true);
const handleLogout = useCallback(async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
navigate({ to: "/login" });
} catch (error) {
console.error(error);
}
}, [logout, navigate]);
const handleSelectOrganization = useCallback(
async (organization: Organization) => {
if (organization.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
let url = "";
if (organization.orgAuthMethod === AuthMethod.OIDC) {
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
} else {
url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`;
if (callbackPort) {
url += `?callback_port=${callbackPort}`;
}
}
window.open(url);
window.close();
return;
}
const { token, isMfaEnabled, mfaMethod } = await selectOrg
.mutateAsync({
organizationId: organization.id,
userAgent: callbackPort ? UserAgentType.CLI : undefined
})
.finally(() => setIsInitialOrgCheckLoading(false));
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleSelectOrganization(organization));
return;
}
if (callbackPort) {
const privateKey = localStorage.getItem("PRIVATE_KEY");
let error: string | null = null;
if (!privateKey) error = "Private key not found";
if (!user?.email) error = "User email not found";
if (!token) error = "No token found";
if (error) {
createNotification({
text: error,
type: "error"
});
return;
}
const payload = {
JTWToken: token,
email: user?.email,
privateKey
} as IsCliLoginSuccessful["loginResponse"];
// send request to server endpoint
const instance = axios.create();
await instance.post(`http://127.0.0.1:${callbackPort}/`, payload).catch(() => {
// if error happens to communicate we set the token with an expiry in sessino storage
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
sessionStorage.setItem(
SessionStorageKeys.CLI_TERMINAL_TOKEN,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 30)),
data: window.btoa(JSON.stringify(payload))
})
);
});
navigate({ to: "/cli-redirect" });
// cli page
} else {
navigateUserToOrg(navigate, organization.id);
}
},
[selectOrg]
);
const handleCliRedirect = useCallback(() => {
const authToken = getAuthToken();
if (authToken && !callbackPort) {
const decodedJwt = jwtDecode(authToken) as any;
if (decodedJwt?.organizationId) {
navigateUserToOrg(navigate, decodedJwt.organizationId);
}
}
if (!isLoggedIn()) {
navigate({ to: "/login" });
}
}, []);
useEffect(() => {
if (callbackPort) {
handleCliRedirect();
}
}, [navigate]);
useEffect(() => {
if (organizations.isLoading || !organizations.data) return;
// Case: User has no organizations.
// This can happen if the user was previously a member, but the organization was deleted or the user was removed.
if (organizations.data.length === 0) {
navigate({ to: "/org/none" });
} else if (organizations.data.length === 1) {
if (callbackPort) {
handleCliRedirect();
setIsInitialOrgCheckLoading(false);
} else {
handleSelectOrganization(organizations.data[0]);
}
} else {
setIsInitialOrgCheckLoading(false);
}
}, [organizations.isLoading, organizations.data]);
useEffect(() => {
if (defaultSelectedOrg) {
handleSelectOrganization(defaultSelectedOrg);
}
}, [defaultSelectedOrg]);
if (
userLoading ||
!user ||
((isInitialOrgCheckLoading || defaultSelectedOrg) && !shouldShowMfa)
) {
return <LoadingScreen />;
}
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">
<Helmet>
<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") ?? ""} />
</Helmet>
{shouldShowMfa ? (
<Mfa
email={user.email as string}
successCallback={mfaSuccessCallback}
method={requiredMfaMethod}
/>
) : (
<div className="mx-auto mt-20 w-fit rounded-lg border-2 border-mineshaft-500 p-10 shadow-lg">
<Link href="/">
<div className="mb-4 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<form className="mx-auto flex w-full flex-col items-center justify-center">
<div className="mb-8 space-y-2">
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-2xl font-medium text-transparent">
Choose your organization
</h1>
<div className="space-y-1">
<p className="text-md text-center text-gray-500">
You&lsquo;re currently logged in as <strong>{user.username}</strong>
</p>
<p className="text-md text-center text-gray-500">
Not you?{" "}
<Button variant="link" onClick={handleLogout} className="font-semibold">
Change account
</Button>
</p>
</div>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] space-y-4 rounded-md text-center md:min-w-[25.1rem] lg:w-1/4">
{organizations.isLoading ? (
<Spinner />
) : (
organizations.data?.map((org) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
onClick={() => handleSelectOrganization(org)}
key={org.id}
className="group flex cursor-pointer items-center justify-between rounded-md bg-mineshaft-700 px-4 py-3 capitalize text-gray-200 shadow-md transition-colors hover:bg-mineshaft-600"
>
<p className="truncate transition-colors">{org.name}</p>
<FontAwesomeIcon
icon={faArrowRight}
className="text-gray-400 transition-all group-hover:translate-x-2 group-hover:text-primary-500"
/>
</div>
))
)}
</div>
</form>
</div>
)}
<div className="pb-28" />
</div>
);
};
export const Route = createFileRoute("/login/select-organization/")({
component: SelectOrganizationPage
});

View File

@@ -0,0 +1,63 @@
import { useTranslation } from "react-i18next";
import { Helmet } from "react-helmet";
import { createFileRoute, Link, useSearch } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { jwtDecode } from "jwt-decode";
import { PasswordStep } from "../-components";
const LoginSSOPage = () => {
const { t } = useTranslation();
const search = useSearch({ from: "/login/sso" });
const token = search.token as string;
const [step, setStep] = useState(0);
const [password, setPassword] = useState("");
const { username, isUserCompleted } = jwtDecode(token) as any;
useEffect(() => {
if (isUserCompleted) {
setStep(1);
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return <div />;
case 1:
return (
<PasswordStep
providerAuthToken={token}
email={username}
password={password}
setPassword={setPassword}
/>
);
default:
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">
<Helmet>
<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") ?? ""} />
</Helmet>
<Link href="/">
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
</div>
</Link>
<div>{renderView()}</div>;
</div>
);
};
export const Route = createFileRoute("/login/sso/")({
component: LoginSSOPage
});

View File

@@ -0,0 +1,104 @@
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { decryptPrivateKeyHelper } from "@app/helpers/key";
/**
* Class to handle key actions
* TODO: in future, all private key-related encryption operations
* must pass through this class
*/
class KeyService {
private static privateKey: string = "";
static setPrivateKey(privateKey: string) {
KeyService.privateKey = privateKey;
}
/** Return the user's decrypted private key
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
* @returns {String} privateKey - decrypted private key
*/
static async decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) {
return decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
}
/**
* Return [plaintext] encrypted by the user's private key
* @param {Object} obj
* @param {String} obj.plaintext - plaintext to encrypt
*/
static encryptWithPrivateKey({ plaintext, publicKey }: { plaintext: string; publicKey: string }) {
return encryptAssymmetric({
plaintext,
publicKey,
privateKey: KeyService.privateKey
});
}
/**
* Return [ciphertext] decrypted by the user's private key
* @param {Object} obj
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.ciphertext - iv of ciphertext
* @param {String} obj.ciphertext - tag of ciphertext
*/
static decryptWithPrivateKey({
ciphertext,
nonce,
publicKey
}: {
ciphertext: string;
nonce: string;
publicKey: string;
}) {
return decryptAssymmetric({
ciphertext,
nonce,
publicKey,
privateKey: KeyService.privateKey
});
}
}
export default KeyService;

View File

@@ -0,0 +1,19 @@
import { initProjectHelper } from "@app/helpers/project";
class ProjectService {
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
static async initProject({ projectName }: { projectName: string }) {
return initProjectHelper({
projectName
});
}
}
export default ProjectService;

View File

@@ -0,0 +1,4 @@
import KeyService from "./KeyService";
import ProjectService from "./ProjectService";
export { KeyService, ProjectService };

View File

@@ -3,7 +3,7 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",

View File

@@ -1,8 +1,23 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
import { nodePolyfills } from "vite-plugin-node-polyfills";
// https://vite.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite(), react()]
plugins: [
tsconfigPaths(),
nodePolyfills({
globals: {
Buffer: true
}
}),
wasm(),
topLevelAwait(),
TanStackRouterVite(),
react()
]
});