mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 19:28:09 +00:00
feat: reset password without emergency kit
This commit is contained in:
@@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
|
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { UserEncryption } from "@app/services/user/user-types";
|
||||||
|
|
||||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
message: z.string(),
|
|
||||||
user: UsersSchema,
|
user: UsersSchema,
|
||||||
token: z.string()
|
token: z.string(),
|
||||||
|
userEncryptionVersion: z.nativeEnum(UserEncryption)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
|
const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
|
||||||
|
|
||||||
return {
|
return passwordReset;
|
||||||
message: "Successfully verified email",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
|
|||||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||||
import { registerMfaRouter } from "./mfa-router";
|
import { registerMfaRouter } from "./mfa-router";
|
||||||
import { registerOrgRouter } from "./organization-router";
|
import { registerOrgRouter } from "./organization-router";
|
||||||
|
import { registerPasswordRouter } from "./password-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerServiceTokenRouter } from "./service-token-router";
|
import { registerServiceTokenRouter } from "./service-token-router";
|
||||||
@@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerMfaRouter, { prefix: "/auth" });
|
await server.register(registerMfaRouter, { prefix: "/auth" });
|
||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
||||||
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (orgRouter) => {
|
async (orgRouter) => {
|
||||||
await orgRouter.register(registerOrgRouter);
|
await orgRouter.register(registerOrgRouter);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
|
||||||
|
|
||||||
|
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/password-reset",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
newPassword: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const token = validatePasswordResetAuthorization(req.headers.authorization);
|
||||||
|
await server.services.password.resetPasswordV2({
|
||||||
|
type: ResetPasswordV2Type.Recovery,
|
||||||
|
newPassword: req.body.newPassword,
|
||||||
|
userId: token.userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/user/password-reset",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
oldPassword: z.string().trim(),
|
||||||
|
newPassword: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.password.resetPasswordV2({
|
||||||
|
type: ResetPasswordV2Type.LoggedInReset,
|
||||||
|
userId: req.permission.id,
|
||||||
|
newPassword: req.body.newPassword,
|
||||||
|
oldPassword: req.body.oldPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -45,6 +45,30 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
|
|||||||
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validatePasswordResetAuthorization = (token?: string) => {
|
||||||
|
if (!token) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
|
||||||
|
if (AUTH_TOKEN_TYPE === null) {
|
||||||
|
throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." });
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_VALUE === null) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Missing Authorization Body in the request header"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
|
||||||
|
|
||||||
|
return decodedToken;
|
||||||
|
};
|
||||||
|
|
||||||
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
|
|||||||
@@ -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 { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
@@ -12,10 +14,13 @@ import { TokenType } from "../auth-token/auth-token-types";
|
|||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
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 { UserEncryption } from "../user/user-types";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import {
|
import {
|
||||||
|
ResetPasswordV2Type,
|
||||||
TChangePasswordDTO,
|
TChangePasswordDTO,
|
||||||
TCreateBackupPrivateKeyDTO,
|
TCreateBackupPrivateKeyDTO,
|
||||||
|
TResetPasswordV2DTO,
|
||||||
TResetPasswordViaBackupKeyDTO,
|
TResetPasswordViaBackupKeyDTO,
|
||||||
TSetupPasswordViaBackupKeyDTO
|
TSetupPasswordViaBackupKeyDTO
|
||||||
} from "./auth-password-type";
|
} from "./auth-password-type";
|
||||||
@@ -114,10 +119,11 @@ export const authPaswordServiceFactory = ({
|
|||||||
* Email password reset flow via email. Step 1 send email
|
* Email password reset flow via email. Step 1 send email
|
||||||
*/
|
*/
|
||||||
const sendPasswordResetEmail = async (email: string) => {
|
const sendPasswordResetEmail = async (email: string) => {
|
||||||
const user = await userDAL.findUserByUsername(email);
|
const startTime = Date.now();
|
||||||
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
|
||||||
if (!user || (user && !user.isAccepted)) return;
|
|
||||||
|
|
||||||
|
const user = await userDAL.findUserByUsername(email);
|
||||||
|
|
||||||
|
if (user && user.isAccepted) {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const token = await tokenService.createTokenForUser({
|
const token = await tokenService.createTokenForUser({
|
||||||
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
@@ -134,6 +140,14 @@ export const authPaswordServiceFactory = ({
|
|||||||
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTime = Date.now() - startTime;
|
||||||
|
// daniel: ensure each request takes 8 seconds to prevent timing attacks
|
||||||
|
if (elapsedTime < 8_000) {
|
||||||
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 8_000 - elapsedTime));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -142,6 +156,11 @@ export const authPaswordServiceFactory = ({
|
|||||||
const verifyPasswordResetEmail = async (email: string, code: string) => {
|
const verifyPasswordResetEmail = async (email: string, code: string) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const user = await userDAL.findUserByUsername(email);
|
const user = await userDAL.findUserByUsername(email);
|
||||||
|
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
|
|
||||||
|
if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
|
||||||
|
|
||||||
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts
|
||||||
if (!user || (user && !user.isAccepted)) {
|
if (!user || (user && !user.isAccepted)) {
|
||||||
throw new Error("Failed email verification for pass reset");
|
throw new Error("Failed email verification for pass reset");
|
||||||
@@ -162,8 +181,95 @@ export const authPaswordServiceFactory = ({
|
|||||||
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
|
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { token, user };
|
return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const user = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.hashedPassword) {
|
||||||
|
throw new BadRequestError({ message: "Unable to reset password, no password is set" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
|
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// we check the old password if the user is resetting their password while logged in
|
||||||
|
if (type === ResetPasswordV2Type.LoggedInReset) {
|
||||||
|
if (!oldPassword) {
|
||||||
|
throw new BadRequestError({ message: "Current password is required." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new BadRequestError({ message: "Incorrect current password." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
|
// we need to get the original private key first for v2
|
||||||
|
let privateKey: string;
|
||||||
|
if (
|
||||||
|
user.serverEncryptedPrivateKey &&
|
||||||
|
user.serverEncryptedPrivateKeyTag &&
|
||||||
|
user.serverEncryptedPrivateKeyIV &&
|
||||||
|
user.serverEncryptedPrivateKeyEncoding &&
|
||||||
|
user.encryptionVersion === UserEncryption.V2
|
||||||
|
) {
|
||||||
|
privateKey = infisicalSymmetricDecrypt({
|
||||||
|
iv: user.serverEncryptedPrivateKeyIV,
|
||||||
|
tag: user.serverEncryptedPrivateKeyTag,
|
||||||
|
ciphertext: user.serverEncryptedPrivateKey,
|
||||||
|
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Cannot reset password without current credentials or recovery method",
|
||||||
|
name: "Reset password"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
|
||||||
|
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
await userDAL.updateUserEncryptionByUserId(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
hashedPassword: newHashedPassword,
|
||||||
|
|
||||||
|
// srp params
|
||||||
|
salt: encKeys.salt,
|
||||||
|
verifier: encKeys.verifier,
|
||||||
|
|
||||||
|
protectedKey: encKeys.protectedKey,
|
||||||
|
protectedKeyIV: encKeys.protectedKeyIV,
|
||||||
|
protectedKeyTag: encKeys.protectedKeyTag,
|
||||||
|
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||||
|
iv: encKeys.encryptedPrivateKeyIV,
|
||||||
|
tag: encKeys.encryptedPrivateKeyTag,
|
||||||
|
|
||||||
|
serverEncryptedPrivateKey: ciphertext,
|
||||||
|
serverEncryptedPrivateKeyIV: iv,
|
||||||
|
serverEncryptedPrivateKeyTag: tag,
|
||||||
|
serverEncryptedPrivateKeyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Reset password of a user via backup key
|
* Reset password of a user via backup key
|
||||||
* */
|
* */
|
||||||
@@ -391,6 +497,7 @@ export const authPaswordServiceFactory = ({
|
|||||||
createBackupPrivateKey,
|
createBackupPrivateKey,
|
||||||
getBackupPrivateKeyOfUser,
|
getBackupPrivateKeyOfUser,
|
||||||
sendPasswordSetupEmail,
|
sendPasswordSetupEmail,
|
||||||
setupPassword
|
setupPassword,
|
||||||
|
resetPasswordV2
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ export type TChangePasswordDTO = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ResetPasswordV2Type {
|
||||||
|
Recovery = "recovery",
|
||||||
|
LoggedInReset = "logged-in-reset"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TResetPasswordV2DTO = {
|
||||||
|
type: ResetPasswordV2Type;
|
||||||
|
userId: string;
|
||||||
|
newPassword: string;
|
||||||
|
oldPassword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TResetPasswordViaBackupKeyDTO = {
|
export type TResetPasswordViaBackupKeyDTO = {
|
||||||
userId: string;
|
userId: string;
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
|
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
@@ -31,6 +33,7 @@ type TUserServiceFactoryDep = {
|
|||||||
| "createUserAction"
|
| "createUserAction"
|
||||||
| "findUserEncKeyByUserId"
|
| "findUserEncKeyByUserId"
|
||||||
| "delete"
|
| "delete"
|
||||||
|
| "updateUserEncryptionByUserId"
|
||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { generateUserBackupKey } from "@app/lib/crypto";
|
|
||||||
|
|
||||||
import { createNotification } from "../notifications";
|
|
||||||
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
|
|
||||||
import { Button } from "../v2";
|
|
||||||
|
|
||||||
interface DownloadBackupPDFStepProps {
|
|
||||||
incrementStep: () => void;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the step of the signup flow where the user downloads the backup pdf
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
|
|
||||||
* @param {string} obj.email - user's email
|
|
||||||
* @param {string} obj.password - user's password
|
|
||||||
* @param {string} obj.name - user's name
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function DonwloadBackupPDFStep({
|
|
||||||
incrementStep,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
name
|
|
||||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useToggle();
|
|
||||||
|
|
||||||
const handleBackupKeyGenerate = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading.on();
|
|
||||||
const generatedKey = await generateUserBackupKey(email, password);
|
|
||||||
await generateBackupPDFAsync({
|
|
||||||
generatedKey,
|
|
||||||
personalEmail: email,
|
|
||||||
personalName: name
|
|
||||||
});
|
|
||||||
incrementStep();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to generate backup key"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading.off();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
|
|
||||||
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faWarning}
|
|
||||||
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
|
|
||||||
/>
|
|
||||||
{t("signup.step4-message")}
|
|
||||||
</p>
|
|
||||||
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
|
|
||||||
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
|
|
||||||
<span className="mb-2">
|
|
||||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
|
||||||
<div className="text-l w-full py-1 text-lg">
|
|
||||||
<Button
|
|
||||||
onClick={handleBackupKeyGenerate}
|
|
||||||
size="sm"
|
|
||||||
isFullWidth
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
className="h-12"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
>
|
|
||||||
Download PDF
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -34,12 +34,12 @@ const passwordCheck = async ({
|
|||||||
const tests = [
|
const tests = [
|
||||||
{
|
{
|
||||||
name: "tooShort",
|
name: "tooShort",
|
||||||
validator: (pwd: string) => pwd.length >= 14,
|
validator: (pwd: string) => pwd?.length >= 14,
|
||||||
setError: setPasswordErrorTooShort
|
setError: setPasswordErrorTooShort
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "tooLong",
|
name: "tooLong",
|
||||||
validator: (pwd: string) => pwd.length < 101,
|
validator: (pwd: string) => pwd?.length < 101,
|
||||||
setError: setPasswordErrorTooLong
|
setError: setPasswordErrorTooLong
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export {
|
|||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
useOauthTokenExchange,
|
useOauthTokenExchange,
|
||||||
useResetPassword,
|
useResetPassword,
|
||||||
|
useResetPasswordV2,
|
||||||
|
useResetUserPasswordV2,
|
||||||
useSelectOrganization,
|
useSelectOrganization,
|
||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
useSendPasswordResetEmail,
|
useSendPasswordResetEmail,
|
||||||
|
|||||||
@@ -22,12 +22,15 @@ import {
|
|||||||
LoginLDAPRes,
|
LoginLDAPRes,
|
||||||
MfaMethod,
|
MfaMethod,
|
||||||
ResetPasswordDTO,
|
ResetPasswordDTO,
|
||||||
|
ResetPasswordV2DTO,
|
||||||
|
ResetUserPasswordV2DTO,
|
||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
SetupPasswordDTO,
|
SetupPasswordDTO,
|
||||||
SRP1DTO,
|
SRP1DTO,
|
||||||
SRPR1Res,
|
SRPR1Res,
|
||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
UserAgentType,
|
UserAgentType,
|
||||||
|
UserEncryptionVersion,
|
||||||
VerifyMfaTokenDTO,
|
VerifyMfaTokenDTO,
|
||||||
VerifyMfaTokenRes,
|
VerifyMfaTokenRes,
|
||||||
VerifySignupInviteDTO
|
VerifySignupInviteDTO
|
||||||
@@ -247,7 +250,10 @@ export const useSendPasswordResetEmail = () => {
|
|||||||
export const useVerifyPasswordResetCode = () => {
|
export const useVerifyPasswordResetCode = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ email, code }: { email: string; code: string }) => {
|
mutationFn: async ({ email, code }: { email: string; code: string }) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
|
const { data } = await apiRequest.post<{
|
||||||
|
token: string;
|
||||||
|
userEncryptionVersion: UserEncryptionVersion;
|
||||||
|
}>("/api/v1/password/email/password-reset-verify", {
|
||||||
email,
|
email,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
@@ -302,6 +308,26 @@ export const useResetPassword = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useResetPasswordV2 = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (details: ResetPasswordV2DTO) => {
|
||||||
|
await apiRequest.post("/api/v2/password/password-reset", details, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${details.verificationToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetUserPasswordV2 = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (details: ResetUserPasswordV2DTO) => {
|
||||||
|
await apiRequest.post("/api/v2/password/user/password-reset", details);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const changePassword = async (details: ChangePasswordDTO) => {
|
export const changePassword = async (details: ChangePasswordDTO) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
|
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ export type GetAuthTokenAPI = {
|
|||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum UserEncryptionVersion {
|
||||||
|
V1 = 1,
|
||||||
|
V2 = 2
|
||||||
|
}
|
||||||
|
|
||||||
export type SendMfaTokenDTO = {
|
export type SendMfaTokenDTO = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
@@ -136,6 +141,16 @@ export type ResetPasswordDTO = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ResetPasswordV2DTO = {
|
||||||
|
newPassword: string;
|
||||||
|
verificationToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResetUserPasswordV2DTO = {
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SetupPasswordDTO = {
|
export type SetupPasswordDTO = {
|
||||||
protectedKey: string;
|
protectedKey: string;
|
||||||
protectedKeyIV: string;
|
protectedKeyIV: string;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -8,16 +7,13 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF";
|
|
||||||
// TODO(akhilmhdh): rewrite this into module functions in lib
|
// TODO(akhilmhdh): rewrite this into module functions in lib
|
||||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
|
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
|
||||||
import { useServerConfig } from "@app/context";
|
import { useServerConfig } from "@app/context";
|
||||||
import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api";
|
import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto";
|
import { generateUserPassKey } from "@app/lib/crypto";
|
||||||
|
|
||||||
import { DownloadBackupKeys } from "./components/DownloadBackupKeys";
|
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -34,25 +30,17 @@ const formSchema = z
|
|||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
enum SignupSteps {
|
|
||||||
DetailsForm = "details-form",
|
|
||||||
BackupKey = "backup-key"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SignUpPage = () => {
|
export const SignUpPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
getValues,
|
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const [step, setStep] = useState(SignupSteps.DetailsForm);
|
|
||||||
|
|
||||||
const { config } = useServerConfig();
|
const { config } = useServerConfig();
|
||||||
const { mutateAsync: createAdminUser } = useCreateAdminUser();
|
const { mutateAsync: createAdminUser } = useCreateAdminUser();
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
@@ -84,7 +72,7 @@ export const SignUpPage = () => {
|
|||||||
// Will be refactored in next iteration to make it url based rather than local storage ones
|
// Will be refactored in next iteration to make it url based rather than local storage ones
|
||||||
// Part of migration to nextjs 14
|
// Part of migration to nextjs 14
|
||||||
localStorage.setItem("orgData.id", res.organization.id);
|
localStorage.setItem("orgData.id", res.organization.id);
|
||||||
setStep(SignupSteps.BackupKey);
|
navigate({ to: "/admin" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -94,27 +82,7 @@ export const SignUpPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackupKeyGenerate = async () => {
|
if (config?.initialized) return <ContentLoader text="Redirecting to admin page..." />;
|
||||||
try {
|
|
||||||
const { email, password, firstName, lastName } = getValues();
|
|
||||||
const generatedKey = await generateUserBackupKey(email, password);
|
|
||||||
await generateBackupPDFAsync({
|
|
||||||
generatedKey,
|
|
||||||
personalEmail: email,
|
|
||||||
personalName: `${firstName} ${lastName}`
|
|
||||||
});
|
|
||||||
navigate({ to: "/admin" });
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to generate backup"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config?.initialized && step === SignupSteps.DetailsForm)
|
|
||||||
return <ContentLoader text="Redirecting to admin page..." />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
|
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
|
||||||
@@ -127,7 +95,6 @@ export const SignUpPage = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{step === SignupSteps.DetailsForm && (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-mineshaft-200"
|
className="text-mineshaft-200"
|
||||||
key="panel-1"
|
key="panel-1"
|
||||||
@@ -175,11 +142,7 @@ export const SignUpPage = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl label="Email" errorText={error?.message} isError={Boolean(error)}>
|
||||||
label="Email"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Input isFullWidth size="md" {...field} />
|
<Input isFullWidth size="md" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -223,19 +186,6 @@ export const SignUpPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
|
||||||
{step === SignupSteps.BackupKey && (
|
|
||||||
<motion.div
|
|
||||||
className="text-mineshaft-200"
|
|
||||||
key="panel-2"
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
|
||||||
>
|
|
||||||
<DownloadBackupKeys onGenerate={handleBackupKeyGenerate} />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-56
@@ -1,56 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onGenerate: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isLoading, setIsLoading] = useToggle();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
|
|
||||||
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faWarning}
|
|
||||||
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
|
|
||||||
/>
|
|
||||||
{t("signup.step4-message")}
|
|
||||||
</p>
|
|
||||||
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
|
|
||||||
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
|
|
||||||
<span className="mb-2">
|
|
||||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
|
||||||
<div className="text-l w-full py-1 text-lg">
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading.on();
|
|
||||||
await onGenerate();
|
|
||||||
} finally {
|
|
||||||
setIsLoading.off();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
isFullWidth
|
|
||||||
className="h-12"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
Download PDF
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DownloadBackupKeys } from "./DownloadBackupKeys";
|
|
||||||
@@ -1,396 +1,75 @@
|
|||||||
import crypto from "crypto";
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { FormEvent, useState } from "react";
|
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||||
import { faCheck, 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 InputField from "@app/components/basic/InputField";
|
import { ConfirmEmailStep } from "./components/ConfirmEmailStep";
|
||||||
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
|
import { EnterPasswordStep } from "./components/EnterPasswordStep";
|
||||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
import { InputBackupKeyStep } from "./components/InputBackupKeyStep";
|
||||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
|
||||||
import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api";
|
|
||||||
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
|
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
enum Steps {
|
||||||
const client = new jsrp.client();
|
ConfirmEmail = 1,
|
||||||
|
InputBackupKey = 2,
|
||||||
|
EnterNewPassword = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = z.object({
|
||||||
|
verificationToken: z.string(),
|
||||||
|
privateKey: z.string(),
|
||||||
|
userEncryptionVersion: z.nativeEnum(UserEncryptionVersion)
|
||||||
|
});
|
||||||
|
type TFormData = z.infer<typeof formData>;
|
||||||
|
|
||||||
export const PasswordResetPage = () => {
|
export const PasswordResetPage = () => {
|
||||||
const [verificationToken, setVerificationToken] = useState("");
|
const { watch, setValue } = useForm<TFormData>({
|
||||||
const [step, setStep] = useState(1);
|
resolver: zodResolver(formData)
|
||||||
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 verificationToken = watch("verificationToken");
|
||||||
|
const encryptionVersion = watch("userEncryptionVersion");
|
||||||
|
const privateKey = watch("privateKey");
|
||||||
|
|
||||||
|
const [step, setStep] = useState<Steps>(Steps.ConfirmEmail);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: verifyPasswordResetCodeMutateAsync,
|
|
||||||
isPending: 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,
|
|
||||||
password: newPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
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"
|
|
||||||
style={{ height: "262px", width: "410px" }}
|
|
||||||
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 (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
|
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
|
||||||
{step === 1 && stepConfirmEmail}
|
{step === Steps.ConfirmEmail && (
|
||||||
{step === 2 && stepInputBackupKey}
|
<ConfirmEmailStep
|
||||||
{step === 3 && stepEnterNewPassword}
|
onComplete={(verifyToken, userEncryptionVersion) => {
|
||||||
|
setValue("verificationToken", verifyToken);
|
||||||
|
setValue("userEncryptionVersion", userEncryptionVersion);
|
||||||
|
|
||||||
|
if (userEncryptionVersion === UserEncryptionVersion.V2) {
|
||||||
|
setStep(Steps.EnterNewPassword);
|
||||||
|
} else {
|
||||||
|
setStep(Steps.InputBackupKey);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === Steps.InputBackupKey && (
|
||||||
|
<InputBackupKeyStep
|
||||||
|
verificationToken={verificationToken}
|
||||||
|
onComplete={(key) => {
|
||||||
|
setValue("privateKey", key);
|
||||||
|
setStep(Steps.EnterNewPassword);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{step === Steps.EnterNewPassword && (
|
||||||
|
<EnterPasswordStep
|
||||||
|
verificationToken={verificationToken}
|
||||||
|
privateKey={privateKey}
|
||||||
|
encryptionVersion={encryptionVersion}
|
||||||
|
onComplete={() => {
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useVerifyPasswordResetCode } from "@app/hooks/api";
|
||||||
|
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onComplete: (verificationToken: string, encryptionVersion: UserEncryptionVersion) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfirmEmailStep = ({ onComplete }: Props) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
|
||||||
|
const { token, to: email } = search;
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: verifyPasswordResetCodeMutateAsync,
|
||||||
|
isPending: isVerifyPasswordResetLoading
|
||||||
|
} = useVerifyPasswordResetCode();
|
||||||
|
return (
|
||||||
|
<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"
|
||||||
|
style={{ height: "262px", width: "410px" }}
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
onComplete(response.token, response.userEncryptionVersion);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("ERROR", err);
|
||||||
|
navigate({ to: "/email-not-verified" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={isVerifyPasswordResetLoading}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
Confirm Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useSearch } from "@tanstack/react-router";
|
||||||
|
import jsrp from "jsrp";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
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, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useResetPassword, useResetPasswordV2 } from "@app/hooks/api";
|
||||||
|
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
|
const formData = z.object({
|
||||||
|
password: z.string(),
|
||||||
|
passwordErrorTooShort: z.boolean().optional(),
|
||||||
|
passwordErrorTooLong: z.boolean().optional(),
|
||||||
|
passwordErrorNoLetterChar: z.boolean().optional(),
|
||||||
|
passwordErrorNoNumOrSpecialChar: z.boolean().optional(),
|
||||||
|
passwordErrorRepeatedChar: z.boolean().optional(),
|
||||||
|
passwordErrorEscapeChar: z.boolean().optional(),
|
||||||
|
passwordErrorLowEntropy: z.boolean().optional(),
|
||||||
|
passwordErrorBreached: z.boolean()
|
||||||
|
});
|
||||||
|
type TFormData = z.infer<typeof formData>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
verificationToken: string;
|
||||||
|
privateKey: string;
|
||||||
|
encryptionVersion: UserEncryptionVersion;
|
||||||
|
onComplete: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnterPasswordStep = ({
|
||||||
|
verificationToken,
|
||||||
|
encryptionVersion,
|
||||||
|
privateKey,
|
||||||
|
onComplete
|
||||||
|
}: Props) => {
|
||||||
|
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
|
||||||
|
const { to: email } = search;
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(formData)
|
||||||
|
});
|
||||||
|
const { mutateAsync: resetPassword, isPending: isLoading } = useResetPassword();
|
||||||
|
const { mutateAsync: resetPasswordV2, isPending: isLoadingV2 } = useResetPasswordV2();
|
||||||
|
|
||||||
|
const passwordErrorTooShort = watch("passwordErrorTooShort");
|
||||||
|
const passwordErrorTooLong = watch("passwordErrorTooLong");
|
||||||
|
const passwordErrorNoLetterChar = watch("passwordErrorNoLetterChar");
|
||||||
|
const passwordErrorNoNumOrSpecialChar = watch("passwordErrorNoNumOrSpecialChar");
|
||||||
|
const passwordErrorRepeatedChar = watch("passwordErrorRepeatedChar");
|
||||||
|
const passwordErrorEscapeChar = watch("passwordErrorEscapeChar");
|
||||||
|
const passwordErrorLowEntropy = watch("passwordErrorLowEntropy");
|
||||||
|
const passwordErrorBreached = watch("passwordErrorBreached");
|
||||||
|
|
||||||
|
const isPasswordError =
|
||||||
|
passwordErrorTooShort ||
|
||||||
|
passwordErrorTooLong ||
|
||||||
|
passwordErrorNoLetterChar ||
|
||||||
|
passwordErrorNoNumOrSpecialChar ||
|
||||||
|
passwordErrorRepeatedChar ||
|
||||||
|
passwordErrorEscapeChar ||
|
||||||
|
passwordErrorLowEntropy ||
|
||||||
|
passwordErrorBreached;
|
||||||
|
|
||||||
|
const handlePasswordCheck = async (checkPassword: string) => {
|
||||||
|
const errorCheck = await passwordCheck({
|
||||||
|
password: checkPassword,
|
||||||
|
setPasswordErrorTooShort: (v) => setValue("passwordErrorTooShort", v),
|
||||||
|
setPasswordErrorTooLong: (v) => setValue("passwordErrorTooLong", v),
|
||||||
|
setPasswordErrorNoLetterChar: (v) => setValue("passwordErrorNoLetterChar", v),
|
||||||
|
setPasswordErrorNoNumOrSpecialChar: (v) => setValue("passwordErrorNoNumOrSpecialChar", v),
|
||||||
|
setPasswordErrorRepeatedChar: (v) => setValue("passwordErrorRepeatedChar", v),
|
||||||
|
setPasswordErrorEscapeChar: (v) => setValue("passwordErrorEscapeChar", v),
|
||||||
|
setPasswordErrorLowEntropy: (v) => setValue("passwordErrorLowEntropy", v),
|
||||||
|
setPasswordErrorBreached: (v) => setValue("passwordErrorBreached", v)
|
||||||
|
});
|
||||||
|
|
||||||
|
return errorCheck;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPasswordHandler = async (data: TFormData) => {
|
||||||
|
const errorCheck = await handlePasswordCheck(data.password);
|
||||||
|
|
||||||
|
if (errorCheck) return;
|
||||||
|
|
||||||
|
if (encryptionVersion === UserEncryptionVersion.V2) {
|
||||||
|
await resetPasswordV2({
|
||||||
|
newPassword: data.password,
|
||||||
|
verificationToken
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const client = new jsrp.client();
|
||||||
|
client.init(
|
||||||
|
{
|
||||||
|
username: email,
|
||||||
|
password: data.password
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
|
||||||
|
const derivedKey = await deriveArgonKey({
|
||||||
|
password: data.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)
|
||||||
|
});
|
||||||
|
|
||||||
|
await resetPassword({
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier,
|
||||||
|
verificationToken,
|
||||||
|
password: data.password
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(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">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
label="New Password"
|
||||||
|
isRequired
|
||||||
|
isError={isPasswordError}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e);
|
||||||
|
handlePasswordCheck(e.target.value);
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting || isLoading || isLoadingV2}
|
||||||
|
isDisabled={isSubmitting || isLoading || isLoadingV2}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||||
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
verificationToken: string;
|
||||||
|
onComplete: (privateKey: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = z.object({
|
||||||
|
backupKey: z.string()
|
||||||
|
});
|
||||||
|
type TFormData = z.infer<typeof formData>;
|
||||||
|
|
||||||
|
export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) => {
|
||||||
|
const { control, handleSubmit, setError } = useForm<TFormData>({
|
||||||
|
resolver: zodResolver(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEncryptedKeyHandler = async (data: z.infer<typeof formData>) => {
|
||||||
|
try {
|
||||||
|
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||||
|
|
||||||
|
const privateKey = Aes256Gcm.decrypt({
|
||||||
|
ciphertext: result.encryptedPrivateKey,
|
||||||
|
iv: result.iv,
|
||||||
|
tag: result.tag,
|
||||||
|
secret: data.backupKey
|
||||||
|
});
|
||||||
|
|
||||||
|
onComplete(privateKey);
|
||||||
|
// setStep(3);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("backupKey", { message: "Failed to decrypt private key" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(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">
|
||||||
|
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-full px-4 text-center text-sm text-gray-400 sm:max-w-md">
|
||||||
|
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">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="backupKey"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Backup Key"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder="08af467b815ffa412f2c98cc3326acdb"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</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" colorSchema="secondary">
|
||||||
|
Submit Backup Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
@@ -16,7 +16,6 @@ import InputField from "@app/components/basic/InputField";
|
|||||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||||
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
|
||||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
@@ -54,8 +53,6 @@ export const SignupInvitePage = () => {
|
|||||||
const [lastNameError, setLastNameError] = useState(false);
|
const [lastNameError, setLastNameError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [, setBackupKeyError] = useState(false);
|
|
||||||
const [, setBackupKeyIssued] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
|
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
@@ -205,7 +202,9 @@ export const SignupInvitePage = () => {
|
|||||||
|
|
||||||
localStorage.setItem("orgData.id", orgId);
|
localStorage.setItem("orgData.id", orgId);
|
||||||
|
|
||||||
setStep(3);
|
navigate({
|
||||||
|
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
await completeSignupFlow();
|
await completeSignupFlow();
|
||||||
@@ -367,44 +366,6 @@ export const SignupInvitePage = () => {
|
|||||||
</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/${ProjectType.SecretManager}/overview` as const
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
Download PDF
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
<Helmet>
|
||||||
@@ -425,7 +386,8 @@ export const SignupInvitePage = () => {
|
|||||||
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4}
|
{step === 1 && stepConfirmEmail}
|
||||||
|
{step === 2 && main}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
import CodeInputStep from "@app/components/auth/CodeInputStep";
|
import CodeInputStep from "@app/components/auth/CodeInputStep";
|
||||||
import DownloadBackupPDF from "@app/components/auth/DonwloadBackupPDFStep";
|
|
||||||
import EnterEmailStep from "@app/components/auth/EnterEmailStep";
|
import EnterEmailStep from "@app/components/auth/EnterEmailStep";
|
||||||
import InitialSignupStep from "@app/components/auth/InitialSignupStep";
|
import InitialSignupStep from "@app/components/auth/InitialSignupStep";
|
||||||
import TeamInviteStep from "@app/components/auth/TeamInviteStep";
|
import TeamInviteStep from "@app/components/auth/TeamInviteStep";
|
||||||
@@ -72,7 +71,7 @@ export const SignUpPage = () => {
|
|||||||
incrementStep();
|
incrementStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serverDetails?.emailConfigured && step === 5) {
|
if (!serverDetails?.emailConfigured && step === 4) {
|
||||||
navigate({
|
navigate({
|
||||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||||
});
|
});
|
||||||
@@ -119,17 +118,6 @@ export const SignUpPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registerStep === 4) {
|
|
||||||
return (
|
|
||||||
<DownloadBackupPDF
|
|
||||||
incrementStep={incrementStep}
|
|
||||||
email={email}
|
|
||||||
password={password}
|
|
||||||
name={name}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverDetails?.emailConfigured) {
|
if (serverDetails?.emailConfigured) {
|
||||||
return <TeamInviteStep />;
|
return <TeamInviteStep />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { jwtDecode } from "jwt-decode";
|
|||||||
|
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
|
||||||
import { BackupPDFStep } from "./components/BackupPDFStep";
|
|
||||||
import { EmailConfirmationStep } from "./components/EmailConfirmationStep";
|
import { EmailConfirmationStep } from "./components/EmailConfirmationStep";
|
||||||
import { UserInfoSSOStep } from "./components/UserInfoSSOStep";
|
import { UserInfoSSOStep } from "./components/UserInfoSSOStep";
|
||||||
|
|
||||||
@@ -57,14 +56,9 @@ export const SignupSsoPage = () => {
|
|||||||
providerOrganizationName={organizationName}
|
providerOrganizationName={organizationName}
|
||||||
password={password}
|
password={password}
|
||||||
setPassword={setPassword}
|
setPassword={setPassword}
|
||||||
setStep={setStep}
|
|
||||||
providerAuthToken={token}
|
providerAuthToken={token}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
|
||||||
|
|
||||||
interface DownloadBackupPDFStepProps {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the step of the signup flow where the user downloads the backup pdf
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
|
|
||||||
* @param {string} obj.email - user's email
|
|
||||||
* @param {string} obj.password - user's password
|
|
||||||
* @param {string} obj.name - user's name
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const BackupPDFStep = ({ email, password, name }: DownloadBackupPDFStepProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
|
|
||||||
<p className="flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
|
||||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
|
|
||||||
{t("signup.step4-message")}
|
|
||||||
</p>
|
|
||||||
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
|
|
||||||
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
|
|
||||||
<span className="mb-2">
|
|
||||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
|
||||||
<div className="text-l w-full py-1 text-lg">
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await issueBackupKey({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
personalName: name,
|
|
||||||
setBackupKeyError: () => {},
|
|
||||||
setBackupKeyIssued: () => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate({
|
|
||||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
isFullWidth
|
|
||||||
className="h-12"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
Download PDF{" "}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { BackupPDFStep } from "./BackupPDFStep";
|
|
||||||
+6
-3
@@ -2,6 +2,7 @@ import crypto from "crypto";
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import jsrp from "jsrp";
|
import jsrp from "jsrp";
|
||||||
import nacl from "tweetnacl";
|
import nacl from "tweetnacl";
|
||||||
import { encodeBase64 } from "tweetnacl-util";
|
import { encodeBase64 } from "tweetnacl-util";
|
||||||
@@ -17,12 +18,12 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setStep: (step: number) => void;
|
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
setPassword: (value: string) => void;
|
setPassword: (value: string) => void;
|
||||||
@@ -50,7 +51,6 @@ export const UserInfoSSOStep = ({
|
|||||||
providerOrganizationName,
|
providerOrganizationName,
|
||||||
password,
|
password,
|
||||||
setPassword,
|
setPassword,
|
||||||
setStep,
|
|
||||||
providerAuthToken
|
providerAuthToken
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [nameError, setNameError] = useState(false);
|
const [nameError, setNameError] = useState(false);
|
||||||
@@ -63,6 +63,7 @@ export const UserInfoSSOStep = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const randomPassword = crypto.randomBytes(32).toString("hex");
|
const randomPassword = crypto.randomBytes(32).toString("hex");
|
||||||
@@ -202,7 +203,9 @@ export const UserInfoSSOStep = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("orgData.id", orgId);
|
localStorage.setItem("orgData.id", orgId);
|
||||||
setStep(2);
|
navigate({
|
||||||
|
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ export const VerifyEmailPage = () => {
|
|||||||
Forgot your password?
|
Forgot your password?
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
<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">
|
<p className="flex w-max justify-center text-center text-sm text-gray-400">
|
||||||
You will need your emergency kit. Enter your email to start account recovery.
|
Enter your email to start the password reset process. You will receive an email with
|
||||||
|
instructions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<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">
|
||||||
|
|||||||
+11
-2
@@ -11,7 +11,8 @@ 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";
|
import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
|
||||||
|
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
type Errors = {
|
type Errors = {
|
||||||
tooShort?: string;
|
tooShort?: string;
|
||||||
@@ -47,6 +48,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 sendSetupPasswordEmail = useSendPasswordSetupEmail();
|
||||||
|
const { mutateAsync: resetPasswordV2 } = useResetUserPasswordV2();
|
||||||
|
|
||||||
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
||||||
try {
|
try {
|
||||||
@@ -56,13 +58,20 @@ export const ChangePasswordSection = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (errorCheck) return;
|
if (errorCheck) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
if (user.encryptionVersion === UserEncryptionVersion.V2) {
|
||||||
|
await resetPasswordV2({
|
||||||
|
oldPassword,
|
||||||
|
newPassword
|
||||||
|
});
|
||||||
|
} else {
|
||||||
await attemptChangePassword({
|
await attemptChangePassword({
|
||||||
email: user.username,
|
email: user.username,
|
||||||
currentPassword: oldPassword,
|
currentPassword: oldPassword,
|
||||||
newPassword
|
newPassword
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
createNotification({
|
createNotification({
|
||||||
|
|||||||
+7
-1
@@ -1,14 +1,20 @@
|
|||||||
|
import { useUser } from "@app/context";
|
||||||
|
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||||
|
|
||||||
import { DeleteAccountSection } from "../DeleteAccountSection";
|
import { DeleteAccountSection } from "../DeleteAccountSection";
|
||||||
import { EmergencyKitSection } from "../EmergencyKitSection";
|
import { EmergencyKitSection } from "../EmergencyKitSection";
|
||||||
import { SessionsSection } from "../SessionsSection";
|
import { SessionsSection } from "../SessionsSection";
|
||||||
import { UserNameSection } from "../UserNameSection";
|
import { UserNameSection } from "../UserNameSection";
|
||||||
|
|
||||||
export const PersonalGeneralTab = () => {
|
export const PersonalGeneralTab = () => {
|
||||||
|
const { user } = useUser();
|
||||||
|
const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserNameSection />
|
<UserNameSection />
|
||||||
<SessionsSection />
|
<SessionsSection />
|
||||||
<EmergencyKitSection />
|
{encryptionVersion === UserEncryptionVersion.V1 && <EmergencyKitSection />}
|
||||||
<DeleteAccountSection />
|
<DeleteAccountSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user