mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: completed signup and personal settings ui
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useGetOrgTrialUrl } from "@app/hooks/api";
|
||||
|
||||
const supportOptions = [
|
||||
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
"Support Forum",
|
||||
@@ -84,7 +84,7 @@ export const SidebarFooter = () => {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{supportOptions.map(([icon, text, url]) => (
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faArrowLeft, faInfo, faMobile, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet, useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/SidebarFooter/SidebarFooter";
|
||||
|
||||
export const PersonalSettingsLayout = () => {
|
||||
const { organizationId } = useRouteContext({ from: "/_authenticate/_personal_settings_layout" });
|
||||
const { t } = useTranslation();
|
||||
|
||||
const infisicalPlatformVersion = import.meta.env.VITE_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="flex-grow">
|
||||
<Link
|
||||
to={`/organization/$organizationId/${ProjectType.SecretManager}` as const}
|
||||
params={{ organizationId }}
|
||||
>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{infisicalPlatformVersion && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {infisicalPlatformVersion}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
frontend-v2/src/layouts/PersonalSettingsLayout/index.tsx
Normal file
1
frontend-v2/src/layouts/PersonalSettingsLayout/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { PersonalSettingsLayout } from "./PersonalSettingsLayout";
|
||||
@@ -11,25 +11,38 @@
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as RestrictloginsignupImport } from './routes/_restrict_login_signup'
|
||||
import { Route as AuthenticateImport } from './routes/_authenticate'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as AuthenticateRestrictloginsignupImport } from './routes/_authenticate/_restrict_login_signup'
|
||||
import { Route as RestrictloginsignupVerifyEmailImport } from './routes/_restrict_login_signup/verify-email'
|
||||
import { Route as RestrictloginsignupSignupinviteImport } from './routes/_restrict_login_signup/signupinvite'
|
||||
import { Route as RestrictloginsignupRequestnewinviteImport } from './routes/_restrict_login_signup/requestnewinvite'
|
||||
import { Route as RestrictloginsignupPasswordResetImport } from './routes/_restrict_login_signup/password-reset'
|
||||
import { Route as RestrictloginsignupEmailNotVerifiedImport } from './routes/_restrict_login_signup/email-not-verified'
|
||||
import { Route as AuthenticateCliRedirectImport } from './routes/_authenticate/cli-redirect'
|
||||
import { Route as AuthenticatePersonalsettingslayoutImport } from './routes/_authenticate/_personal_settings_layout'
|
||||
import { Route as AuthenticateOrgdetailsImport } from './routes/_authenticate/_org_details'
|
||||
import { Route as RestrictloginsignupSignupIndexImport } from './routes/_restrict_login_signup/signup/index'
|
||||
import { Route as RestrictloginsignupLoginIndexImport } from './routes/_restrict_login_signup/login/index'
|
||||
import { Route as AuthenticateOrgdetailsOrganizationlayoutImport } from './routes/_authenticate/_org_details/_organization_layout'
|
||||
import { Route as AuthenticateRestrictloginsignupSignupIndexImport } from './routes/_authenticate/_restrict_login_signup/signup/index'
|
||||
import { Route as AuthenticateRestrictloginsignupLoginIndexImport } from './routes/_authenticate/_restrict_login_signup/login/index'
|
||||
import { Route as AuthenticateRestrictloginsignupSignupSsoIndexImport } from './routes/_authenticate/_restrict_login_signup/signup/sso/index'
|
||||
import { Route as AuthenticateRestrictloginsignupLoginSsoIndexImport } from './routes/_authenticate/_restrict_login_signup/login/sso/index'
|
||||
import { Route as AuthenticateRestrictloginsignupLoginSelectOrganizationIndexImport } from './routes/_authenticate/_restrict_login_signup/login/select-organization/index'
|
||||
import { Route as AuthenticateRestrictloginsignupLoginLdapIndexImport } from './routes/_authenticate/_restrict_login_signup/login/ldap/index'
|
||||
import { Route as RestrictloginsignupSignupSsoIndexImport } from './routes/_restrict_login_signup/signup/sso/index'
|
||||
import { Route as RestrictloginsignupLoginSsoIndexImport } from './routes/_restrict_login_signup/login/sso/index'
|
||||
import { Route as RestrictloginsignupLoginSelectOrganizationIndexImport } from './routes/_restrict_login_signup/login/select-organization/index'
|
||||
import { Route as RestrictloginsignupLoginLdapIndexImport } from './routes/_restrict_login_signup/login/ldap/index'
|
||||
import { Route as AuthenticatePersonalsettingslayoutPersonalSettingsIndexImport } from './routes/_authenticate/_personal_settings_layout/personal-settings/index'
|
||||
import { Route as RestrictloginsignupLoginProviderSuccessImport } from './routes/_restrict_login_signup/login/provider/success'
|
||||
import { Route as RestrictloginsignupLoginProviderErrorImport } from './routes/_restrict_login_signup/login/provider/error'
|
||||
import { Route as AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexImport } from './routes/_authenticate/_org_details/_organization_layout/organization/index'
|
||||
import { Route as AuthenticateRestrictloginsignupLoginProviderSuccessImport } from './routes/_authenticate/_restrict_login_signup/login/provider/success'
|
||||
import { Route as AuthenticateRestrictloginsignupLoginProviderErrorImport } from './routes/_authenticate/_restrict_login_signup/login/provider/error'
|
||||
import { Route as AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdIndexImport } from './routes/_authenticate/_org_details/_organization_layout/organization/$organizationId/index'
|
||||
import { Route as AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdSecretManagerImport } from './routes/_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const RestrictloginsignupRoute = RestrictloginsignupImport.update({
|
||||
id: '/_restrict_login_signup',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRoute = AuthenticateImport.update({
|
||||
id: '/_authenticate',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -41,9 +54,50 @@ const IndexRoute = IndexImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupRoute =
|
||||
AuthenticateRestrictloginsignupImport.update({
|
||||
id: '/_restrict_login_signup',
|
||||
const RestrictloginsignupVerifyEmailRoute =
|
||||
RestrictloginsignupVerifyEmailImport.update({
|
||||
id: '/verify-email',
|
||||
path: '/verify-email',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupSignupinviteRoute =
|
||||
RestrictloginsignupSignupinviteImport.update({
|
||||
id: '/signupinvite',
|
||||
path: '/signupinvite',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupRequestnewinviteRoute =
|
||||
RestrictloginsignupRequestnewinviteImport.update({
|
||||
id: '/requestnewinvite',
|
||||
path: '/requestnewinvite',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupPasswordResetRoute =
|
||||
RestrictloginsignupPasswordResetImport.update({
|
||||
id: '/password-reset',
|
||||
path: '/password-reset',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupEmailNotVerifiedRoute =
|
||||
RestrictloginsignupEmailNotVerifiedImport.update({
|
||||
id: '/email-not-verified',
|
||||
path: '/email-not-verified',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateCliRedirectRoute = AuthenticateCliRedirectImport.update({
|
||||
id: '/cli-redirect',
|
||||
path: '/cli-redirect',
|
||||
getParentRoute: () => AuthenticateRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticatePersonalsettingslayoutRoute =
|
||||
AuthenticatePersonalsettingslayoutImport.update({
|
||||
id: '/_personal_settings_layout',
|
||||
getParentRoute: () => AuthenticateRoute,
|
||||
} as any)
|
||||
|
||||
@@ -52,52 +106,73 @@ const AuthenticateOrgdetailsRoute = AuthenticateOrgdetailsImport.update({
|
||||
getParentRoute: () => AuthenticateRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupSignupIndexRoute =
|
||||
RestrictloginsignupSignupIndexImport.update({
|
||||
id: '/signup/',
|
||||
path: '/signup/',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupLoginIndexRoute =
|
||||
RestrictloginsignupLoginIndexImport.update({
|
||||
id: '/login/',
|
||||
path: '/login/',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateOrgdetailsOrganizationlayoutRoute =
|
||||
AuthenticateOrgdetailsOrganizationlayoutImport.update({
|
||||
id: '/_organization_layout',
|
||||
getParentRoute: () => AuthenticateOrgdetailsRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupSignupIndexRoute =
|
||||
AuthenticateRestrictloginsignupSignupIndexImport.update({
|
||||
id: '/signup/',
|
||||
path: '/signup/',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupLoginIndexRoute =
|
||||
AuthenticateRestrictloginsignupLoginIndexImport.update({
|
||||
id: '/login/',
|
||||
path: '/login/',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupSignupSsoIndexRoute =
|
||||
AuthenticateRestrictloginsignupSignupSsoIndexImport.update({
|
||||
const RestrictloginsignupSignupSsoIndexRoute =
|
||||
RestrictloginsignupSignupSsoIndexImport.update({
|
||||
id: '/signup/sso/',
|
||||
path: '/signup/sso/',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupLoginSsoIndexRoute =
|
||||
AuthenticateRestrictloginsignupLoginSsoIndexImport.update({
|
||||
const RestrictloginsignupLoginSsoIndexRoute =
|
||||
RestrictloginsignupLoginSsoIndexImport.update({
|
||||
id: '/login/sso/',
|
||||
path: '/login/sso/',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute =
|
||||
AuthenticateRestrictloginsignupLoginSelectOrganizationIndexImport.update({
|
||||
const RestrictloginsignupLoginSelectOrganizationIndexRoute =
|
||||
RestrictloginsignupLoginSelectOrganizationIndexImport.update({
|
||||
id: '/login/select-organization/',
|
||||
path: '/login/select-organization/',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupLoginLdapIndexRoute =
|
||||
AuthenticateRestrictloginsignupLoginLdapIndexImport.update({
|
||||
const RestrictloginsignupLoginLdapIndexRoute =
|
||||
RestrictloginsignupLoginLdapIndexImport.update({
|
||||
id: '/login/ldap/',
|
||||
path: '/login/ldap/',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute =
|
||||
AuthenticatePersonalsettingslayoutPersonalSettingsIndexImport.update({
|
||||
id: '/personal-settings/',
|
||||
path: '/personal-settings/',
|
||||
getParentRoute: () => AuthenticatePersonalsettingslayoutRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupLoginProviderSuccessRoute =
|
||||
RestrictloginsignupLoginProviderSuccessImport.update({
|
||||
id: '/login/provider/success',
|
||||
path: '/login/provider/success',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const RestrictloginsignupLoginProviderErrorRoute =
|
||||
RestrictloginsignupLoginProviderErrorImport.update({
|
||||
id: '/login/provider/error',
|
||||
path: '/login/provider/error',
|
||||
getParentRoute: () => RestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexRoute =
|
||||
@@ -107,20 +182,6 @@ const AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexRoute =
|
||||
getParentRoute: () => AuthenticateOrgdetailsOrganizationlayoutRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupLoginProviderSuccessRoute =
|
||||
AuthenticateRestrictloginsignupLoginProviderSuccessImport.update({
|
||||
id: '/login/provider/success',
|
||||
path: '/login/provider/success',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateRestrictloginsignupLoginProviderErrorRoute =
|
||||
AuthenticateRestrictloginsignupLoginProviderErrorImport.update({
|
||||
id: '/login/provider/error',
|
||||
path: '/login/provider/error',
|
||||
getParentRoute: () => AuthenticateRestrictloginsignupRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdIndexRoute =
|
||||
AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdIndexImport.update(
|
||||
{
|
||||
@@ -157,6 +218,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_restrict_login_signup': {
|
||||
id: '/_restrict_login_signup'
|
||||
path: ''
|
||||
fullPath: ''
|
||||
preLoaderRoute: typeof RestrictloginsignupImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_authenticate/_org_details': {
|
||||
id: '/_authenticate/_org_details'
|
||||
path: ''
|
||||
@@ -164,13 +232,55 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateOrgdetailsImport
|
||||
parentRoute: typeof AuthenticateImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup': {
|
||||
id: '/_authenticate/_restrict_login_signup'
|
||||
'/_authenticate/_personal_settings_layout': {
|
||||
id: '/_authenticate/_personal_settings_layout'
|
||||
path: ''
|
||||
fullPath: ''
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
preLoaderRoute: typeof AuthenticatePersonalsettingslayoutImport
|
||||
parentRoute: typeof AuthenticateImport
|
||||
}
|
||||
'/_authenticate/cli-redirect': {
|
||||
id: '/_authenticate/cli-redirect'
|
||||
path: '/cli-redirect'
|
||||
fullPath: '/cli-redirect'
|
||||
preLoaderRoute: typeof AuthenticateCliRedirectImport
|
||||
parentRoute: typeof AuthenticateImport
|
||||
}
|
||||
'/_restrict_login_signup/email-not-verified': {
|
||||
id: '/_restrict_login_signup/email-not-verified'
|
||||
path: '/email-not-verified'
|
||||
fullPath: '/email-not-verified'
|
||||
preLoaderRoute: typeof RestrictloginsignupEmailNotVerifiedImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/password-reset': {
|
||||
id: '/_restrict_login_signup/password-reset'
|
||||
path: '/password-reset'
|
||||
fullPath: '/password-reset'
|
||||
preLoaderRoute: typeof RestrictloginsignupPasswordResetImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/requestnewinvite': {
|
||||
id: '/_restrict_login_signup/requestnewinvite'
|
||||
path: '/requestnewinvite'
|
||||
fullPath: '/requestnewinvite'
|
||||
preLoaderRoute: typeof RestrictloginsignupRequestnewinviteImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/signupinvite': {
|
||||
id: '/_restrict_login_signup/signupinvite'
|
||||
path: '/signupinvite'
|
||||
fullPath: '/signupinvite'
|
||||
preLoaderRoute: typeof RestrictloginsignupSignupinviteImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/verify-email': {
|
||||
id: '/_restrict_login_signup/verify-email'
|
||||
path: '/verify-email'
|
||||
fullPath: '/verify-email'
|
||||
preLoaderRoute: typeof RestrictloginsignupVerifyEmailImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_org_details/_organization_layout': {
|
||||
id: '/_authenticate/_org_details/_organization_layout'
|
||||
path: ''
|
||||
@@ -178,33 +288,68 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateOrgdetailsOrganizationlayoutImport
|
||||
parentRoute: typeof AuthenticateOrgdetailsImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/login/': {
|
||||
id: '/_authenticate/_restrict_login_signup/login/'
|
||||
'/_restrict_login_signup/login/': {
|
||||
id: '/_restrict_login_signup/login/'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupLoginIndexImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
preLoaderRoute: typeof RestrictloginsignupLoginIndexImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/signup/': {
|
||||
id: '/_authenticate/_restrict_login_signup/signup/'
|
||||
'/_restrict_login_signup/signup/': {
|
||||
id: '/_restrict_login_signup/signup/'
|
||||
path: '/signup'
|
||||
fullPath: '/signup'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupSignupIndexImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
preLoaderRoute: typeof RestrictloginsignupSignupIndexImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/login/provider/error': {
|
||||
id: '/_authenticate/_restrict_login_signup/login/provider/error'
|
||||
'/_restrict_login_signup/login/provider/error': {
|
||||
id: '/_restrict_login_signup/login/provider/error'
|
||||
path: '/login/provider/error'
|
||||
fullPath: '/login/provider/error'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupLoginProviderErrorImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
preLoaderRoute: typeof RestrictloginsignupLoginProviderErrorImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/login/provider/success': {
|
||||
id: '/_authenticate/_restrict_login_signup/login/provider/success'
|
||||
'/_restrict_login_signup/login/provider/success': {
|
||||
id: '/_restrict_login_signup/login/provider/success'
|
||||
path: '/login/provider/success'
|
||||
fullPath: '/login/provider/success'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupLoginProviderSuccessImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
preLoaderRoute: typeof RestrictloginsignupLoginProviderSuccessImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_personal_settings_layout/personal-settings/': {
|
||||
id: '/_authenticate/_personal_settings_layout/personal-settings/'
|
||||
path: '/personal-settings'
|
||||
fullPath: '/personal-settings'
|
||||
preLoaderRoute: typeof AuthenticatePersonalsettingslayoutPersonalSettingsIndexImport
|
||||
parentRoute: typeof AuthenticatePersonalsettingslayoutImport
|
||||
}
|
||||
'/_restrict_login_signup/login/ldap/': {
|
||||
id: '/_restrict_login_signup/login/ldap/'
|
||||
path: '/login/ldap'
|
||||
fullPath: '/login/ldap'
|
||||
preLoaderRoute: typeof RestrictloginsignupLoginLdapIndexImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/login/select-organization/': {
|
||||
id: '/_restrict_login_signup/login/select-organization/'
|
||||
path: '/login/select-organization'
|
||||
fullPath: '/login/select-organization'
|
||||
preLoaderRoute: typeof RestrictloginsignupLoginSelectOrganizationIndexImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/login/sso/': {
|
||||
id: '/_restrict_login_signup/login/sso/'
|
||||
path: '/login/sso'
|
||||
fullPath: '/login/sso'
|
||||
preLoaderRoute: typeof RestrictloginsignupLoginSsoIndexImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_restrict_login_signup/signup/sso/': {
|
||||
id: '/_restrict_login_signup/signup/sso/'
|
||||
path: '/signup/sso'
|
||||
fullPath: '/signup/sso'
|
||||
preLoaderRoute: typeof RestrictloginsignupSignupSsoIndexImport
|
||||
parentRoute: typeof RestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_org_details/_organization_layout/organization/': {
|
||||
id: '/_authenticate/_org_details/_organization_layout/organization/'
|
||||
@@ -213,34 +358,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexImport
|
||||
parentRoute: typeof AuthenticateOrgdetailsOrganizationlayoutImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/login/ldap/': {
|
||||
id: '/_authenticate/_restrict_login_signup/login/ldap/'
|
||||
path: '/login/ldap'
|
||||
fullPath: '/login/ldap'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupLoginLdapIndexImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/login/select-organization/': {
|
||||
id: '/_authenticate/_restrict_login_signup/login/select-organization/'
|
||||
path: '/login/select-organization'
|
||||
fullPath: '/login/select-organization'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupLoginSelectOrganizationIndexImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/login/sso/': {
|
||||
id: '/_authenticate/_restrict_login_signup/login/sso/'
|
||||
path: '/login/sso'
|
||||
fullPath: '/login/sso'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupLoginSsoIndexImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_restrict_login_signup/signup/sso/': {
|
||||
id: '/_authenticate/_restrict_login_signup/signup/sso/'
|
||||
path: '/signup/sso'
|
||||
fullPath: '/signup/sso'
|
||||
preLoaderRoute: typeof AuthenticateRestrictloginsignupSignupSsoIndexImport
|
||||
parentRoute: typeof AuthenticateRestrictloginsignupImport
|
||||
}
|
||||
'/_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager': {
|
||||
id: '/_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager'
|
||||
path: '/organization/$organizationId/secret-manager'
|
||||
@@ -296,69 +413,99 @@ const AuthenticateOrgdetailsRouteWithChildren =
|
||||
AuthenticateOrgdetailsRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateRestrictloginsignupRouteChildren {
|
||||
AuthenticateRestrictloginsignupLoginIndexRoute: typeof AuthenticateRestrictloginsignupLoginIndexRoute
|
||||
AuthenticateRestrictloginsignupSignupIndexRoute: typeof AuthenticateRestrictloginsignupSignupIndexRoute
|
||||
AuthenticateRestrictloginsignupLoginProviderErrorRoute: typeof AuthenticateRestrictloginsignupLoginProviderErrorRoute
|
||||
AuthenticateRestrictloginsignupLoginProviderSuccessRoute: typeof AuthenticateRestrictloginsignupLoginProviderSuccessRoute
|
||||
AuthenticateRestrictloginsignupLoginLdapIndexRoute: typeof AuthenticateRestrictloginsignupLoginLdapIndexRoute
|
||||
AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute: typeof AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
AuthenticateRestrictloginsignupLoginSsoIndexRoute: typeof AuthenticateRestrictloginsignupLoginSsoIndexRoute
|
||||
AuthenticateRestrictloginsignupSignupSsoIndexRoute: typeof AuthenticateRestrictloginsignupSignupSsoIndexRoute
|
||||
interface AuthenticatePersonalsettingslayoutRouteChildren {
|
||||
AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute: typeof AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute
|
||||
}
|
||||
|
||||
const AuthenticateRestrictloginsignupRouteChildren: AuthenticateRestrictloginsignupRouteChildren =
|
||||
const AuthenticatePersonalsettingslayoutRouteChildren: AuthenticatePersonalsettingslayoutRouteChildren =
|
||||
{
|
||||
AuthenticateRestrictloginsignupLoginIndexRoute:
|
||||
AuthenticateRestrictloginsignupLoginIndexRoute,
|
||||
AuthenticateRestrictloginsignupSignupIndexRoute:
|
||||
AuthenticateRestrictloginsignupSignupIndexRoute,
|
||||
AuthenticateRestrictloginsignupLoginProviderErrorRoute:
|
||||
AuthenticateRestrictloginsignupLoginProviderErrorRoute,
|
||||
AuthenticateRestrictloginsignupLoginProviderSuccessRoute:
|
||||
AuthenticateRestrictloginsignupLoginProviderSuccessRoute,
|
||||
AuthenticateRestrictloginsignupLoginLdapIndexRoute:
|
||||
AuthenticateRestrictloginsignupLoginLdapIndexRoute,
|
||||
AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute:
|
||||
AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute,
|
||||
AuthenticateRestrictloginsignupLoginSsoIndexRoute:
|
||||
AuthenticateRestrictloginsignupLoginSsoIndexRoute,
|
||||
AuthenticateRestrictloginsignupSignupSsoIndexRoute:
|
||||
AuthenticateRestrictloginsignupSignupSsoIndexRoute,
|
||||
AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute:
|
||||
AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute,
|
||||
}
|
||||
|
||||
const AuthenticateRestrictloginsignupRouteWithChildren =
|
||||
AuthenticateRestrictloginsignupRoute._addFileChildren(
|
||||
AuthenticateRestrictloginsignupRouteChildren,
|
||||
const AuthenticatePersonalsettingslayoutRouteWithChildren =
|
||||
AuthenticatePersonalsettingslayoutRoute._addFileChildren(
|
||||
AuthenticatePersonalsettingslayoutRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateRouteChildren {
|
||||
AuthenticateOrgdetailsRoute: typeof AuthenticateOrgdetailsRouteWithChildren
|
||||
AuthenticateRestrictloginsignupRoute: typeof AuthenticateRestrictloginsignupRouteWithChildren
|
||||
AuthenticatePersonalsettingslayoutRoute: typeof AuthenticatePersonalsettingslayoutRouteWithChildren
|
||||
AuthenticateCliRedirectRoute: typeof AuthenticateCliRedirectRoute
|
||||
}
|
||||
|
||||
const AuthenticateRouteChildren: AuthenticateRouteChildren = {
|
||||
AuthenticateOrgdetailsRoute: AuthenticateOrgdetailsRouteWithChildren,
|
||||
AuthenticateRestrictloginsignupRoute:
|
||||
AuthenticateRestrictloginsignupRouteWithChildren,
|
||||
AuthenticatePersonalsettingslayoutRoute:
|
||||
AuthenticatePersonalsettingslayoutRouteWithChildren,
|
||||
AuthenticateCliRedirectRoute: AuthenticateCliRedirectRoute,
|
||||
}
|
||||
|
||||
const AuthenticateRouteWithChildren = AuthenticateRoute._addFileChildren(
|
||||
AuthenticateRouteChildren,
|
||||
)
|
||||
|
||||
interface RestrictloginsignupRouteChildren {
|
||||
RestrictloginsignupEmailNotVerifiedRoute: typeof RestrictloginsignupEmailNotVerifiedRoute
|
||||
RestrictloginsignupPasswordResetRoute: typeof RestrictloginsignupPasswordResetRoute
|
||||
RestrictloginsignupRequestnewinviteRoute: typeof RestrictloginsignupRequestnewinviteRoute
|
||||
RestrictloginsignupSignupinviteRoute: typeof RestrictloginsignupSignupinviteRoute
|
||||
RestrictloginsignupVerifyEmailRoute: typeof RestrictloginsignupVerifyEmailRoute
|
||||
RestrictloginsignupLoginIndexRoute: typeof RestrictloginsignupLoginIndexRoute
|
||||
RestrictloginsignupSignupIndexRoute: typeof RestrictloginsignupSignupIndexRoute
|
||||
RestrictloginsignupLoginProviderErrorRoute: typeof RestrictloginsignupLoginProviderErrorRoute
|
||||
RestrictloginsignupLoginProviderSuccessRoute: typeof RestrictloginsignupLoginProviderSuccessRoute
|
||||
RestrictloginsignupLoginLdapIndexRoute: typeof RestrictloginsignupLoginLdapIndexRoute
|
||||
RestrictloginsignupLoginSelectOrganizationIndexRoute: typeof RestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
RestrictloginsignupLoginSsoIndexRoute: typeof RestrictloginsignupLoginSsoIndexRoute
|
||||
RestrictloginsignupSignupSsoIndexRoute: typeof RestrictloginsignupSignupSsoIndexRoute
|
||||
}
|
||||
|
||||
const RestrictloginsignupRouteChildren: RestrictloginsignupRouteChildren = {
|
||||
RestrictloginsignupEmailNotVerifiedRoute:
|
||||
RestrictloginsignupEmailNotVerifiedRoute,
|
||||
RestrictloginsignupPasswordResetRoute: RestrictloginsignupPasswordResetRoute,
|
||||
RestrictloginsignupRequestnewinviteRoute:
|
||||
RestrictloginsignupRequestnewinviteRoute,
|
||||
RestrictloginsignupSignupinviteRoute: RestrictloginsignupSignupinviteRoute,
|
||||
RestrictloginsignupVerifyEmailRoute: RestrictloginsignupVerifyEmailRoute,
|
||||
RestrictloginsignupLoginIndexRoute: RestrictloginsignupLoginIndexRoute,
|
||||
RestrictloginsignupSignupIndexRoute: RestrictloginsignupSignupIndexRoute,
|
||||
RestrictloginsignupLoginProviderErrorRoute:
|
||||
RestrictloginsignupLoginProviderErrorRoute,
|
||||
RestrictloginsignupLoginProviderSuccessRoute:
|
||||
RestrictloginsignupLoginProviderSuccessRoute,
|
||||
RestrictloginsignupLoginLdapIndexRoute:
|
||||
RestrictloginsignupLoginLdapIndexRoute,
|
||||
RestrictloginsignupLoginSelectOrganizationIndexRoute:
|
||||
RestrictloginsignupLoginSelectOrganizationIndexRoute,
|
||||
RestrictloginsignupLoginSsoIndexRoute: RestrictloginsignupLoginSsoIndexRoute,
|
||||
RestrictloginsignupSignupSsoIndexRoute:
|
||||
RestrictloginsignupSignupSsoIndexRoute,
|
||||
}
|
||||
|
||||
const RestrictloginsignupRouteWithChildren =
|
||||
RestrictloginsignupRoute._addFileChildren(RestrictloginsignupRouteChildren)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'': typeof AuthenticateOrgdetailsOrganizationlayoutRouteWithChildren
|
||||
'/login': typeof AuthenticateRestrictloginsignupLoginIndexRoute
|
||||
'/signup': typeof AuthenticateRestrictloginsignupSignupIndexRoute
|
||||
'/login/provider/error': typeof AuthenticateRestrictloginsignupLoginProviderErrorRoute
|
||||
'/login/provider/success': typeof AuthenticateRestrictloginsignupLoginProviderSuccessRoute
|
||||
'/cli-redirect': typeof AuthenticateCliRedirectRoute
|
||||
'/email-not-verified': typeof RestrictloginsignupEmailNotVerifiedRoute
|
||||
'/password-reset': typeof RestrictloginsignupPasswordResetRoute
|
||||
'/requestnewinvite': typeof RestrictloginsignupRequestnewinviteRoute
|
||||
'/signupinvite': typeof RestrictloginsignupSignupinviteRoute
|
||||
'/verify-email': typeof RestrictloginsignupVerifyEmailRoute
|
||||
'/login': typeof RestrictloginsignupLoginIndexRoute
|
||||
'/signup': typeof RestrictloginsignupSignupIndexRoute
|
||||
'/login/provider/error': typeof RestrictloginsignupLoginProviderErrorRoute
|
||||
'/login/provider/success': typeof RestrictloginsignupLoginProviderSuccessRoute
|
||||
'/personal-settings': typeof AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute
|
||||
'/login/ldap': typeof RestrictloginsignupLoginLdapIndexRoute
|
||||
'/login/select-organization': typeof RestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
'/login/sso': typeof RestrictloginsignupLoginSsoIndexRoute
|
||||
'/signup/sso': typeof RestrictloginsignupSignupSsoIndexRoute
|
||||
'/organization': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexRoute
|
||||
'/login/ldap': typeof AuthenticateRestrictloginsignupLoginLdapIndexRoute
|
||||
'/login/select-organization': typeof AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
'/login/sso': typeof AuthenticateRestrictloginsignupLoginSsoIndexRoute
|
||||
'/signup/sso': typeof AuthenticateRestrictloginsignupSignupSsoIndexRoute
|
||||
'/organization/$organizationId/secret-manager': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdSecretManagerRoute
|
||||
'/organization/$organizationId': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdIndexRoute
|
||||
}
|
||||
@@ -366,15 +513,22 @@ export interface FileRoutesByFullPath {
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'': typeof AuthenticateOrgdetailsOrganizationlayoutRouteWithChildren
|
||||
'/login': typeof AuthenticateRestrictloginsignupLoginIndexRoute
|
||||
'/signup': typeof AuthenticateRestrictloginsignupSignupIndexRoute
|
||||
'/login/provider/error': typeof AuthenticateRestrictloginsignupLoginProviderErrorRoute
|
||||
'/login/provider/success': typeof AuthenticateRestrictloginsignupLoginProviderSuccessRoute
|
||||
'/cli-redirect': typeof AuthenticateCliRedirectRoute
|
||||
'/email-not-verified': typeof RestrictloginsignupEmailNotVerifiedRoute
|
||||
'/password-reset': typeof RestrictloginsignupPasswordResetRoute
|
||||
'/requestnewinvite': typeof RestrictloginsignupRequestnewinviteRoute
|
||||
'/signupinvite': typeof RestrictloginsignupSignupinviteRoute
|
||||
'/verify-email': typeof RestrictloginsignupVerifyEmailRoute
|
||||
'/login': typeof RestrictloginsignupLoginIndexRoute
|
||||
'/signup': typeof RestrictloginsignupSignupIndexRoute
|
||||
'/login/provider/error': typeof RestrictloginsignupLoginProviderErrorRoute
|
||||
'/login/provider/success': typeof RestrictloginsignupLoginProviderSuccessRoute
|
||||
'/personal-settings': typeof AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute
|
||||
'/login/ldap': typeof RestrictloginsignupLoginLdapIndexRoute
|
||||
'/login/select-organization': typeof RestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
'/login/sso': typeof RestrictloginsignupLoginSsoIndexRoute
|
||||
'/signup/sso': typeof RestrictloginsignupSignupSsoIndexRoute
|
||||
'/organization': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexRoute
|
||||
'/login/ldap': typeof AuthenticateRestrictloginsignupLoginLdapIndexRoute
|
||||
'/login/select-organization': typeof AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
'/login/sso': typeof AuthenticateRestrictloginsignupLoginSsoIndexRoute
|
||||
'/signup/sso': typeof AuthenticateRestrictloginsignupSignupSsoIndexRoute
|
||||
'/organization/$organizationId/secret-manager': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdSecretManagerRoute
|
||||
'/organization/$organizationId': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdIndexRoute
|
||||
}
|
||||
@@ -383,18 +537,26 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexRoute
|
||||
'/_authenticate': typeof AuthenticateRouteWithChildren
|
||||
'/_restrict_login_signup': typeof RestrictloginsignupRouteWithChildren
|
||||
'/_authenticate/_org_details': typeof AuthenticateOrgdetailsRouteWithChildren
|
||||
'/_authenticate/_restrict_login_signup': typeof AuthenticateRestrictloginsignupRouteWithChildren
|
||||
'/_authenticate/_personal_settings_layout': typeof AuthenticatePersonalsettingslayoutRouteWithChildren
|
||||
'/_authenticate/cli-redirect': typeof AuthenticateCliRedirectRoute
|
||||
'/_restrict_login_signup/email-not-verified': typeof RestrictloginsignupEmailNotVerifiedRoute
|
||||
'/_restrict_login_signup/password-reset': typeof RestrictloginsignupPasswordResetRoute
|
||||
'/_restrict_login_signup/requestnewinvite': typeof RestrictloginsignupRequestnewinviteRoute
|
||||
'/_restrict_login_signup/signupinvite': typeof RestrictloginsignupSignupinviteRoute
|
||||
'/_restrict_login_signup/verify-email': typeof RestrictloginsignupVerifyEmailRoute
|
||||
'/_authenticate/_org_details/_organization_layout': typeof AuthenticateOrgdetailsOrganizationlayoutRouteWithChildren
|
||||
'/_authenticate/_restrict_login_signup/login/': typeof AuthenticateRestrictloginsignupLoginIndexRoute
|
||||
'/_authenticate/_restrict_login_signup/signup/': typeof AuthenticateRestrictloginsignupSignupIndexRoute
|
||||
'/_authenticate/_restrict_login_signup/login/provider/error': typeof AuthenticateRestrictloginsignupLoginProviderErrorRoute
|
||||
'/_authenticate/_restrict_login_signup/login/provider/success': typeof AuthenticateRestrictloginsignupLoginProviderSuccessRoute
|
||||
'/_restrict_login_signup/login/': typeof RestrictloginsignupLoginIndexRoute
|
||||
'/_restrict_login_signup/signup/': typeof RestrictloginsignupSignupIndexRoute
|
||||
'/_restrict_login_signup/login/provider/error': typeof RestrictloginsignupLoginProviderErrorRoute
|
||||
'/_restrict_login_signup/login/provider/success': typeof RestrictloginsignupLoginProviderSuccessRoute
|
||||
'/_authenticate/_personal_settings_layout/personal-settings/': typeof AuthenticatePersonalsettingslayoutPersonalSettingsIndexRoute
|
||||
'/_restrict_login_signup/login/ldap/': typeof RestrictloginsignupLoginLdapIndexRoute
|
||||
'/_restrict_login_signup/login/select-organization/': typeof RestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
'/_restrict_login_signup/login/sso/': typeof RestrictloginsignupLoginSsoIndexRoute
|
||||
'/_restrict_login_signup/signup/sso/': typeof RestrictloginsignupSignupSsoIndexRoute
|
||||
'/_authenticate/_org_details/_organization_layout/organization/': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationIndexRoute
|
||||
'/_authenticate/_restrict_login_signup/login/ldap/': typeof AuthenticateRestrictloginsignupLoginLdapIndexRoute
|
||||
'/_authenticate/_restrict_login_signup/login/select-organization/': typeof AuthenticateRestrictloginsignupLoginSelectOrganizationIndexRoute
|
||||
'/_authenticate/_restrict_login_signup/login/sso/': typeof AuthenticateRestrictloginsignupLoginSsoIndexRoute
|
||||
'/_authenticate/_restrict_login_signup/signup/sso/': typeof AuthenticateRestrictloginsignupSignupSsoIndexRoute
|
||||
'/_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdSecretManagerRoute
|
||||
'/_authenticate/_org_details/_organization_layout/organization/$organizationId/': typeof AuthenticateOrgdetailsOrganizationlayoutOrganizationOrganizationIdIndexRoute
|
||||
}
|
||||
@@ -404,48 +566,70 @@ export interface FileRouteTypes {
|
||||
fullPaths:
|
||||
| '/'
|
||||
| ''
|
||||
| '/cli-redirect'
|
||||
| '/email-not-verified'
|
||||
| '/password-reset'
|
||||
| '/requestnewinvite'
|
||||
| '/signupinvite'
|
||||
| '/verify-email'
|
||||
| '/login'
|
||||
| '/signup'
|
||||
| '/login/provider/error'
|
||||
| '/login/provider/success'
|
||||
| '/organization'
|
||||
| '/personal-settings'
|
||||
| '/login/ldap'
|
||||
| '/login/select-organization'
|
||||
| '/login/sso'
|
||||
| '/signup/sso'
|
||||
| '/organization'
|
||||
| '/organization/$organizationId/secret-manager'
|
||||
| '/organization/$organizationId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| ''
|
||||
| '/cli-redirect'
|
||||
| '/email-not-verified'
|
||||
| '/password-reset'
|
||||
| '/requestnewinvite'
|
||||
| '/signupinvite'
|
||||
| '/verify-email'
|
||||
| '/login'
|
||||
| '/signup'
|
||||
| '/login/provider/error'
|
||||
| '/login/provider/success'
|
||||
| '/organization'
|
||||
| '/personal-settings'
|
||||
| '/login/ldap'
|
||||
| '/login/select-organization'
|
||||
| '/login/sso'
|
||||
| '/signup/sso'
|
||||
| '/organization'
|
||||
| '/organization/$organizationId/secret-manager'
|
||||
| '/organization/$organizationId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_authenticate'
|
||||
| '/_restrict_login_signup'
|
||||
| '/_authenticate/_org_details'
|
||||
| '/_authenticate/_restrict_login_signup'
|
||||
| '/_authenticate/_personal_settings_layout'
|
||||
| '/_authenticate/cli-redirect'
|
||||
| '/_restrict_login_signup/email-not-verified'
|
||||
| '/_restrict_login_signup/password-reset'
|
||||
| '/_restrict_login_signup/requestnewinvite'
|
||||
| '/_restrict_login_signup/signupinvite'
|
||||
| '/_restrict_login_signup/verify-email'
|
||||
| '/_authenticate/_org_details/_organization_layout'
|
||||
| '/_authenticate/_restrict_login_signup/login/'
|
||||
| '/_authenticate/_restrict_login_signup/signup/'
|
||||
| '/_authenticate/_restrict_login_signup/login/provider/error'
|
||||
| '/_authenticate/_restrict_login_signup/login/provider/success'
|
||||
| '/_restrict_login_signup/login/'
|
||||
| '/_restrict_login_signup/signup/'
|
||||
| '/_restrict_login_signup/login/provider/error'
|
||||
| '/_restrict_login_signup/login/provider/success'
|
||||
| '/_authenticate/_personal_settings_layout/personal-settings/'
|
||||
| '/_restrict_login_signup/login/ldap/'
|
||||
| '/_restrict_login_signup/login/select-organization/'
|
||||
| '/_restrict_login_signup/login/sso/'
|
||||
| '/_restrict_login_signup/signup/sso/'
|
||||
| '/_authenticate/_org_details/_organization_layout/organization/'
|
||||
| '/_authenticate/_restrict_login_signup/login/ldap/'
|
||||
| '/_authenticate/_restrict_login_signup/login/select-organization/'
|
||||
| '/_authenticate/_restrict_login_signup/login/sso/'
|
||||
| '/_authenticate/_restrict_login_signup/signup/sso/'
|
||||
| '/_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager'
|
||||
| '/_authenticate/_org_details/_organization_layout/organization/$organizationId/'
|
||||
fileRoutesById: FileRoutesById
|
||||
@@ -454,11 +638,13 @@ export interface FileRouteTypes {
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AuthenticateRoute: typeof AuthenticateRouteWithChildren
|
||||
RestrictloginsignupRoute: typeof RestrictloginsignupRouteWithChildren
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AuthenticateRoute: AuthenticateRouteWithChildren,
|
||||
RestrictloginsignupRoute: RestrictloginsignupRouteWithChildren,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
@@ -472,7 +658,8 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/_authenticate"
|
||||
"/_authenticate",
|
||||
"/_restrict_login_signup"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
@@ -482,7 +669,26 @@ export const routeTree = rootRoute
|
||||
"filePath": "_authenticate.tsx",
|
||||
"children": [
|
||||
"/_authenticate/_org_details",
|
||||
"/_authenticate/_restrict_login_signup"
|
||||
"/_authenticate/_personal_settings_layout",
|
||||
"/_authenticate/cli-redirect"
|
||||
]
|
||||
},
|
||||
"/_restrict_login_signup": {
|
||||
"filePath": "_restrict_login_signup.tsx",
|
||||
"children": [
|
||||
"/_restrict_login_signup/email-not-verified",
|
||||
"/_restrict_login_signup/password-reset",
|
||||
"/_restrict_login_signup/requestnewinvite",
|
||||
"/_restrict_login_signup/signupinvite",
|
||||
"/_restrict_login_signup/verify-email",
|
||||
"/_restrict_login_signup/login/",
|
||||
"/_restrict_login_signup/signup/",
|
||||
"/_restrict_login_signup/login/provider/error",
|
||||
"/_restrict_login_signup/login/provider/success",
|
||||
"/_restrict_login_signup/login/ldap/",
|
||||
"/_restrict_login_signup/login/select-organization/",
|
||||
"/_restrict_login_signup/login/sso/",
|
||||
"/_restrict_login_signup/signup/sso/"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_org_details": {
|
||||
@@ -492,20 +698,37 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_org_details/_organization_layout"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup": {
|
||||
"filePath": "_authenticate/_restrict_login_signup.tsx",
|
||||
"/_authenticate/_personal_settings_layout": {
|
||||
"filePath": "_authenticate/_personal_settings_layout.tsx",
|
||||
"parent": "/_authenticate",
|
||||
"children": [
|
||||
"/_authenticate/_restrict_login_signup/login/",
|
||||
"/_authenticate/_restrict_login_signup/signup/",
|
||||
"/_authenticate/_restrict_login_signup/login/provider/error",
|
||||
"/_authenticate/_restrict_login_signup/login/provider/success",
|
||||
"/_authenticate/_restrict_login_signup/login/ldap/",
|
||||
"/_authenticate/_restrict_login_signup/login/select-organization/",
|
||||
"/_authenticate/_restrict_login_signup/login/sso/",
|
||||
"/_authenticate/_restrict_login_signup/signup/sso/"
|
||||
"/_authenticate/_personal_settings_layout/personal-settings/"
|
||||
]
|
||||
},
|
||||
"/_authenticate/cli-redirect": {
|
||||
"filePath": "_authenticate/cli-redirect.tsx",
|
||||
"parent": "/_authenticate"
|
||||
},
|
||||
"/_restrict_login_signup/email-not-verified": {
|
||||
"filePath": "_restrict_login_signup/email-not-verified.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/password-reset": {
|
||||
"filePath": "_restrict_login_signup/password-reset.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/requestnewinvite": {
|
||||
"filePath": "_restrict_login_signup/requestnewinvite.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/signupinvite": {
|
||||
"filePath": "_restrict_login_signup/signupinvite.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/verify-email": {
|
||||
"filePath": "_restrict_login_signup/verify-email.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_org_details/_organization_layout": {
|
||||
"filePath": "_authenticate/_org_details/_organization_layout.tsx",
|
||||
"parent": "/_authenticate/_org_details",
|
||||
@@ -515,42 +738,46 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_org_details/_organization_layout/organization/$organizationId/"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/login/": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/login/index.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
"/_restrict_login_signup/login/": {
|
||||
"filePath": "_restrict_login_signup/login/index.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/signup/": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/signup/index.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
"/_restrict_login_signup/signup/": {
|
||||
"filePath": "_restrict_login_signup/signup/index.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/login/provider/error": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/login/provider/error.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
"/_restrict_login_signup/login/provider/error": {
|
||||
"filePath": "_restrict_login_signup/login/provider/error.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/login/provider/success": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/login/provider/success.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
"/_restrict_login_signup/login/provider/success": {
|
||||
"filePath": "_restrict_login_signup/login/provider/success.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_personal_settings_layout/personal-settings/": {
|
||||
"filePath": "_authenticate/_personal_settings_layout/personal-settings/index.tsx",
|
||||
"parent": "/_authenticate/_personal_settings_layout"
|
||||
},
|
||||
"/_restrict_login_signup/login/ldap/": {
|
||||
"filePath": "_restrict_login_signup/login/ldap/index.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/login/select-organization/": {
|
||||
"filePath": "_restrict_login_signup/login/select-organization/index.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/login/sso/": {
|
||||
"filePath": "_restrict_login_signup/login/sso/index.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_restrict_login_signup/signup/sso/": {
|
||||
"filePath": "_restrict_login_signup/signup/sso/index.tsx",
|
||||
"parent": "/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_org_details/_organization_layout/organization/": {
|
||||
"filePath": "_authenticate/_org_details/_organization_layout/organization/index.tsx",
|
||||
"parent": "/_authenticate/_org_details/_organization_layout"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/login/ldap/": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/login/ldap/index.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/login/select-organization/": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/login/select-organization/index.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/login/sso/": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/login/sso/index.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_restrict_login_signup/signup/sso/": {
|
||||
"filePath": "_authenticate/_restrict_login_signup/signup/sso/index.tsx",
|
||||
"parent": "/_authenticate/_restrict_login_signup"
|
||||
},
|
||||
"/_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager": {
|
||||
"filePath": "_authenticate/_org_details/_organization_layout/organization/$organizationId/secret-manager.tsx",
|
||||
"parent": "/_authenticate/_org_details/_organization_layout"
|
||||
|
||||
@@ -7,9 +7,7 @@ import { setAuthToken } from "@app/hooks/api/reactQuery";
|
||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||
|
||||
export const Route = createFileRoute("/_authenticate")({
|
||||
beforeLoad: async ({ context, location }) => {
|
||||
const isLoginRoute = location.pathname.startsWith("/login");
|
||||
const isSignupRoute = location.pathname.startsWith("/signup");
|
||||
beforeLoad: async ({ context }) => {
|
||||
try {
|
||||
const data = await context.queryClient.fetchQuery({
|
||||
queryKey: authKeys.getAuthToken,
|
||||
@@ -27,7 +25,6 @@ export const Route = createFileRoute("/_authenticate")({
|
||||
|
||||
return { organizationId: data.organizationId as string, isAuthenticated: true, user };
|
||||
} catch {
|
||||
if (isLoginRoute || isSignupRoute) return {};
|
||||
createNotification({
|
||||
type: "error",
|
||||
title: "Access Restricted",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { PersonalSettingsLayout } from "@app/layouts/PersonalSettingsLayout";
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_personal_settings_layout")({
|
||||
component: PersonalSettingsLayout
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddAPIKeyModal } from "./AddAPIKeyModal";
|
||||
import { APIKeyTable } from "./APIKeyTable";
|
||||
|
||||
export const APIKeySection = () => {
|
||||
const { t } = useTranslation();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addAPIKey"] as const);
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-8 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
{t("settings.personal.api-keys.title")}
|
||||
</p>
|
||||
<Tooltip content="API Keys are deprecated and will be removed in the future.">
|
||||
<Button
|
||||
isDisabled
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addAPIKey")}
|
||||
>
|
||||
Add API Key
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<APIKeyTable />
|
||||
<AddAPIKeyModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useDeleteAPIKey, useGetMyAPIKeys } from "@app/hooks/api";
|
||||
|
||||
export const APIKeyTable = () => {
|
||||
const { data, isLoading } = useGetMyAPIKeys();
|
||||
const { mutateAsync } = useDeleteAPIKey();
|
||||
|
||||
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
|
||||
try {
|
||||
await mutateAsync(apiKeyDataId);
|
||||
createNotification({
|
||||
text: "Successfully deleted API key",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete API key",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Name</Th>
|
||||
<Th className="flex-1">Last active</Th>
|
||||
<Th className="flex-1">Created</Th>
|
||||
<Th className="flex-1">Expiration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({ id, name, createdAt, expiresAt, lastUsed }) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`api-key-${id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{format(new Date(lastUsed), "yyyy-MM-dd")}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td>{format(new Date(expiresAt), "yyyy-MM-dd")}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteAPIKeyDataClick(id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No API Keys on file" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useCreateAPIKey } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const expirations = [
|
||||
{ label: "1 day", value: "1d" },
|
||||
{ label: "7 days", value: "7d" },
|
||||
{ label: "1 month", value: "1mo" },
|
||||
{ label: "6 months", value: "6mo" },
|
||||
{ label: "12 months", value: "12mo" }
|
||||
];
|
||||
|
||||
const expirationMapping: { [key: string]: number } = {
|
||||
"1d": 86400,
|
||||
"7d": 604800,
|
||||
"1mo": 2592000,
|
||||
"6mo": 15552000,
|
||||
"12mo": 31104000
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
expiresIn: z.string()
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["addAPIKey"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addAPIKey"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddAPIKeyModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [newAPIKey, setNewAPIKey] = useState("");
|
||||
const [isAPIKeyCopied, setIsAPIKeyCopied] = useToggle(false);
|
||||
|
||||
const { mutateAsync, isLoading } = useCreateAPIKey();
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
if (isAPIKeyCopied) {
|
||||
timer = setTimeout(() => setIsAPIKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [setIsAPIKeyCopied]);
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
navigator.clipboard.writeText(newAPIKey);
|
||||
setIsAPIKeyCopied.on();
|
||||
};
|
||||
|
||||
const onFormSubmit = async ({ name, expiresIn }: FormData) => {
|
||||
try {
|
||||
const { apiKey } = await mutateAsync({
|
||||
name,
|
||||
expiresIn: expirationMapping[expiresIn]
|
||||
});
|
||||
|
||||
setNewAPIKey(apiKey);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created API key",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create API key",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasAPIKey = Boolean(newAPIKey);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addAPIKey?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addAPIKey", isOpen);
|
||||
reset();
|
||||
setNewAPIKey("");
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Create API Key">
|
||||
{!hasAPIKey ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="My API Key" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue="6mo"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expirations.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("addAPIKey", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mb-3 mr-2 mt-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{newAPIKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isAPIKeyCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { APIKeySection } from "./APIKeySection";
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { faGithub, faGitlab, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Switch } from "@app/components/v2";
|
||||
import { useServerConfig, useUser } from "@app/context";
|
||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||
import { LoginMethod } from "@app/hooks/api/admin/types";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
|
||||
interface AuthMethodOption {
|
||||
label: string;
|
||||
value: AuthMethod;
|
||||
icon: IconDefinition;
|
||||
loginMethod: LoginMethod;
|
||||
}
|
||||
|
||||
const authMethodOpts: AuthMethodOption[] = [
|
||||
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope, loginMethod: LoginMethod.EMAIL },
|
||||
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle, loginMethod: LoginMethod.GOOGLE },
|
||||
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub, loginMethod: LoginMethod.GITHUB },
|
||||
{ label: "GitLab", value: AuthMethod.GITLAB, icon: faGitlab, loginMethod: LoginMethod.GITLAB }
|
||||
];
|
||||
const schema = z.object({
|
||||
authMethods: z.nativeEnum(AuthMethod).array()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const AuthMethodSection = () => {
|
||||
const { user } = useUser();
|
||||
const { config } = useServerConfig();
|
||||
const { mutateAsync } = useUpdateUserAuthMethods();
|
||||
|
||||
const { reset, setValue, watch } = useForm<FormData>({
|
||||
defaultValues: {
|
||||
authMethods: user.authMethods
|
||||
},
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const authMethods = watch("authMethods");
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({
|
||||
authMethods: user.authMethods
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
|
||||
const newAuthMethods = value
|
||||
? [...authMethods, authMethodOpt.value]
|
||||
: authMethods.filter((auth) => auth !== authMethodOpt.value);
|
||||
|
||||
if (value) {
|
||||
const newUser = await mutateAsync({
|
||||
authMethods: newAuthMethods
|
||||
});
|
||||
|
||||
setValue("authMethods", newUser.authMethods);
|
||||
createNotification({
|
||||
text: "Successfully enabled authentication method",
|
||||
type: "success"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newAuthMethods.length === 0) {
|
||||
createNotification({
|
||||
text: "You must keep at least 1 authentication method enabled",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newUser = await mutateAsync({
|
||||
authMethods: newAuthMethods
|
||||
});
|
||||
|
||||
setValue("authMethods", newUser.authMethods);
|
||||
createNotification({
|
||||
text: "Successfully disabled authentication method",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">
|
||||
Authentication methods
|
||||
</h2>
|
||||
<p className="mb-4 text-gray-400">
|
||||
By enabling a SSO provider, you are allowing an account with that provider which uses the
|
||||
same email address as your existing Infisical account to be able to log in to Infisical.
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
{user &&
|
||||
authMethodOpts.map((authMethodOpt) => {
|
||||
// only filter when enabledLoginMethods is explicitly configured by admin
|
||||
if (
|
||||
config.enabledLoginMethods &&
|
||||
!config.enabledLoginMethods.includes(authMethodOpt.loginMethod)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center p-4" key={`auth-method-${authMethodOpt.value}`}>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
|
||||
</div>
|
||||
<Switch
|
||||
id={`enable-${authMethodOpt.value}-auth`}
|
||||
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
|
||||
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
|
||||
>
|
||||
<p className="mr-4 w-12">{authMethodOpt.label}</p>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AuthMethodSection } from "./AuthMethodSection";
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptChangePassword from "@app/components/utilities/attemptChangePassword";
|
||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
|
||||
type Errors = {
|
||||
tooShort?: string;
|
||||
tooLong?: string;
|
||||
noLetterChar?: string;
|
||||
noNumOrSpecialChar?: string;
|
||||
repeatedChar?: string;
|
||||
escapeChar?: string;
|
||||
lowEntropy?: string;
|
||||
breached?: string;
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
oldPassword: z.string(),
|
||||
newPassword: z.string()
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const ChangePasswordSection = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { user } = useUser();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
oldPassword: "",
|
||||
newPassword: ""
|
||||
},
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
||||
try {
|
||||
const errorCheck = await checkPassword({
|
||||
password: newPassword,
|
||||
setErrors
|
||||
});
|
||||
|
||||
if (errorCheck) return;
|
||||
|
||||
setIsLoading(true);
|
||||
await attemptChangePassword({
|
||||
email: user.username,
|
||||
currentPassword: oldPassword,
|
||||
newPassword
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
createNotification({
|
||||
text: "Successfully changed password",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
window.location.href = "/login";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setIsLoading(false);
|
||||
createNotification({
|
||||
text: "Failed to change password",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Change password</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Old password"
|
||||
type="password"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="oldPassword"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="New password"
|
||||
type="password"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="newPassword"
|
||||
/>
|
||||
</div>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="my-4 flex max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
|
||||
{Object.keys(errors).map((key) => {
|
||||
if (errors[key as keyof Errors]) {
|
||||
return (
|
||||
<div className="items-top ml-1 flex flex-row justify-start" key={key}>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-md ml-0.5 mr-2.5 text-red" />
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ChangePasswordSection } from "./ChangePasswordSection";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useDeleteMe } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const DeleteAccountSection = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteAccount"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteMe();
|
||||
|
||||
const handleDeleteAccountSubmit = async () => {
|
||||
try {
|
||||
await deleteUserMutateAsync();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted account",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
navigate({ to: "/login" });
|
||||
handlePopUpClose("deleteAccount");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete account",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handlePopUpOpen("deleteAccount")}
|
||||
>
|
||||
Delete my account
|
||||
</Button>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteAccount.isOpen}
|
||||
title="Are you sure want to delete your account?"
|
||||
subTitle="Permanently remove this account and all of its data. This action is not reversible, so please be careful."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteAccountSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { DeleteAccountSection } from "./DeleteAccountSection";
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
password: z.string().describe("Password is required")
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const EmergencyKitSection = () => {
|
||||
const { user } = useUser();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
password: ""
|
||||
},
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = ({ password }: FormData) => {
|
||||
try {
|
||||
if (!user?.email) return;
|
||||
|
||||
issueBackupKey({
|
||||
email: user.email,
|
||||
password,
|
||||
personalName: `${user.firstName} ${user.lastName}`,
|
||||
setBackupKeyError: () => {},
|
||||
setBackupKeyIssued: () => {}
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to download emergency kit",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<h2 className="flex-1 text-xl font-semibold text-mineshaft-100">Emergency Kit</h2>
|
||||
<p className="mb-8 text-gray-400">
|
||||
The kit contains information you can use to recover your account.
|
||||
</p>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" colorSchema="secondary" isLoading={false}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { EmergencyKitSection } from "./EmergencyKitSection";
|
||||
@@ -0,0 +1,5 @@
|
||||
import { APIKeySection } from "../APIKeySection";
|
||||
|
||||
export const PersonalAPIKeyTab = () => {
|
||||
return <APIKeySection />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PersonalAPIKeyTab } from "./PersonalAPIKeyTab";
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useGetUser } from "@app/hooks/api";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
|
||||
import { AuthMethodSection } from "../AuthMethodSection";
|
||||
import { ChangePasswordSection } from "../ChangePasswordSection";
|
||||
import { MFASection } from "../SecuritySection";
|
||||
|
||||
export const PersonalAuthTab = () => {
|
||||
const { data: user } = useGetUser();
|
||||
return (
|
||||
<div>
|
||||
{user && !user.authMethods.includes(AuthMethod.LDAP) && (
|
||||
<>
|
||||
<MFASection />
|
||||
<AuthMethodSection />
|
||||
</>
|
||||
)}
|
||||
<ChangePasswordSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PersonalAuthTab } from "./PersonalAuthTab";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DeleteAccountSection } from "../DeleteAccountSection";
|
||||
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||
import { SessionsSection } from "../SessionsSection";
|
||||
import { UserNameSection } from "../UserNameSection";
|
||||
|
||||
export const PersonalGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<UserNameSection />
|
||||
<SessionsSection />
|
||||
<EmergencyKitSection />
|
||||
<DeleteAccountSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PersonalGeneralTab } from "./PersonalGeneralTab";
|
||||
@@ -0,0 +1,67 @@
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
|
||||
import { PersonalAuthTab } from "../PersonalAuthTab";
|
||||
import { PersonalGeneralTab } from "../PersonalGeneralTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-account-general" },
|
||||
{ name: "Authentication", key: "tab-account-auth" },
|
||||
{ name: "API Keys", key: "tab-account-api-keys" }
|
||||
];
|
||||
|
||||
export const PersonalTabGroup = () => {
|
||||
return (
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key}>{tab.name}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanel value={tabs[0].key}>
|
||||
<PersonalGeneralTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={tabs[1].key}>
|
||||
<PersonalAuthTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={tabs[2].key}>
|
||||
<div className="space-y-3">
|
||||
<div className="mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
|
||||
<FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" />
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<span className="mb-4 text-lg font-semibold">Deprecation Notice</span>
|
||||
<p>
|
||||
API Keys are being deprecated in favor of Machine Identities.
|
||||
<br />
|
||||
They will be removed in the future in accordance with the deprecation notice and
|
||||
timeline stated{" "}
|
||||
<a
|
||||
href="https://infisical.com/blog/deprecating-api-keys"
|
||||
target="_blank"
|
||||
className="font-semibold text-primary-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
<br />
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
className="font-semibold text-primary-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more about Machine Identities
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PersonalAPIKeyTab />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PersonalTabGroup } from "./PersonalTabGroup";
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import TotpRegistration from "@app/components/mfa/TotpRegistration";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
ContentLoader,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetUser, userKeys, useUpdateUserMfa } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import {
|
||||
useCreateNewTotpRecoveryCodes,
|
||||
useDeleteUserTotpConfiguration
|
||||
} from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserTotpConfiguration } from "@app/hooks/api/users/queries";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const MFASection = () => {
|
||||
const { data: user } = useGetUser();
|
||||
const { mutateAsync } = useUpdateUserMfa();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"setUpEmail",
|
||||
"deleteTotpConfig"
|
||||
] as const);
|
||||
const [shouldShowRecoveryCodes, setShouldShowRecoveryCodes] = useToggle();
|
||||
const { data: totpConfiguration, isLoading: isTotpConfigurationLoading } =
|
||||
useGetUserTotpConfiguration();
|
||||
const { mutateAsync: deleteTotpConfiguration } = useDeleteUserTotpConfiguration();
|
||||
const { mutateAsync: createTotpRecoveryCodes } = useCreateNewTotpRecoveryCodes();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
const handleTotpDeletion = async () => {
|
||||
try {
|
||||
await deleteTotpConfiguration();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted mobile authenticator",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteTotpConfig");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to delete mobile authenticator";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateMoreRecoveryCodes = async () => {
|
||||
try {
|
||||
await createTotpRecoveryCodes();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully generated new recovery codes",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to generate new recovery codes";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateSelectedMfa = async (mfaMethod: MfaMethod) => {
|
||||
try {
|
||||
if (!user) return;
|
||||
|
||||
await mutateAsync({
|
||||
selectedMfaMethod: mfaMethod
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated selected 2FA method",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Something went wrong while updating selected 2FA method.",
|
||||
type: "error"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMfa = async (state: boolean) => {
|
||||
try {
|
||||
if (!user) return;
|
||||
if (user.authMethods.includes(AuthMethod.LDAP)) {
|
||||
createNotification({
|
||||
text: "Two-factor authentication is not available for LDAP users.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newUser = await mutateAsync({
|
||||
isMfaEnabled: state
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `${
|
||||
newUser.isMfaEnabled
|
||||
? "Successfully turned on two-factor authentication."
|
||||
: "Successfully turned off two-factor authentication."
|
||||
}`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Something went wrong while toggling the two-factor authentication.",
|
||||
type: "error"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 max-w-6xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Two-factor Authentication</p>
|
||||
{user && (
|
||||
<Switch
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="isTwoFAEnabled"
|
||||
isChecked={user?.isMfaEnabled}
|
||||
onCheckedChange={(state) => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
toggleMfa(state as boolean);
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Enable 2-factor authentication
|
||||
</Switch>
|
||||
)}
|
||||
{user?.isMfaEnabled && (
|
||||
<FormControl label="Selected 2FA method" className="mt-3">
|
||||
<Select
|
||||
className="min-w-[20rem] border border-mineshaft-500"
|
||||
onValueChange={updateSelectedMfa}
|
||||
defaultValue={user.selectedMfaMethod ?? MfaMethod.EMAIL}
|
||||
>
|
||||
<SelectItem value={MfaMethod.EMAIL} key="mfa-method-email">
|
||||
Email
|
||||
</SelectItem>
|
||||
<SelectItem value={MfaMethod.TOTP} key="mfa-method-totp">
|
||||
Mobile Authenticator
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<div className="mt-8 text-lg font-semibold text-mineshaft-100">Mobile Authenticator</div>
|
||||
{isTotpConfigurationLoading ? (
|
||||
<ContentLoader />
|
||||
) : (
|
||||
<div>
|
||||
{totpConfiguration?.isVerified ? (
|
||||
<div className="mt-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button colorSchema="secondary" onClick={setShouldShowRecoveryCodes.toggle}>
|
||||
{shouldShowRecoveryCodes ? "Hide recovery codes" : "Show recovery codes"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" onClick={handleGenerateMoreRecoveryCodes}>
|
||||
Generate more codes
|
||||
</Button>
|
||||
<Button colorSchema="danger" onClick={() => handlePopUpOpen("deleteTotpConfig")}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{shouldShowRecoveryCodes && totpConfiguration.recoveryCodes && (
|
||||
<div className="mt-4 bg-mineshaft-600 p-4">
|
||||
{totpConfiguration.recoveryCodes.map((code) => (
|
||||
<div key={code}>{code}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm text-gray-400">
|
||||
For added security, you can configure a mobile authenticator and set it as your
|
||||
selected 2FA method.
|
||||
</div>
|
||||
<div className="ml-6 mt-6 flex min-w-full">
|
||||
<TotpRegistration
|
||||
onComplete={async () => {
|
||||
await queryClient.invalidateQueries(userKeys.totpConfiguration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTotpConfig.isOpen}
|
||||
title="Are you sure want to delete the configured authenticator?"
|
||||
subTitle="This action is irreversible. You’ll have to go through the setup process to enable it again."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTotpConfig", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleTotpDeletion}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MFASection } from "./MFASection";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { faBan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useRevokeMySessions } from "@app/hooks/api";
|
||||
|
||||
import { SessionsTable } from "./SessionsTable";
|
||||
|
||||
export const SessionsSection = () => {
|
||||
const { mutateAsync } = useRevokeMySessions();
|
||||
|
||||
const onRevokeAllSessionsClick = async () => {
|
||||
try {
|
||||
await mutateAsync();
|
||||
window.location.href = "/login";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-8 flex justify-between">
|
||||
<h2 className="flex-1 text-xl font-semibold text-mineshaft-100">Sessions</h2>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faBan} />}
|
||||
onClick={onRevokeAllSessionsClick}
|
||||
>
|
||||
Revoke all
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">
|
||||
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your
|
||||
account out all active sessions across all browsers and CLIs.
|
||||
</p>
|
||||
<SessionsTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useGetMySessions } from "@app/hooks/api";
|
||||
|
||||
export const SessionsTable = () => {
|
||||
const { data, isLoading } = useGetMySessions();
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
|
||||
const formattedDate = `${day}/${month}/${year}`;
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Created</Th>
|
||||
<Th>Last active</Th>
|
||||
<Th>IP address</Th>
|
||||
<Th>Device</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="sesssions" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`session-${id}`}>
|
||||
<Td>{formatDate(createdAt)}</Td>
|
||||
<Td>{formatDate(lastUsed)}</Td>
|
||||
<Td>{ip}</Td>
|
||||
<Td>{userAgent}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState title="No sessions on file" icon={faServer} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SessionsSection } from "./SessionsSection";
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useRenameUser } from "@app/hooks/api/users/queries";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().describe("User Name")
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const UserNameSection = (): JSX.Element => {
|
||||
const { user } = useUser();
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
const { mutateAsync, isLoading } = useRenameUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({ name: `${user?.firstName}${user?.lastName && " "}${user?.lastName}` });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!user?.id) return;
|
||||
if (name === "") return;
|
||||
|
||||
await mutateAsync({ newName: name });
|
||||
createNotification({
|
||||
text: "Successfully renamed user",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to rename user",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Name</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder={`${user?.firstName} ${user?.lastName}`} {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button isLoading={isLoading} colorSchema="primary" variant="outline_bg" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { UserNameSection } from "./UserNameSection";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { PersonalTabGroup } from "./-components/PersonalTabGroup";
|
||||
|
||||
const PersonalSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="mb-6 mt-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">{t("settings.personal.title")}</p>
|
||||
</div>
|
||||
<PersonalTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_personal_settings_layout/personal-settings/")(
|
||||
{
|
||||
component: PersonalSettingsPage
|
||||
}
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup")({
|
||||
beforeLoad: ({ context }) => {
|
||||
if (context.isAuthenticated) {
|
||||
redirect({
|
||||
throw: true,
|
||||
to: "/organization/$organizationId/secret-manager",
|
||||
params: {
|
||||
organizationId: context.organizationId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
95
frontend-v2/src/routes/_authenticate/cli-redirect.tsx
Normal file
95
frontend-v2/src/routes/_authenticate/cli-redirect.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { IconButton, SecretInput } from "@app/components/v2";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
|
||||
const getTerminalCliToken = () => {
|
||||
const cliTerminalTokenInfo = sessionStorage.getItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
|
||||
if (!cliTerminalTokenInfo) return;
|
||||
|
||||
const { expiry, data } = JSON.parse(cliTerminalTokenInfo);
|
||||
if (new Date() > new Date(expiry)) {
|
||||
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
return data as string;
|
||||
};
|
||||
|
||||
const CliRedirectPage = () => {
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
initialState: false
|
||||
});
|
||||
const cliToken = getTerminalCliToken();
|
||||
|
||||
const copyUrlToClipboard = () => {
|
||||
if (cliToken) {
|
||||
navigator.clipboard.writeText(cliToken);
|
||||
setIsUrlCopied(true);
|
||||
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen">
|
||||
<Helmet>
|
||||
<title>Infisical CLI | Login Successful!</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center space-y-4 text-gray-200">
|
||||
<div className="mb-4 flex justify-center">
|
||||
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||
</div>
|
||||
{cliToken ? (
|
||||
<>
|
||||
<div className="pb-4">
|
||||
<p className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-3xl font-medium text-transparent">
|
||||
Unable to reach CLI
|
||||
</p>
|
||||
<p className="text-light mb-1 text-center text-lg text-mineshaft-400">
|
||||
Your login was successful but, Infisical couldn't automatically push your login
|
||||
token to the CLI.
|
||||
</p>
|
||||
<p className="text-light mb-1 text-center text-lg text-mineshaft-400">
|
||||
Please copy the token below and manually provide it to your CLI.
|
||||
</p>
|
||||
</div>
|
||||
<div className="dark relative flex max-h-36 max-w-xl flex-col items-center space-y-4 overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-3">
|
||||
<SecretInput value={cliToken as string} />
|
||||
<div className="mx-1 flex">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
ariaLabel="copy to clipboard"
|
||||
onClick={copyUrlToClipboard}
|
||||
className="flex items-center rounded py-2"
|
||||
>
|
||||
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy to
|
||||
clipboard
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-3xl font-medium text-transparent">
|
||||
Head back to your terminal
|
||||
</p>
|
||||
<p className="text-light mb-1 text-lg text-mineshaft-400">
|
||||
You've successfully logged in to the Infisical CLI
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/cli-redirect")({
|
||||
component: CliRedirectPage
|
||||
});
|
||||
27
frontend-v2/src/routes/_restrict_login_signup.tsx
Normal file
27
frontend-v2/src/routes/_restrict_login_signup.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||
import { setAuthToken } from "@app/hooks/api/reactQuery";
|
||||
|
||||
export const Route = createFileRoute("/_restrict_login_signup")({
|
||||
beforeLoad: async ({ context }) => {
|
||||
try {
|
||||
const data = await context.queryClient.fetchQuery({
|
||||
queryKey: authKeys.getAuthToken,
|
||||
queryFn: fetchAuthToken
|
||||
});
|
||||
setAuthToken(data.token);
|
||||
if (!data.organizationId) {
|
||||
throw redirect({ to: "/login/select-organization" });
|
||||
}
|
||||
throw redirect({
|
||||
to: "/organization/$organizationId/secret-manager",
|
||||
params: {
|
||||
organizationId: data.organizationId
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
const EmailNotVerifiedPage = () => {
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen">
|
||||
<Helmet>
|
||||
<title>Request a New Invite</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center text-gray-200">
|
||||
<p className="text-6xl">Oops.</p>
|
||||
<p className="mb-1 mt-2 text-xl">Your email was not verified. </p>
|
||||
<p className="text-xl">Please try again.</p>
|
||||
<p className="text-md mt-8 max-w-sm text-center text-gray-600">
|
||||
Note: If it still doesn't work, please reach out to us at support@infisical.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict_login_signup/email-not-verified")({
|
||||
component: EmailNotVerifiedPage
|
||||
});
|
||||
@@ -76,6 +76,6 @@ const LoginPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup/login/")({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/login/")({
|
||||
component: LoginPage
|
||||
});
|
||||
@@ -153,6 +153,6 @@ const LoginLDAPPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup/login/ldap/")({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/login/ldap/")({
|
||||
component: LoginLDAPPage
|
||||
});
|
||||
@@ -10,6 +10,6 @@ const LoginProviderError = () => {
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup/login/provider/error")({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/login/provider/error")({
|
||||
component: LoginProviderError
|
||||
});
|
||||
@@ -14,8 +14,6 @@ const LoginProviderSuccess = () => {
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_restrict_login_signup/login/provider/success"
|
||||
)({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/login/provider/success")({
|
||||
component: LoginProviderSuccess
|
||||
});
|
||||
@@ -277,8 +277,6 @@ const SelectOrganizationPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_restrict_login_signup/login/select-organization/"
|
||||
)({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/login/select-organization/")({
|
||||
component: SelectOrganizationPage
|
||||
});
|
||||
@@ -58,6 +58,6 @@ const LoginSSOPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup/login/sso/")({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/login/sso/")({
|
||||
component: LoginSSOPage
|
||||
});
|
||||
394
frontend-v2/src/routes/_restrict_login_signup/password-reset.tsx
Normal file
394
frontend-v2/src/routes/_restrict_login_signup/password-reset.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import InputField from "@app/components/basic/InputField";
|
||||
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api";
|
||||
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
const PasswordResetPage = () => {
|
||||
const [verificationToken, setVerificationToken] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [backupKey, setBackupKey] = useState("");
|
||||
const [privateKey, setPrivateKey] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false);
|
||||
const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
|
||||
const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(false);
|
||||
const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(false);
|
||||
const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
|
||||
const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
|
||||
const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
|
||||
const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const search = useSearch({ from: "/_restrict_login_signup/password-reset" });
|
||||
|
||||
const {
|
||||
mutateAsync: verifyPasswordResetCodeMutateAsync,
|
||||
isLoading: isVerifyPasswordResetLoading
|
||||
} = useVerifyPasswordResetCode();
|
||||
const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
|
||||
|
||||
const parsedUrl = search;
|
||||
const token = parsedUrl.token as string;
|
||||
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
|
||||
|
||||
// Decrypt the private key with a backup key
|
||||
const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||
|
||||
setPrivateKey(
|
||||
Aes256Gcm.decrypt({
|
||||
ciphertext: result.encryptedPrivateKey,
|
||||
iv: result.iv,
|
||||
tag: result.tag,
|
||||
secret: backupKey
|
||||
})
|
||||
);
|
||||
setStep(3);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setBackupKeyError(true);
|
||||
}
|
||||
};
|
||||
|
||||
// If everything is correct, reset the password
|
||||
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const errorCheck = await passwordCheck({
|
||||
password: newPassword,
|
||||
setPasswordErrorTooShort,
|
||||
setPasswordErrorTooLong,
|
||||
setPasswordErrorNoLetterChar,
|
||||
setPasswordErrorNoNumOrSpecialChar,
|
||||
setPasswordErrorRepeatedChar,
|
||||
setPasswordErrorEscapeChar,
|
||||
setPasswordErrorLowEntropy,
|
||||
setPasswordErrorBreached
|
||||
});
|
||||
|
||||
if (!errorCheck) {
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: newPassword
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password: newPassword,
|
||||
salt: result.salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString("hex"),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
await resetPasswordMutateAsync({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
verificationToken
|
||||
});
|
||||
|
||||
navigate({ to: "/login" });
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Click a button to confirm email
|
||||
const stepConfirmEmail = (
|
||||
<div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
|
||||
Confirm your email
|
||||
</p>
|
||||
<img src="/images/envelope.svg" height={262} width={410} alt="verify email" />
|
||||
<div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const response = await verifyPasswordResetCodeMutateAsync({
|
||||
email,
|
||||
code: token
|
||||
});
|
||||
|
||||
setVerificationToken(response.token);
|
||||
setStep(2);
|
||||
} catch (err) {
|
||||
console.log("ERROR", err);
|
||||
navigate({ to: "/email-not-verified" });
|
||||
}
|
||||
}}
|
||||
isLoading={isVerifyPasswordResetLoading}
|
||||
size="lg"
|
||||
>
|
||||
Confirm Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Input backup key
|
||||
const stepInputBackupKey = (
|
||||
<form
|
||||
onSubmit={getEncryptedKeyHandler}
|
||||
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
|
||||
>
|
||||
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter your backup key
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
|
||||
You can find it in your emergency kit. You had to download the emergency kit during
|
||||
signup.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
<InputField
|
||||
label="Backup Key"
|
||||
onChangeHandler={setBackupKey}
|
||||
type="password"
|
||||
value={backupKey}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={backupKeyError}
|
||||
errorText="Something is wrong with the backup key"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button type="submit" size="lg">
|
||||
Submit Backup Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
// Enter new password
|
||||
const stepEnterNewPassword = (
|
||||
<form
|
||||
onSubmit={resetPasswordHandler}
|
||||
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
|
||||
>
|
||||
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter new password
|
||||
</p>
|
||||
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
|
||||
Make sure you save it somewhere safe.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
<InputField
|
||||
label="New Password"
|
||||
onChangeHandler={(password) => {
|
||||
setNewPassword(password);
|
||||
passwordCheck({
|
||||
password,
|
||||
setPasswordErrorTooShort,
|
||||
setPasswordErrorTooLong,
|
||||
setPasswordErrorNoLetterChar,
|
||||
setPasswordErrorNoNumOrSpecialChar,
|
||||
setPasswordErrorRepeatedChar,
|
||||
setPasswordErrorEscapeChar,
|
||||
setPasswordErrorLowEntropy,
|
||||
setPasswordErrorBreached
|
||||
});
|
||||
}}
|
||||
type="password"
|
||||
value={newPassword}
|
||||
isRequired
|
||||
error={
|
||||
passwordErrorTooShort &&
|
||||
passwordErrorTooLong &&
|
||||
passwordErrorNoLetterChar &&
|
||||
passwordErrorNoNumOrSpecialChar &&
|
||||
passwordErrorRepeatedChar &&
|
||||
passwordErrorEscapeChar &&
|
||||
passwordErrorLowEntropy &&
|
||||
passwordErrorBreached
|
||||
}
|
||||
autoComplete="new-password"
|
||||
id="new-password"
|
||||
/>
|
||||
</div>
|
||||
{passwordErrorTooShort ||
|
||||
passwordErrorTooLong ||
|
||||
passwordErrorNoLetterChar ||
|
||||
passwordErrorNoNumOrSpecialChar ||
|
||||
passwordErrorRepeatedChar ||
|
||||
passwordErrorEscapeChar ||
|
||||
passwordErrorLowEntropy ||
|
||||
passwordErrorBreached ? (
|
||||
<div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-1 text-sm text-gray-400">Password should contain:</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorTooShort ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
|
||||
at least 14 characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorTooLong ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
|
||||
at most 100 characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorNoLetterChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
>
|
||||
at least 1 letter character
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorNoNumOrSpecialChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
|
||||
} text-sm`}
|
||||
>
|
||||
at least 1 number or special character
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorRepeatedChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
>
|
||||
at most 3 repeated, consecutive characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorEscapeChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
>
|
||||
No escape characters allowed.
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorLowEntropy ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
>
|
||||
Password contains personal info.
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
{passwordErrorBreached ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
)}
|
||||
<div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
|
||||
Password was found in a data breach.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-2" />
|
||||
)}
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button type="submit" onClick={() => setLoading(true)} size="lg" isLoading={loading}>
|
||||
Submit New Password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
|
||||
{step === 1 && stepConfirmEmail}
|
||||
{step === 2 && stepInputBackupKey}
|
||||
{step === 3 && stepEnterNewPassword}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict_login_signup/password-reset")({
|
||||
component: PasswordResetPage
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
/**
|
||||
* This is the page that shows up when a user's invitation
|
||||
* to join a project/organization on Infisical has expired
|
||||
*/
|
||||
const RequestNewInvitePage = () => {
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-700 md:h-screen">
|
||||
<Helmet>
|
||||
<title>Request a New Invite</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<div className="mt-8 flex h-screen w-screen flex-col items-center justify-center text-bunker-200">
|
||||
<p className="text-4xl text-primary-100">Oops, your invite has expired.</p>
|
||||
<p className="my-4 text-lg">Ask your admin for a new one.</p>
|
||||
<p className="max-w-xs px-7 text-center text-sm leading-tight text-bunker-400">
|
||||
<span className="rounded-md bg-primary-500/40 px-1 text-black">Note:</span> If it still
|
||||
doesn't work, please reach out to us at support@infisical.com
|
||||
</p>
|
||||
<div className="">
|
||||
<img
|
||||
src="/images/invitation-expired.svg"
|
||||
height={500}
|
||||
width={800}
|
||||
alt="invitation expired illustration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict_login_signup/requestnewinvite")({
|
||||
component: RequestNewInvitePage
|
||||
});
|
||||
@@ -173,7 +173,7 @@ const SignUpPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup/signup/")({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/signup/")({
|
||||
component: SignUpPage,
|
||||
loader: ({ context }) => {
|
||||
console.log(context);
|
||||
@@ -85,6 +85,6 @@ const SignupSSOPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_restrict_login_signup/signup/sso/")({
|
||||
export const Route = createFileRoute("/_restrict_login_signup/signup/sso/")({
|
||||
component: SignupSSOPage
|
||||
});
|
||||
432
frontend-v2/src/routes/_restrict_login_signup/signupinvite.tsx
Normal file
432
frontend-v2/src/routes/_restrict_login_signup/signupinvite.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import InputField from "@app/components/basic/InputField";
|
||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
completeAccountSignupInvite,
|
||||
useSelectOrganization,
|
||||
verifySignupInvite
|
||||
} from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { navigateUserToOrg } from "./login/-components/Login.utils";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
type Errors = {
|
||||
tooShort?: string;
|
||||
tooLong?: string;
|
||||
noLetterChar?: string;
|
||||
noNumOrSpecialChar?: string;
|
||||
repeatedChar?: string;
|
||||
escapeChar?: string;
|
||||
lowEntropy?: string;
|
||||
breached?: string;
|
||||
};
|
||||
|
||||
const SignupInvitePage = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [firstNameError, setFirstNameError] = useState(false);
|
||||
const [lastNameError, setLastNameError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const [, setBackupKeyError] = useState(false);
|
||||
const [, setBackupKeyIssued] = useState(false);
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
const navigate = useNavigate();
|
||||
const search = useSearch({ from: "/_restrict_login_signup/signupinvite" });
|
||||
const parsedUrl = search;
|
||||
const token = parsedUrl.token as string;
|
||||
const organizationId = parsedUrl.organization_id as string;
|
||||
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const metadata = queryParams.get("metadata") || undefined;
|
||||
|
||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||
|
||||
// Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria.
|
||||
const signupErrorCheck = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
let errorCheck = await checkPassword({
|
||||
password,
|
||||
setErrors
|
||||
});
|
||||
|
||||
if (!firstName) {
|
||||
setFirstNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setFirstNameError(false);
|
||||
}
|
||||
if (!lastName) {
|
||||
setLastNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setLastNameError(false);
|
||||
}
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (_err, result) => {
|
||||
try {
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password,
|
||||
salt: result.salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString("hex"),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
const { token: jwtToken } = await completeAccountSignupInvite({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
tokenMetadata: metadata
|
||||
});
|
||||
|
||||
// unset temporary signup JWT token and set JWT token
|
||||
SecurityClient.setSignupToken("");
|
||||
SecurityClient.setToken(jwtToken);
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
const orgId = userOrgs[0].id;
|
||||
|
||||
if (!orgId) throw new Error("You are not part of any organization");
|
||||
|
||||
const completeSignupFlow = async () => {
|
||||
const {
|
||||
token: mfaToken,
|
||||
isMfaEnabled,
|
||||
mfaMethod
|
||||
} = await selectOrganization({
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(mfaToken);
|
||||
if (mfaMethod) {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => completeSignupFlow);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
|
||||
setStep(3);
|
||||
};
|
||||
|
||||
await completeSignupFlow();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const stepConfirmEmail = (
|
||||
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 py-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
|
||||
<p className="mb-6 flex justify-center text-center text-4xl font-semibold text-primary-100">
|
||||
Confirm your email
|
||||
</p>
|
||||
<img src="/images/dragon-signupinvite.svg" height={262} width={410} alt="verify email" />
|
||||
<div className="mx-auto mb-2 mt-10 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const response = await verifySignupInvite({
|
||||
email,
|
||||
code: token,
|
||||
organizationId
|
||||
});
|
||||
|
||||
if (response) {
|
||||
// user will have temp token if doesn't have an account
|
||||
// then continue with account setup workflow
|
||||
if (response?.token) {
|
||||
SecurityClient.setSignupToken(response.token);
|
||||
setStep(2);
|
||||
} else {
|
||||
const redirectExistingUser = async () => {
|
||||
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
|
||||
organizationId
|
||||
});
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(mfaToken);
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => redirectExistingUser);
|
||||
return;
|
||||
}
|
||||
|
||||
// user will be redirected to dashboard
|
||||
// if not logged in gets kicked out to login
|
||||
await navigateUserToOrg(navigate, organizationId);
|
||||
};
|
||||
|
||||
await redirectExistingUser();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
navigate({ to: "/requestnewinvite" });
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
>
|
||||
Confirm Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Because this is the invite signup - we directly go to the last step of signup (email is already verified)
|
||||
const main = (
|
||||
<div className="h-7/12 mx-auto mb-32 w-max rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-8 py-10 drop-shadow-xl md:mb-16">
|
||||
<p className="mx-8 mb-6 flex justify-center bg-gradient-to-tr from-mineshaft-300 to-white bg-clip-text text-4xl font-bold text-transparent md:mx-16">
|
||||
Almost there!
|
||||
</p>
|
||||
<div className="relative z-0 flex max-h-24 w-full items-center justify-end rounded-lg md:p-2">
|
||||
<InputField
|
||||
label="First Name"
|
||||
onChangeHandler={setFirstName}
|
||||
type="name"
|
||||
value={firstName}
|
||||
isRequired
|
||||
errorText="Please input your first name."
|
||||
error={firstNameError}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:p-2">
|
||||
<InputField
|
||||
label="Last Name"
|
||||
onChangeHandler={setLastName}
|
||||
type="name"
|
||||
value={lastName}
|
||||
isRequired
|
||||
errorText="Please input your last name."
|
||||
error={lastNameError}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex max-h-60 w-full flex-col items-center justify-center rounded-lg md:p-2">
|
||||
<InputField
|
||||
label="Password"
|
||||
onChangeHandler={(pass) => {
|
||||
setPassword(pass);
|
||||
checkPassword({
|
||||
password: pass,
|
||||
setErrors
|
||||
});
|
||||
}}
|
||||
type="password"
|
||||
value={password}
|
||||
isRequired
|
||||
error={Object.keys(errors).length > 0}
|
||||
autoComplete="new-password"
|
||||
id="new-password"
|
||||
/>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-2 text-sm text-gray-400">Password should contain at least:</div>
|
||||
{Object.keys(errors).map((key) => {
|
||||
if (errors[key as keyof Errors]) {
|
||||
return (
|
||||
<div className="items-top ml-1 flex flex-row justify-start" key={key}>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-md ml-0.5 mr-2.5 text-red" />
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-auto mt-2 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
|
||||
<Button
|
||||
onClick={() => {
|
||||
signupErrorCheck();
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
size="lg"
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const step4 = (
|
||||
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 pb-6 pt-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
|
||||
<p className="flex justify-center bg-gradient-to-br from-white to-mineshaft-300 bg-clip-text text-center text-4xl font-semibold text-transparent">
|
||||
Save your Emergency Kit
|
||||
</p>
|
||||
<div className="text-md mt-4 flex w-full max-w-md flex-col items-center justify-center rounded-md px-2 text-gray-400 md:mt-8">
|
||||
<div>
|
||||
If you get locked out of your account, your Emergency Kit is the only way to sign in.
|
||||
</div>
|
||||
<div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex w-full max-w-xs flex-row items-center rounded-md bg-white/10 p-2 text-gray-400 md:max-w-md">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
|
||||
It contains your Secret Key which we cannot access or recover for you if you lose it.
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: `${firstName} ${lastName}`,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
navigate({
|
||||
to: `/organization/$organizationId/${ProjectType.SecretManager}` as const,
|
||||
params: {
|
||||
organizationId
|
||||
}
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
>
|
||||
Download PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||
<Helmet>
|
||||
<title>Sign Up</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
{shouldShowMfa ? (
|
||||
<Mfa
|
||||
email={email}
|
||||
successCallback={mfaSuccessCallback}
|
||||
method={requiredMfaMethod}
|
||||
closeMfa={() => toggleShowMfa.off()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict_login_signup/signupinvite")({
|
||||
component: SignupInvitePage
|
||||
});
|
||||
115
frontend-v2/src/routes/_restrict_login_signup/verify-email.tsx
Normal file
115
frontend-v2/src/routes/_restrict_login_signup/verify-email.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
|
||||
import InputField from "@app/components/basic/InputField";
|
||||
import { Button, EmailServiceSetupModal } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useSendPasswordResetEmail } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
|
||||
const VerifyEmailPage = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
|
||||
|
||||
const { mutateAsync } = useSendPasswordResetEmail();
|
||||
|
||||
/**
|
||||
* This function sends the verification email and forwards a user to the next step.
|
||||
*/
|
||||
const sendVerificationEmail = async () => {
|
||||
if (email) {
|
||||
await mutateAsync({ email });
|
||||
setStep(2);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (serverDetails?.emailConfigured) {
|
||||
sendVerificationEmail();
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
|
||||
<Helmet>
|
||||
<title>Login</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Verify your email in Infisical" />
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||
/>
|
||||
</Helmet>
|
||||
<Link to="/">
|
||||
<div className="mb-8 mt-20 flex cursor-pointer justify-center">
|
||||
<img src="/images/biglogo.png" height={90} width={120} alt="long logo" />
|
||||
</div>
|
||||
</Link>
|
||||
{step === 1 && (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl"
|
||||
>
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Forgot your password?
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max justify-center text-sm text-gray-400">
|
||||
You will need your emergency kit. Enter your email to start account recovery.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button type="submit" size="lg" onClick={() => {}} isLoading={loading}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-semibold text-bunker-100 md:text-2xl">
|
||||
Look for an email in your inbox.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max justify-center text-center text-sm text-gray-400">
|
||||
An email with instructions has been sent to {email}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict_login_signup/verify-email")({
|
||||
component: VerifyEmailPage
|
||||
});
|
||||
Reference in New Issue
Block a user