mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 01:52:22 +00:00
Merge remote-tracking branch 'origin/main' into misc/improved-helper-text-for-gcp-sa-field
This commit is contained in:
@@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/_restrict-login-signup/login/provider/success"
|
||||
),
|
||||
SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"),
|
||||
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset")
|
||||
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
|
||||
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
|
||||
},
|
||||
Organization: {
|
||||
SecretScanning: setRoute(
|
||||
|
||||
@@ -10,6 +10,7 @@ export type TGetAuditLogsFilter = {
|
||||
actorType?: ActorType;
|
||||
projectId?: string;
|
||||
actor?: string; // user ID format
|
||||
secretPath?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
limit: number;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
MfaMethod,
|
||||
ResetPasswordDTO,
|
||||
SendMfaTokenDTO,
|
||||
SetupPasswordDTO,
|
||||
SRP1DTO,
|
||||
SRPR1Res,
|
||||
TOauthTokenExchangeDTO,
|
||||
@@ -286,7 +287,8 @@ export const useResetPassword = () => {
|
||||
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
|
||||
salt: details.salt,
|
||||
verifier: details.verifier
|
||||
verifier: details.verifier,
|
||||
password: details.password
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => {
|
||||
|
||||
return data.isVerified;
|
||||
};
|
||||
|
||||
export const useSendPasswordSetupEmail = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/email/password-setup");
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSetupPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (payload: SetupPasswordDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/password-setup", payload);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,6 +133,20 @@ export type ResetPasswordDTO = {
|
||||
salt: string;
|
||||
verifier: string;
|
||||
verificationToken: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SetupPasswordDTO = {
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
token: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type IssueBackupPrivateKeyDTO = {
|
||||
|
||||
@@ -16,5 +16,6 @@ export {
|
||||
useGetIntegrationAuthTeamCityBuildConfigs,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches,
|
||||
useGetIntegrationAuthVercelCustomEnvironments,
|
||||
useSaveIntegrationAccessToken
|
||||
} from "./queries";
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
Team,
|
||||
TeamCityBuildConfig,
|
||||
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
|
||||
TOctopusDeployVariableSetScopeValues
|
||||
TOctopusDeployVariableSetScopeValues,
|
||||
VercelEnvironment
|
||||
} from "./types";
|
||||
|
||||
const integrationAuthKeys = {
|
||||
@@ -132,7 +133,9 @@ const integrationAuthKeys = {
|
||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
||||
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
|
||||
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const,
|
||||
getIntegrationAuthVercelCustomEnv: (integrationAuthId: string, teamId: string) =>
|
||||
[{ integrationAuthId, teamId }, "integrationAuthVercelCustomEnv"] as const
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
@@ -362,6 +365,29 @@ const fetchIntegrationAuthQoveryScopes = async ({
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthVercelCustomEnvironments = async ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId: string;
|
||||
}) => {
|
||||
const {
|
||||
data: { environments }
|
||||
} = await apiRequest.get<{
|
||||
environments: {
|
||||
appId: string;
|
||||
customEnvironments: VercelEnvironment[];
|
||||
}[];
|
||||
}>(`/api/v1/integration-auth/${integrationAuthId}/vercel/custom-environments`, {
|
||||
params: {
|
||||
teamId
|
||||
}
|
||||
});
|
||||
|
||||
return environments;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthHerokuPipelines = async ({
|
||||
integrationAuthId
|
||||
}: {
|
||||
@@ -730,6 +756,24 @@ export const useGetIntegrationAuthQoveryScopes = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthVercelCustomEnvironments = ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelCustomEnv(integrationAuthId, teamId),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthVercelCustomEnvironments({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}),
|
||||
enabled: Boolean(teamId && integrationAuthId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthHerokuPipelines = ({
|
||||
integrationAuthId
|
||||
}: {
|
||||
|
||||
@@ -43,6 +43,11 @@ export type Environment = {
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
export type VercelEnvironment = {
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type ChecklyGroup = {
|
||||
name: string;
|
||||
groupId: number;
|
||||
|
||||
@@ -136,7 +136,8 @@ export const PasswordResetPage = () => {
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
verificationToken
|
||||
verificationToken,
|
||||
password: newPassword
|
||||
});
|
||||
|
||||
navigate({ to: "/login" });
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
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, Card, CardTitle, FormControl, Input } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useSetupPassword } from "@app/hooks/api/auth/queries";
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
export const PasswordSetupPage = () => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [passwordsMatch, setPasswordsMatch] = useState(true);
|
||||
const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(true);
|
||||
const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
|
||||
const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(true);
|
||||
const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(true);
|
||||
const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
|
||||
const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
|
||||
const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
|
||||
const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordSetupPage.id });
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const setupPassword = useSetupPassword();
|
||||
|
||||
const parsedUrl = search;
|
||||
const token = parsedUrl.token as string;
|
||||
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
|
||||
|
||||
const handleSetPassword = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const errorCheck = await passwordCheck({
|
||||
password,
|
||||
setPasswordErrorTooShort,
|
||||
setPasswordErrorTooLong,
|
||||
setPasswordErrorNoLetterChar,
|
||||
setPasswordErrorNoNumOrSpecialChar,
|
||||
setPasswordErrorRepeatedChar,
|
||||
setPasswordErrorEscapeChar,
|
||||
setPasswordErrorLowEntropy,
|
||||
setPasswordErrorBreached
|
||||
});
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setPasswordsMatch(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setPasswordsMatch(true);
|
||||
|
||||
if (!errorCheck) {
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
|
||||
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: localStorage.getItem("PRIVATE_KEY") as string,
|
||||
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)
|
||||
});
|
||||
|
||||
try {
|
||||
await setupPassword.mutateAsync({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token,
|
||||
password
|
||||
});
|
||||
|
||||
setIsRedirecting(true);
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
title: "Password successfully set",
|
||||
text: "Redirecting to login..."
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = "/login";
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: (error as Error).message ?? "Error setting password"
|
||||
});
|
||||
navigate({ to: "/personal-settings" });
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isInvalidPassword =
|
||||
passwordErrorTooShort ||
|
||||
passwordErrorTooLong ||
|
||||
passwordErrorNoLetterChar ||
|
||||
passwordErrorNoNumOrSpecialChar ||
|
||||
passwordErrorRepeatedChar ||
|
||||
passwordErrorEscapeChar ||
|
||||
passwordErrorLowEntropy ||
|
||||
passwordErrorBreached;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
|
||||
<form onSubmit={handleSetPassword}>
|
||||
<Card className="flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 px-8 py-4">
|
||||
<CardTitle
|
||||
className="p-0 pb-4 pt-2 text-left text-xl"
|
||||
subTitle="Make sure to store your password somewhere safe."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</div>
|
||||
<span className="ml-2.5">Set Password</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl label="Password">
|
||||
<Input
|
||||
value={password}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
passwordCheck({
|
||||
password: e.target.value,
|
||||
setPasswordErrorTooShort,
|
||||
setPasswordErrorTooLong,
|
||||
setPasswordErrorNoLetterChar,
|
||||
setPasswordErrorNoNumOrSpecialChar,
|
||||
setPasswordErrorRepeatedChar,
|
||||
setPasswordErrorEscapeChar,
|
||||
setPasswordErrorLowEntropy,
|
||||
setPasswordErrorBreached
|
||||
});
|
||||
}}
|
||||
rightIcon={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowPassword((prev) => !prev);
|
||||
}}
|
||||
className="cursor-pointer self-end text-gray-400"
|
||||
>
|
||||
{showPassword ? (
|
||||
<FontAwesomeIcon size="sm" icon={faEyeSlash} />
|
||||
) : (
|
||||
<FontAwesomeIcon size="sm" icon={faEye} />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Confirm Password"
|
||||
errorText="Passwords must match"
|
||||
isError={!passwordsMatch}
|
||||
>
|
||||
<Input
|
||||
value={confirmPassword}
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
rightIcon={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowConfirmPassword((prev) => !prev);
|
||||
}}
|
||||
className="cursor-pointer self-end text-gray-400"
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<FontAwesomeIcon size="sm" icon={faEyeSlash} />
|
||||
) : (
|
||||
<FontAwesomeIcon size="sm" icon={faEye} />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="mb-4 flex w-full max-w-md flex-col items-start rounded-md bg-mineshaft-700 px-2 py-2 transition-opacity duration-100">
|
||||
<div className="mb-1 text-sm text-gray-400">Password must 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
|
||||
</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`}
|
||||
>
|
||||
no personal information
|
||||
</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 not found in a data breach.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
isDisabled={isInvalidPassword || setupPassword.isPending || isRedirecting}
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={setupPassword.isPending}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Card>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PasswordSetupPage } from "./PasswordSetupPage";
|
||||
|
||||
const PasswordSetupPageQueryParamsSchema = z.object({
|
||||
token: z.string(),
|
||||
to: z.string()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/password-setup")({
|
||||
component: PasswordSetupPage,
|
||||
validateSearch: zodValidator(PasswordSetupPageQueryParamsSchema)
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { userKeys } from "@app/hooks/api";
|
||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||
|
||||
export const Route = createFileRoute("/_authenticate")({
|
||||
beforeLoad: async ({ context }) => {
|
||||
beforeLoad: async ({ context, location }) => {
|
||||
if (!context.serverConfig.initialized) {
|
||||
throw redirect({ to: "/admin/signup" });
|
||||
}
|
||||
@@ -26,7 +27,7 @@ export const Route = createFileRoute("/_authenticate")({
|
||||
});
|
||||
});
|
||||
|
||||
if (!data.organizationId) {
|
||||
if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) {
|
||||
throw redirect({ to: "/login/select-organization" });
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
@@ -22,6 +23,7 @@ import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor } from "@app/hooks/api/auditLogs/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { AuditLogFilterFormData } from "./types";
|
||||
|
||||
@@ -50,6 +52,7 @@ export const LogsFilter = ({
|
||||
className,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
watch
|
||||
}: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
@@ -101,6 +104,7 @@ export const LogsFilter = ({
|
||||
};
|
||||
|
||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||
const selectedProject = watch("project");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -109,30 +113,48 @@ export const LogsFilter = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-12 w-64"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={onChange}
|
||||
placeholder="Select a project..."
|
||||
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-64"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={(e) => {
|
||||
if (e === null) {
|
||||
setValue("secretPath", "");
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
placeholder="Select a project..."
|
||||
options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedProject?.type === ProjectType.SecretManager && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl label="Secret path" className="w-40">
|
||||
<Input {...field} value={value} onChange={(e) => onChange(e.target.value)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -67,10 +68,13 @@ export const LogsSection = withPermission(
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
const actor = watch("actor");
|
||||
const projectId = watch("project")?.id;
|
||||
const secretPath = watch("secretPath");
|
||||
|
||||
const startDate = watch("startDate");
|
||||
const endDate = watch("endDate");
|
||||
|
||||
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showFilters && (
|
||||
@@ -90,6 +94,7 @@ export const LogsSection = withPermission(
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn}
|
||||
filter={{
|
||||
secretPath: debouncedSecretPath || undefined,
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
actorType: presets?.actorType,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const auditLogFilterFormSchema = z
|
||||
.object({
|
||||
eventMetadata: z.object({}).optional(),
|
||||
project: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
|
||||
project: z
|
||||
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
||||
.optional()
|
||||
.nullable(),
|
||||
eventType: z.nativeEnum(EventType).array(),
|
||||
actor: z.string().optional(),
|
||||
userAgentType: z.nativeEnum(UserAgentType),
|
||||
secretPath: z.string().optional(),
|
||||
startDate: z.date().optional(),
|
||||
endDate: z.date().optional(),
|
||||
page: z.coerce.number().optional(),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
@@ -45,14 +45,12 @@ export const IntegrationsListPage = () => {
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</Helmet>
|
||||
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
||||
<div className="mx-6 mb-8">
|
||||
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Integrations</h1>
|
||||
<p className="text-base text-bunker-300">
|
||||
Manage integrations with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-2 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||
<div className="mb-8">
|
||||
<PageHeader
|
||||
title="Integrations"
|
||||
description="Manage integrations with third-party services."
|
||||
/>
|
||||
<div className="mb-4 mt-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
|
||||
Integrations Update
|
||||
|
||||
+1
-2
@@ -73,7 +73,7 @@ const PageContent = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
@@ -89,7 +89,6 @@ const PageContent = () => {
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="mb-4"
|
||||
>
|
||||
Secret Syncs
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export const AzureDevopsAuthorizePage = () => {
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src="/images/integrations/Amazon Web Services.png"
|
||||
src="/images/integrations/Microsoft Azure.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Azure DevOps logo"
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ export const AzureDevopsConfigurePage = () => {
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src="/images/integrations/Amazon Web Services.png"
|
||||
src="/images/integrations/Microsoft Azure.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Azure DevOps logo"
|
||||
|
||||
+6
-1
@@ -47,7 +47,12 @@ export function AzureKeyVaultAuthorizePage() {
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<img src="/images/integrations/GitHub.png" height={30} width={30} alt="Github logo" />
|
||||
<img
|
||||
src="/images/integrations/Microsoft Azure.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="Azure Key Vault logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-2.5">Azure Key Vault Integration </span>
|
||||
<a
|
||||
|
||||
+36
-4
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
@@ -25,7 +25,8 @@ import { useCreateIntegration } from "@app/hooks/api";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthVercelBranches
|
||||
useGetIntegrationAuthVercelBranches,
|
||||
useGetIntegrationAuthVercelCustomEnvironments
|
||||
} from "@app/hooks/api/integrationAuth";
|
||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||
|
||||
@@ -75,6 +76,11 @@ export const VercelConfigurePage = () => {
|
||||
teamId: integrationAuth?.teamId as string
|
||||
});
|
||||
|
||||
const { data: customEnvironments } = useGetIntegrationAuthVercelCustomEnvironments({
|
||||
teamId: integrationAuth?.teamId as string,
|
||||
integrationAuthId: integrationAuthId as string
|
||||
});
|
||||
|
||||
const { data: branches } = useGetIntegrationAuthVercelBranches({
|
||||
integrationAuthId: integrationAuthId as string,
|
||||
appId: targetAppId
|
||||
@@ -135,6 +141,26 @@ export const VercelConfigurePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedVercelEnvironments = useMemo(() => {
|
||||
let selectedEnvironments = vercelEnvironments;
|
||||
|
||||
const environments = customEnvironments?.find(
|
||||
(e) => e.appId === targetAppId
|
||||
)?.customEnvironments;
|
||||
|
||||
if (environments && environments.length > 0) {
|
||||
selectedEnvironments = [
|
||||
...selectedEnvironments,
|
||||
...environments.map((env) => ({
|
||||
name: env.slug,
|
||||
slug: env.id
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
return selectedEnvironments;
|
||||
}, [targetAppId, customEnvironments]);
|
||||
|
||||
return integrationAuth &&
|
||||
selectedSourceEnvironment &&
|
||||
integrationAuthApps &&
|
||||
@@ -210,7 +236,13 @@ export const VercelConfigurePage = () => {
|
||||
>
|
||||
<Select
|
||||
value={targetAppId}
|
||||
onValueChange={(val) => setTargetAppId(val)}
|
||||
onValueChange={(val) => {
|
||||
if (vercelEnvironments.every((env) => env.slug !== targetEnvironment)) {
|
||||
setTargetEnvironment(vercelEnvironments[0].slug);
|
||||
}
|
||||
|
||||
setTargetAppId(val);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
@@ -236,7 +268,7 @@ export const VercelConfigurePage = () => {
|
||||
onValueChange={(val) => setTargetEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{vercelEnvironments.map((vercelEnvironment) => (
|
||||
{selectedVercelEnvironments.map((vercelEnvironment) => (
|
||||
<SelectItem
|
||||
value={vercelEnvironment.slug}
|
||||
key={`target-environment-${vercelEnvironment.slug}`}
|
||||
|
||||
+30
@@ -11,6 +11,7 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
|
||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
|
||||
|
||||
type Errors = {
|
||||
tooShort?: string;
|
||||
@@ -45,6 +46,7 @@ export const ChangePasswordSection = () => {
|
||||
});
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const sendSetupPasswordEmail = useSendPasswordSetupEmail();
|
||||
|
||||
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
||||
try {
|
||||
@@ -80,6 +82,24 @@ export const ChangePasswordSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSetupPassword = async () => {
|
||||
try {
|
||||
await sendSetupPasswordEmail.mutateAsync();
|
||||
|
||||
createNotification({
|
||||
title: "Password setup verification email sent",
|
||||
text: "Check your email to confirm password setup",
|
||||
type: "info"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to send password setup email",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
@@ -142,6 +162,16 @@ export const ChangePasswordSection = () => {
|
||||
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
<p className="mt-2 font-inter text-sm text-mineshaft-400">
|
||||
Need to setup a password?{" "}
|
||||
<button
|
||||
onClick={onSetupPassword}
|
||||
type="button"
|
||||
className="underline underline-offset-2 hover:text-mineshaft-200"
|
||||
>
|
||||
Click here
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Route as authSignUpInvitePageRouteImport } from './pages/auth/SignUpInv
|
||||
import { Route as authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route'
|
||||
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
|
||||
import { Route as authEmailNotVerifiedPageRouteImport } from './pages/auth/EmailNotVerifiedPage/route'
|
||||
import { Route as authPasswordSetupPageRouteImport } from './pages/auth/PasswordSetupPage/route'
|
||||
import { Route as userLayoutImport } from './pages/user/layout'
|
||||
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
||||
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
||||
@@ -310,6 +311,14 @@ const authEmailNotVerifiedPageRouteRoute =
|
||||
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
|
||||
} as any)
|
||||
|
||||
const authPasswordSetupPageRouteRoute = authPasswordSetupPageRouteImport.update(
|
||||
{
|
||||
id: '/password-setup',
|
||||
path: '/password-setup',
|
||||
getParentRoute: () => middlewaresAuthenticateRoute,
|
||||
} as any,
|
||||
)
|
||||
|
||||
const userLayoutRoute = userLayoutImport.update({
|
||||
id: '/_layout',
|
||||
getParentRoute: () => AuthenticatePersonalSettingsRoute,
|
||||
@@ -1577,6 +1586,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_authenticate/password-setup': {
|
||||
id: '/_authenticate/password-setup'
|
||||
path: '/password-setup'
|
||||
fullPath: '/password-setup'
|
||||
preLoaderRoute: typeof authPasswordSetupPageRouteImport
|
||||
parentRoute: typeof middlewaresAuthenticateImport
|
||||
}
|
||||
'/_restrict-login-signup/email-not-verified': {
|
||||
id: '/_restrict-login-signup/email-not-verified'
|
||||
path: '/email-not-verified'
|
||||
@@ -3397,12 +3413,14 @@ const AuthenticatePersonalSettingsRouteWithChildren =
|
||||
)
|
||||
|
||||
interface middlewaresAuthenticateRouteChildren {
|
||||
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
|
||||
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
|
||||
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
|
||||
}
|
||||
|
||||
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
|
||||
{
|
||||
authPasswordSetupPageRouteRoute: authPasswordSetupPageRouteRoute,
|
||||
middlewaresInjectOrgDetailsRoute:
|
||||
middlewaresInjectOrgDetailsRouteWithChildren,
|
||||
AuthenticatePersonalSettingsRoute:
|
||||
@@ -3487,6 +3505,7 @@ export interface FileRoutesByFullPath {
|
||||
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||
'': typeof organizationLayoutRouteWithChildren
|
||||
'/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||
'/password-reset': typeof authPasswordResetPageRouteRoute
|
||||
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||
@@ -3657,6 +3676,7 @@ export interface FileRoutesByTo {
|
||||
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||
'': typeof organizationLayoutRouteWithChildren
|
||||
'/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||
'/password-reset': typeof authPasswordResetPageRouteRoute
|
||||
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||
@@ -3824,6 +3844,7 @@ export interface FileRoutesById {
|
||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
|
||||
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
|
||||
'/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
|
||||
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||
@@ -4004,6 +4025,7 @@ export interface FileRouteTypes {
|
||||
| '/cli-redirect'
|
||||
| '/share-secret'
|
||||
| ''
|
||||
| '/password-setup'
|
||||
| '/email-not-verified'
|
||||
| '/password-reset'
|
||||
| '/requestnewinvite'
|
||||
@@ -4173,6 +4195,7 @@ export interface FileRouteTypes {
|
||||
| '/cli-redirect'
|
||||
| '/share-secret'
|
||||
| ''
|
||||
| '/password-setup'
|
||||
| '/email-not-verified'
|
||||
| '/password-reset'
|
||||
| '/requestnewinvite'
|
||||
@@ -4338,6 +4361,7 @@ export interface FileRouteTypes {
|
||||
| '/share-secret'
|
||||
| '/_authenticate'
|
||||
| '/_restrict-login-signup'
|
||||
| '/_authenticate/password-setup'
|
||||
| '/_restrict-login-signup/email-not-verified'
|
||||
| '/_restrict-login-signup/password-reset'
|
||||
| '/_restrict-login-signup/requestnewinvite'
|
||||
@@ -4562,6 +4586,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate": {
|
||||
"filePath": "middlewares/authenticate.tsx",
|
||||
"children": [
|
||||
"/_authenticate/password-setup",
|
||||
"/_authenticate/_inject-org-details",
|
||||
"/_authenticate/personal-settings"
|
||||
]
|
||||
@@ -4579,6 +4604,10 @@ export const routeTree = rootRoute
|
||||
"/_restrict-login-signup/admin/signup"
|
||||
]
|
||||
},
|
||||
"/_authenticate/password-setup": {
|
||||
"filePath": "auth/PasswordSetupPage/route.tsx",
|
||||
"parent": "/_authenticate"
|
||||
},
|
||||
"/_restrict-login-signup/email-not-verified": {
|
||||
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
|
||||
"parent": "/_restrict-login-signup"
|
||||
|
||||
@@ -335,6 +335,7 @@ export const routes = rootRoute("root.tsx", [
|
||||
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
|
||||
]),
|
||||
middleware("authenticate.tsx", [
|
||||
route("/password-setup", "auth/PasswordSetupPage/route.tsx"),
|
||||
route("/personal-settings", [
|
||||
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user