mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
Merge pull request #3052 from Infisical/set-password-feature
Feature: Setup Password
This commit is contained in:
@@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim()
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
userId: token.userId
|
userId: token.userId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { message: "Successfully updated backup private key" };
|
return { message: "Successfully reset password" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/email/password-setup",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.password.sendPasswordSetupEmail(req.permission);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "A password setup link has been sent"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/password-setup",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
protectedKey: z.string().trim(),
|
||||||
|
protectedKeyIV: z.string().trim(),
|
||||||
|
protectedKeyTag: z.string().trim(),
|
||||||
|
encryptedPrivateKey: z.string().trim(),
|
||||||
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
|
salt: z.string().trim(),
|
||||||
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim(),
|
||||||
|
token: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
await server.services.password.setupPassword(req.body, req.permission);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
void res.cookie("jid", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully setup password" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
return { token, expiresAt };
|
||||||
}
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: {
|
||||||
|
// generate random hex
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
case TokenType.TOKEN_USER_UNLOCK: {
|
case TokenType.TOKEN_USER_UNLOCK: {
|
||||||
const token = crypto.randomBytes(16).toString("hex");
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
const expiresAt = new Date(new Date().getTime() + 259200000);
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum TokenType {
|
|||||||
TOKEN_EMAIL_MFA = "emailMfa",
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||||
|
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
|
||||||
TOKEN_USER_UNLOCK = "userUnlock"
|
TOKEN_USER_UNLOCK = "userUnlock"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
|
|||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
@@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|||||||
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
import {
|
||||||
import { AuthTokenType } from "./auth-type";
|
TChangePasswordDTO,
|
||||||
|
TCreateBackupPrivateKeyDTO,
|
||||||
|
TResetPasswordViaBackupKeyDTO,
|
||||||
|
TSetupPasswordViaBackupKeyDTO
|
||||||
|
} from "./auth-password-type";
|
||||||
|
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
||||||
|
|
||||||
type TAuthPasswordServiceFactoryDep = {
|
type TAuthPasswordServiceFactoryDep = {
|
||||||
authDAL: TAuthDALFactory;
|
authDAL: TAuthDALFactory;
|
||||||
@@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
|
|||||||
verifier,
|
verifier,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
userId
|
userId,
|
||||||
|
password
|
||||||
}: TResetPasswordViaBackupKeyDTO) => {
|
}: TResetPasswordViaBackupKeyDTO) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier,
|
||||||
|
hashedPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
await userDAL.updateById(userId, {
|
await userDAL.updateById(userId, {
|
||||||
@@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({
|
|||||||
return backupKey;
|
return backupKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendPasswordSetupEmail = async (actor: OrgServiceActor) => {
|
||||||
|
if (actor.type !== ActorType.USER)
|
||||||
|
throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` });
|
||||||
|
|
||||||
|
const user = await userDAL.findById(actor.id);
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
|
||||||
|
|
||||||
|
if (!user.isAccepted || !user.authMethods)
|
||||||
|
throw new BadRequestError({ message: `You must complete signup to set a password` });
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = user.email ?? user.username;
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.SetupPassword,
|
||||||
|
recipients: [email],
|
||||||
|
subjectLine: "Infisical Password Setup",
|
||||||
|
substitutions: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupPassword = async (
|
||||||
|
{
|
||||||
|
encryptedPrivateKey,
|
||||||
|
protectedKeyTag,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
password,
|
||||||
|
token
|
||||||
|
}: TSetupPasswordViaBackupKeyDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||||
|
userId: actor.id,
|
||||||
|
code: token
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadRequestError({ message: "Expired or invalid token. Please try again." });
|
||||||
|
}
|
||||||
|
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
const user = await userDAL.findById(actor.id, tx);
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
|
||||||
|
|
||||||
|
if (!user.isAccepted || !user.authMethods)
|
||||||
|
throw new BadRequestError({ message: `You must complete signup to set a password` });
|
||||||
|
|
||||||
|
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
||||||
|
await userDAL.updateById(
|
||||||
|
actor.id,
|
||||||
|
{
|
||||||
|
authMethods: [...user.authMethods, AuthMethod.EMAIL]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
|
await userDAL.updateUserEncryptionByUserId(
|
||||||
|
actor.id,
|
||||||
|
{
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
hashedPassword,
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tokenService.revokeAllMySessions(actor.id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generateServerPubKey,
|
generateServerPubKey,
|
||||||
changePassword,
|
changePassword,
|
||||||
@@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
verifyPasswordResetEmail,
|
verifyPasswordResetEmail,
|
||||||
createBackupPrivateKey,
|
createBackupPrivateKey,
|
||||||
getBackupPrivateKeyOfUser
|
getBackupPrivateKeyOfUser,
|
||||||
|
sendPasswordSetupEmail,
|
||||||
|
setupPassword
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
|
|||||||
encryptedPrivateKeyTag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSetupPasswordViaBackupKeyDTO = {
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
password: string;
|
||||||
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateBackupPrivateKeyDTO = {
|
export type TCreateBackupPrivateKeyDTO = {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export enum SmtpTemplates {
|
|||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
ResetPassword = "passwordReset.handlebars",
|
ResetPassword = "passwordReset.handlebars",
|
||||||
|
SetupPassword = "passwordSetup.handlebars",
|
||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Password Setup</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Setup your password</h2>
|
||||||
|
<p>Someone requested to set up a password for your account.</p>
|
||||||
|
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
|
||||||
|
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
|
||||||
|
<p>If you didn't initiate this request, please contact
|
||||||
|
{{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p>
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/_restrict-login-signup/login/provider/success"
|
"/_restrict-login-signup/login/provider/success"
|
||||||
),
|
),
|
||||||
SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"),
|
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: {
|
Organization: {
|
||||||
SecretScanning: setRoute(
|
SecretScanning: setRoute(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
MfaMethod,
|
MfaMethod,
|
||||||
ResetPasswordDTO,
|
ResetPasswordDTO,
|
||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
|
SetupPasswordDTO,
|
||||||
SRP1DTO,
|
SRP1DTO,
|
||||||
SRPR1Res,
|
SRPR1Res,
|
||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
@@ -286,7 +287,8 @@ export const useResetPassword = () => {
|
|||||||
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
|
||||||
salt: details.salt,
|
salt: details.salt,
|
||||||
verifier: details.verifier
|
verifier: details.verifier,
|
||||||
|
password: details.password
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => {
|
|||||||
|
|
||||||
return data.isVerified;
|
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;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
verificationToken: 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 = {
|
export type IssueBackupPrivateKeyDTO = {
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ export const PasswordResetPage = () => {
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
verifier: result.verifier,
|
verifier: result.verifier,
|
||||||
verificationToken
|
verificationToken,
|
||||||
|
password: newPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate({ to: "/login" });
|
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 { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { userKeys } from "@app/hooks/api";
|
import { userKeys } from "@app/hooks/api";
|
||||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticate")({
|
export const Route = createFileRoute("/_authenticate")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context, location }) => {
|
||||||
if (!context.serverConfig.initialized) {
|
if (!context.serverConfig.initialized) {
|
||||||
throw redirect({ to: "/admin/signup" });
|
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" });
|
throw redirect({ to: "/login/select-organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
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 { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||||
@@ -45,14 +45,12 @@ export const IntegrationsListPage = () => {
|
|||||||
<meta name="og:description" content={t("integrations.description") as string} />
|
<meta name="og:description" content={t("integrations.description") as string} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
||||||
<div className="mx-6 mb-8">
|
<div className="mb-8">
|
||||||
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
<PageHeader
|
||||||
<h1 className="text-3xl font-semibold">Integrations</h1>
|
title="Integrations"
|
||||||
<p className="text-base text-bunker-300">
|
description="Manage integrations with third-party services."
|
||||||
Manage integrations with third-party services.
|
/>
|
||||||
</p>
|
<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>
|
|
||||||
<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-1 flex items-center text-sm">
|
<div className="mb-1 flex items-center text-sm">
|
||||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
|
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
|
||||||
Integrations Update
|
Integrations Update
|
||||||
|
|||||||
+1
-2
@@ -73,7 +73,7 @@ const PageContent = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
<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
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -89,7 +89,6 @@ const PageContent = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="mb-4"
|
|
||||||
>
|
>
|
||||||
Secret Syncs
|
Secret Syncs
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+30
@@ -11,6 +11,7 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
|
|||||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
|
import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
|
||||||
|
|
||||||
type Errors = {
|
type Errors = {
|
||||||
tooShort?: string;
|
tooShort?: string;
|
||||||
@@ -45,6 +46,7 @@ export const ChangePasswordSection = () => {
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const sendSetupPasswordEmail = useSendPasswordSetupEmail();
|
||||||
|
|
||||||
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
||||||
try {
|
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 (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
@@ -142,6 +162,16 @@ export const ChangePasswordSection = () => {
|
|||||||
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
|
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</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>
|
</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 authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route'
|
||||||
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
|
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
|
||||||
import { Route as authEmailNotVerifiedPageRouteImport } from './pages/auth/EmailNotVerifiedPage/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 userLayoutImport } from './pages/user/layout'
|
||||||
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
||||||
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
||||||
@@ -310,6 +311,14 @@ const authEmailNotVerifiedPageRouteRoute =
|
|||||||
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
|
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const authPasswordSetupPageRouteRoute = authPasswordSetupPageRouteImport.update(
|
||||||
|
{
|
||||||
|
id: '/password-setup',
|
||||||
|
path: '/password-setup',
|
||||||
|
getParentRoute: () => middlewaresAuthenticateRoute,
|
||||||
|
} as any,
|
||||||
|
)
|
||||||
|
|
||||||
const userLayoutRoute = userLayoutImport.update({
|
const userLayoutRoute = userLayoutImport.update({
|
||||||
id: '/_layout',
|
id: '/_layout',
|
||||||
getParentRoute: () => AuthenticatePersonalSettingsRoute,
|
getParentRoute: () => AuthenticatePersonalSettingsRoute,
|
||||||
@@ -1577,6 +1586,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
|
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/_restrict-login-signup/email-not-verified': {
|
||||||
id: '/_restrict-login-signup/email-not-verified'
|
id: '/_restrict-login-signup/email-not-verified'
|
||||||
path: '/email-not-verified'
|
path: '/email-not-verified'
|
||||||
@@ -3397,12 +3413,14 @@ const AuthenticatePersonalSettingsRouteWithChildren =
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface middlewaresAuthenticateRouteChildren {
|
interface middlewaresAuthenticateRouteChildren {
|
||||||
|
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
|
||||||
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
|
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
|
||||||
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
|
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
|
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
|
||||||
{
|
{
|
||||||
|
authPasswordSetupPageRouteRoute: authPasswordSetupPageRouteRoute,
|
||||||
middlewaresInjectOrgDetailsRoute:
|
middlewaresInjectOrgDetailsRoute:
|
||||||
middlewaresInjectOrgDetailsRouteWithChildren,
|
middlewaresInjectOrgDetailsRouteWithChildren,
|
||||||
AuthenticatePersonalSettingsRoute:
|
AuthenticatePersonalSettingsRoute:
|
||||||
@@ -3487,6 +3505,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
||||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||||
'': typeof organizationLayoutRouteWithChildren
|
'': typeof organizationLayoutRouteWithChildren
|
||||||
|
'/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||||
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||||
'/password-reset': typeof authPasswordResetPageRouteRoute
|
'/password-reset': typeof authPasswordResetPageRouteRoute
|
||||||
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||||
@@ -3657,6 +3676,7 @@ export interface FileRoutesByTo {
|
|||||||
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
||||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||||
'': typeof organizationLayoutRouteWithChildren
|
'': typeof organizationLayoutRouteWithChildren
|
||||||
|
'/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||||
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||||
'/password-reset': typeof authPasswordResetPageRouteRoute
|
'/password-reset': typeof authPasswordResetPageRouteRoute
|
||||||
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||||
@@ -3824,6 +3844,7 @@ export interface FileRoutesById {
|
|||||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||||
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
|
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
|
||||||
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
|
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
|
||||||
|
'/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||||
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||||
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
|
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
|
||||||
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||||
@@ -4004,6 +4025,7 @@ export interface FileRouteTypes {
|
|||||||
| '/cli-redirect'
|
| '/cli-redirect'
|
||||||
| '/share-secret'
|
| '/share-secret'
|
||||||
| ''
|
| ''
|
||||||
|
| '/password-setup'
|
||||||
| '/email-not-verified'
|
| '/email-not-verified'
|
||||||
| '/password-reset'
|
| '/password-reset'
|
||||||
| '/requestnewinvite'
|
| '/requestnewinvite'
|
||||||
@@ -4173,6 +4195,7 @@ export interface FileRouteTypes {
|
|||||||
| '/cli-redirect'
|
| '/cli-redirect'
|
||||||
| '/share-secret'
|
| '/share-secret'
|
||||||
| ''
|
| ''
|
||||||
|
| '/password-setup'
|
||||||
| '/email-not-verified'
|
| '/email-not-verified'
|
||||||
| '/password-reset'
|
| '/password-reset'
|
||||||
| '/requestnewinvite'
|
| '/requestnewinvite'
|
||||||
@@ -4338,6 +4361,7 @@ export interface FileRouteTypes {
|
|||||||
| '/share-secret'
|
| '/share-secret'
|
||||||
| '/_authenticate'
|
| '/_authenticate'
|
||||||
| '/_restrict-login-signup'
|
| '/_restrict-login-signup'
|
||||||
|
| '/_authenticate/password-setup'
|
||||||
| '/_restrict-login-signup/email-not-verified'
|
| '/_restrict-login-signup/email-not-verified'
|
||||||
| '/_restrict-login-signup/password-reset'
|
| '/_restrict-login-signup/password-reset'
|
||||||
| '/_restrict-login-signup/requestnewinvite'
|
| '/_restrict-login-signup/requestnewinvite'
|
||||||
@@ -4562,6 +4586,7 @@ export const routeTree = rootRoute
|
|||||||
"/_authenticate": {
|
"/_authenticate": {
|
||||||
"filePath": "middlewares/authenticate.tsx",
|
"filePath": "middlewares/authenticate.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
|
"/_authenticate/password-setup",
|
||||||
"/_authenticate/_inject-org-details",
|
"/_authenticate/_inject-org-details",
|
||||||
"/_authenticate/personal-settings"
|
"/_authenticate/personal-settings"
|
||||||
]
|
]
|
||||||
@@ -4579,6 +4604,10 @@ export const routeTree = rootRoute
|
|||||||
"/_restrict-login-signup/admin/signup"
|
"/_restrict-login-signup/admin/signup"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"/_authenticate/password-setup": {
|
||||||
|
"filePath": "auth/PasswordSetupPage/route.tsx",
|
||||||
|
"parent": "/_authenticate"
|
||||||
|
},
|
||||||
"/_restrict-login-signup/email-not-verified": {
|
"/_restrict-login-signup/email-not-verified": {
|
||||||
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
|
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
|
||||||
"parent": "/_restrict-login-signup"
|
"parent": "/_restrict-login-signup"
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ export const routes = rootRoute("root.tsx", [
|
|||||||
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
|
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
|
||||||
]),
|
]),
|
||||||
middleware("authenticate.tsx", [
|
middleware("authenticate.tsx", [
|
||||||
|
route("/password-setup", "auth/PasswordSetupPage/route.tsx"),
|
||||||
route("/personal-settings", [
|
route("/personal-settings", [
|
||||||
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
|
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
|
||||||
]),
|
]),
|
||||||
|
|||||||
Reference in New Issue
Block a user