mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 19:26:38 +00:00
Added the account recovery flow
This commit is contained in:
+16
-16
@@ -2,25 +2,25 @@ import { Schema, model } from 'mongoose';
|
|||||||
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
import { EMAIL_TOKEN_LIFETIME } from '../config';
|
||||||
|
|
||||||
export interface IToken {
|
export interface IToken {
|
||||||
email: String;
|
email: string;
|
||||||
token: String;
|
token: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenSchema = new Schema<IToken>({
|
const tokenSchema = new Schema<IToken>({
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
expires: parseInt(EMAIL_TOKEN_LIFETIME),
|
expires: parseInt(EMAIL_TOKEN_LIFETIME),
|
||||||
default: Date.now
|
default: Date.now
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const Token = model<IToken>('Token', tokenSchema);
|
const Token = model<IToken>('Token', tokenSchema);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ const InputField = (
|
|||||||
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
|
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
|
||||||
) => {
|
) => {
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
if (props.static === true) {
|
if (props.static === true) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ interface LayoutProps {
|
|||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [workspaceList, setWorkspaceList] = useState([]);
|
const [workspaceList, setWorkspaceList] = useState([]);
|
||||||
const [workspaceMapping, setWorkspaceMapping] = useState([{ 1: 2 }]);
|
const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]);
|
||||||
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
|
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
|
||||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -221,20 +221,20 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
workspaceMapping[Number(workspaceSelected)] &&
|
workspaceMapping[workspaceSelected as any] &&
|
||||||
`${workspaceMapping[Number(workspaceSelected)]}` !==
|
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||||
router.asPath
|
router.asPath
|
||||||
.split('/')
|
.split('/')
|
||||||
[router.asPath.split('/').length - 1].split('?')[0]
|
[router.asPath.split('/').length - 1].split('?')[0]
|
||||||
) {
|
) {
|
||||||
router.push(
|
router.push(
|
||||||
'/dashboard/' +
|
'/dashboard/' +
|
||||||
workspaceMapping[Number(workspaceSelected)] +
|
workspaceMapping[workspaceSelected as any] +
|
||||||
'?Development'
|
'?Development'
|
||||||
);
|
);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'projectData.id',
|
'projectData.id',
|
||||||
`${workspaceMapping[Number(workspaceSelected)]}`
|
`${workspaceMapping[workspaceSelected as any]}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
interface Props {
|
||||||
|
email: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the second part of the account recovery step (a user needs to verify their email).
|
||||||
|
* A user need to click on a button in a magic link page
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {object} obj.email - email of a user that is trying to recover access to their account
|
||||||
|
* @param {object} obj.code - token that a use received via the magic link
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
|
||||||
|
const response = await fetch('/api/v1/password/email/password-reset-verify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
code: code
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (response?.status === 200) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Something went wrong during email verification on password reset.'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailVerifyOnPasswordReset;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
interface Props {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the first of the account recovery step (a user needs to verify their email).
|
||||||
|
* It will send an email containing a magic link to start the account recovery flow.
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {object} obj.email - email of a user that is trying to recover access to their account
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const SendEmailOnPasswordReset = async ({ email }: Props) => {
|
||||||
|
const response = await fetch('/api/v1/password/email/password-reset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// need precise error handling about the status code
|
||||||
|
if (response?.status === 200) {
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Something went wrong while sending the email verification for password reset.'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SendEmailOnPasswordReset;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* This is the route that get an encrypted private key (will be decrypted with a backup key)
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getBackupEncryptedPrivateKey = ({
|
||||||
|
verificationToken
|
||||||
|
}: {
|
||||||
|
verificationToken: string;
|
||||||
|
}) => {
|
||||||
|
return fetch('/api/v1/password/backup-private-key', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + verificationToken
|
||||||
|
}
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res?.status !== 200) {
|
||||||
|
console.log('Failed to get the backup key');
|
||||||
|
}
|
||||||
|
return (await res?.json())?.backupPrivateKey;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBackupEncryptedPrivateKey;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
interface Props {
|
||||||
|
verificationToken: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the route that resets the account password if all the previus steps were passed
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
|
||||||
|
* @param {object} obj.encryptedPrivateKey - the new encrypted private key (encrypted using the new password)
|
||||||
|
* @param {object} obj.iv
|
||||||
|
* @param {object} obj.tag
|
||||||
|
* @param {object} obj.salt
|
||||||
|
* @param {object} obj.verifier
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const resetPasswordOnAccountRecovery = ({
|
||||||
|
verificationToken,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
}: Props) => {
|
||||||
|
return fetch('/api/v1/password/password-reset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + verificationToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
encryptedPrivateKey: encryptedPrivateKey,
|
||||||
|
iv: iv,
|
||||||
|
tag: tag,
|
||||||
|
salt: salt,
|
||||||
|
verifier: verifier
|
||||||
|
})
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res?.status !== 200) {
|
||||||
|
console.log('Failed to get the backup key');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resetPasswordOnAccountRecovery;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
export default function Activity() {
|
||||||
|
return (
|
||||||
|
<div className="bg-bunker-800 md:h-screen flex flex-col justify-between">
|
||||||
|
<Head>
|
||||||
|
<title>Request a New Invite</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Head>
|
||||||
|
<div className="flex flex-col items-center justify-center text-gray-200 h-screen w-screen">
|
||||||
|
<p className="text-6xl">Oops.</p>
|
||||||
|
<p className="mt-2 mb-1 text-xl">Your email was not verified. </p>
|
||||||
|
<p className="text-xl">Please try again.</p>
|
||||||
|
<p className="text-md mt-8 text-gray-600 max-w-sm text-center">
|
||||||
|
Note: If it still {"doesn't work"}, please reach out to us at
|
||||||
|
support@infisical.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from 'react';
|
||||||
import Head from "next/head";
|
import Head from 'next/head';
|
||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import Button from "~/components/basic/buttons/Button";
|
import Button from '~/components/basic/buttons/Button';
|
||||||
import Error from "~/components/basic/Error";
|
import Error from '~/components/basic/Error';
|
||||||
import InputField from "~/components/basic/InputField";
|
import InputField from '~/components/basic/InputField';
|
||||||
import attemptLogin from "~/utilities/attemptLogin";
|
import attemptLogin from '~/utilities/attemptLogin';
|
||||||
|
|
||||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
import getWorkspaces from './api/workspace/getWorkspaces';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState('');
|
||||||
const [errorLogin, setErrorLogin] = useState(false);
|
const [errorLogin, setErrorLogin] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(() => {
|
||||||
let userWorkspace;
|
const redirectToDashboard = async () => {
|
||||||
try {
|
let userWorkspace;
|
||||||
const userWorkspaces = await getWorkspaces();
|
try {
|
||||||
userWorkspace = userWorkspaces[0]._id;
|
const userWorkspaces = await getWorkspaces();
|
||||||
router.push("/dashboard/" + userWorkspace);
|
userWorkspace = userWorkspaces[0]._id;
|
||||||
} catch (error) {
|
router.push('/dashboard/' + userWorkspace);
|
||||||
console.log("Error - Not logged in yet");
|
} catch (error) {
|
||||||
}
|
console.log('Error - Not logged in yet');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
redirectToDashboard();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,23 +76,9 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||||
<p className="text-4xl flex justify-center font-semibold text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||||
Log In
|
Log in to your account
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row items-center justify-center">
|
|
||||||
<p className="text-md flex justify-center mt-2 text-gray-400">
|
|
||||||
Need an Infisical account?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center justify-center w-full md:pb-4 max-h-24 max-w-md mx-auto">
|
|
||||||
<Link href="/signup">
|
|
||||||
<button className="w-full pb-3 hover:opacity-90 duration-200">
|
|
||||||
<u className="font-normal text-md text-sky-500">
|
|
||||||
Create an account
|
|
||||||
</u>
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label="Email"
|
label="Email"
|
||||||
@@ -98,10 +87,10 @@ export default function Login() {
|
|||||||
value={email}
|
value={email}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
autocomplete="username"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||||
<InputField
|
<InputField
|
||||||
label="Password"
|
label="Password"
|
||||||
onChangeHandler={setPassword}
|
onChangeHandler={setPassword}
|
||||||
@@ -112,6 +101,9 @@ export default function Login() {
|
|||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
id="current-password"
|
id="current-password"
|
||||||
/>
|
/>
|
||||||
|
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||||
|
<Link href="/verify-email">Forgot password?</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{errorLogin && <Error text="Your email and/or password are wrong." />}
|
{errorLogin && <Error text="Your email and/or password are wrong." />}
|
||||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||||
@@ -135,6 +127,16 @@ export default function Login() {
|
|||||||
solving it right now. Please come back in a few minutes.
|
solving it right now. Please come back in a few minutes.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||||
|
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||||
|
Need an Infisical account?
|
||||||
|
</p>
|
||||||
|
<Link href="/signup">
|
||||||
|
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
|
||||||
|
Sign up here.
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import Button from '~/components/basic/buttons/Button';
|
||||||
|
import InputField from '~/components/basic/InputField';
|
||||||
|
import passwordCheck from '~/components/utilities/checks/PasswordCheck';
|
||||||
|
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
|
||||||
|
|
||||||
|
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
|
||||||
|
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
|
||||||
|
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
|
||||||
|
|
||||||
|
const queryString = require('query-string');
|
||||||
|
const nacl = require('tweetnacl');
|
||||||
|
const jsrp = require('jsrp');
|
||||||
|
nacl.util = require('tweetnacl-util');
|
||||||
|
const client = new jsrp.client();
|
||||||
|
|
||||||
|
export default function PasswordReset() {
|
||||||
|
const router = useRouter();
|
||||||
|
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
||||||
|
const token = parsedUrl.token;
|
||||||
|
const email = parsedUrl.to?.replace(' ', '+').trim();
|
||||||
|
const [verificationToken, setVerificationToken] = useState('');
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [backupKey, setBackupKey] = useState('');
|
||||||
|
const [privateKey, setPrivateKey] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||||
|
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||||
|
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||||
|
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||||
|
|
||||||
|
// Unencrypt the private key with a backup key
|
||||||
|
const getEncryptedKeyHandler = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||||
|
setPrivateKey(
|
||||||
|
Aes256Gcm.decrypt({
|
||||||
|
ciphertext: result.encryptedPrivateKey,
|
||||||
|
iv: result.iv,
|
||||||
|
tag: result.tag,
|
||||||
|
secret: backupKey
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setStep(3);
|
||||||
|
} catch {
|
||||||
|
setBackupKeyError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If everything is correct, reset the password
|
||||||
|
const resetPasswordHandler = async () => {
|
||||||
|
let errorCheck = false;
|
||||||
|
errorCheck = passwordCheck({
|
||||||
|
password: newPassword,
|
||||||
|
setPasswordErrorLength,
|
||||||
|
setPasswordErrorNumber,
|
||||||
|
setPasswordErrorLowerCase,
|
||||||
|
currentErrorCheck: errorCheck
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!errorCheck) {
|
||||||
|
// Generate a random pair of a public and a private key
|
||||||
|
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||||
|
text: privateKey,
|
||||||
|
secret: newPassword
|
||||||
|
.slice(0, 32)
|
||||||
|
.padStart(
|
||||||
|
32 +
|
||||||
|
(newPassword.slice(0, 32).length - new Blob([newPassword]).size),
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
}) as { ciphertext: string; iv: string; tag: string };
|
||||||
|
|
||||||
|
client.init(
|
||||||
|
{
|
||||||
|
username: email,
|
||||||
|
password: newPassword
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
client.createVerifier(
|
||||||
|
async (err: any, result: { salt: string; verifier: string }) => {
|
||||||
|
const response = await resetPasswordOnAccountRecovery({
|
||||||
|
verificationToken,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
salt: result.salt,
|
||||||
|
verifier: result.verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
// if everything works, go the main dashboard page.
|
||||||
|
if (response?.status === 200) {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Click a button to confirm email
|
||||||
|
const stepConfirmEmail = (
|
||||||
|
<div className="bg-bunker flex flex-col items-center w-full py-6 max-w-xs md:max-w-lg mx-auto my-32 px-4 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||||
|
<p className="text-4xl text-center font-semibold mb-8 flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||||
|
Confirm your email
|
||||||
|
</p>
|
||||||
|
<Image
|
||||||
|
src="/images/envelope.svg"
|
||||||
|
height={262}
|
||||||
|
width={410}
|
||||||
|
alt="verify email"
|
||||||
|
></Image>
|
||||||
|
<div className="flex max-w-max flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-4 mb-2">
|
||||||
|
<Button
|
||||||
|
text="Confirm Email"
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
const response = await EmailVerifyOnPasswordReset({
|
||||||
|
email,
|
||||||
|
code: token
|
||||||
|
});
|
||||||
|
if (response.status == 200) {
|
||||||
|
setVerificationToken((await response.json()).token);
|
||||||
|
setStep(2);
|
||||||
|
} else {
|
||||||
|
console.log('ERROR', response);
|
||||||
|
router.push('/email-not-verified');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Input backup key
|
||||||
|
const stepInputBackupKey = (
|
||||||
|
<div className="bg-bunker flex flex-col items-center w-full pt-6 pb-3 max-w-xs md:max-w-lg mx-auto my-32 px-4 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||||
|
<p className="text-2xl md:text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-4">
|
||||||
|
Enter your backup key
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
|
||||||
|
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
|
||||||
|
You can find it in your emrgency kit. You had to download the enrgency
|
||||||
|
kit during signup.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||||
|
<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="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||||
|
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||||
|
<Button
|
||||||
|
text="Submit Backup Key"
|
||||||
|
onButtonPressed={() => getEncryptedKeyHandler()}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enter new password
|
||||||
|
const stepEnterNewPassword = (
|
||||||
|
<div className="bg-bunker flex flex-col items-center w-full pt-6 pb-3 max-w-xs md:max-w-lg mx-auto my-32 px-4 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||||
|
<p className="text-2xl md:text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100">
|
||||||
|
Enter new password
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-row items-center justify-center md:pb-4 mt-1 md:mx-2">
|
||||||
|
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
|
||||||
|
Make sure you save it somewhere save.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||||
|
<InputField
|
||||||
|
label="New Password"
|
||||||
|
onChangeHandler={(password) => {
|
||||||
|
setNewPassword(password);
|
||||||
|
passwordCheck({
|
||||||
|
password,
|
||||||
|
setPasswordErrorLength,
|
||||||
|
setPasswordErrorNumber,
|
||||||
|
setPasswordErrorLowerCase,
|
||||||
|
currentErrorCheck: false
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
isRequired
|
||||||
|
error={
|
||||||
|
passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber
|
||||||
|
}
|
||||||
|
autoComplete="new-password"
|
||||||
|
id="new-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
|
||||||
|
<div className="w-full mt-3 bg-white/5 px-2 mx-2 flex flex-col items-start py-2 rounded-md max-w-md mb-2">
|
||||||
|
<div className={`text-gray-400 text-sm mb-1`}>
|
||||||
|
Password should contain at least:
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-start items-center ml-1">
|
||||||
|
{passwordErrorLength ? (
|
||||||
|
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheck}
|
||||||
|
className="text-md text-primary mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
|
||||||
|
} text-sm`}
|
||||||
|
>
|
||||||
|
14 characters
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-start items-center ml-1">
|
||||||
|
{passwordErrorLowerCase ? (
|
||||||
|
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheck}
|
||||||
|
className="text-md text-primary mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
|
||||||
|
} text-sm`}
|
||||||
|
>
|
||||||
|
1 lowercase character
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-start items-center ml-1">
|
||||||
|
{passwordErrorNumber ? (
|
||||||
|
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCheck}
|
||||||
|
className="text-md text-primary mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
|
||||||
|
} text-sm`}
|
||||||
|
>
|
||||||
|
1 number
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-2"></div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||||
|
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||||
|
<Button
|
||||||
|
text="Submit New Password"
|
||||||
|
onButtonPressed={() => resetPasswordHandler()}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-bunker-800 h-screen w-full flex flex-col items-center justify-center">
|
||||||
|
{step === 1 && stepConfirmEmail}
|
||||||
|
{step === 2 && stepInputBackupKey}
|
||||||
|
{step === 3 && stepEnterNewPassword}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ export default function SignupInvite() {
|
|||||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
|
||||||
const [email, setEmail] = useState(parsedUrl.to);
|
const [email, setEmail] = useState(parsedUrl.to.replace(' ', '+').trim());
|
||||||
const token = parsedUrl.token;
|
const token = parsedUrl.token;
|
||||||
const [errorLogin, setErrorLogin] = useState(false);
|
const [errorLogin, setErrorLogin] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -150,7 +150,7 @@ export default function SignupInvite() {
|
|||||||
width={410}
|
width={410}
|
||||||
alt="verify email"
|
alt="verify email"
|
||||||
></Image>
|
></Image>
|
||||||
<div className="flex flex-row items-center justify-center w-3/4 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto text-lg py-1 text-center md:text-left">
|
<div className="flex max-w-max flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-4 mb-2">
|
||||||
<Button
|
<Button
|
||||||
text="Confirm Email"
|
text="Confirm Email"
|
||||||
onButtonPressed={async () => {
|
onButtonPressed={async () => {
|
||||||
@@ -361,7 +361,7 @@ export default function SignupInvite() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker-800 h-screen flex flex-col items-center justify-center">
|
<div className="bg-bunker-800 h-full flex flex-col items-center justify-center">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Sign Up</title>
|
<title>Sign Up</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import Button from '~/components/basic/buttons/Button';
|
||||||
|
import InputField from '~/components/basic/InputField';
|
||||||
|
|
||||||
|
import SendEmailOnPasswordReset from './api/auth/SendEmailOnPasswordReset';
|
||||||
|
|
||||||
|
export default function VerifyEmail() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function sends the verification email and forwards a user to the next step.
|
||||||
|
*/
|
||||||
|
const sendVerificationEmail = async () => {
|
||||||
|
if (email) {
|
||||||
|
await SendEmailOnPasswordReset({ email });
|
||||||
|
setStep(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-bunker-800 h-screen flex flex-col justify-start px-6">
|
||||||
|
<Head>
|
||||||
|
<title>Login</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content="Log In to Infisical" />
|
||||||
|
<meta
|
||||||
|
name="og:description"
|
||||||
|
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<Link href="/">
|
||||||
|
<div className="flex justify-center mb-8 mt-20 cursor-pointer">
|
||||||
|
<Image
|
||||||
|
src="/images/biglogo.png"
|
||||||
|
height={90}
|
||||||
|
width={120}
|
||||||
|
alt="long logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{step == 1 && (
|
||||||
|
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||||
|
<p className="text-2xl md:text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||||
|
Forgot your password?
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
|
||||||
|
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||||
|
You will need your emergency kit. Enter your email to start
|
||||||
|
account recovery.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||||
|
<InputField
|
||||||
|
label="Email"
|
||||||
|
onChangeHandler={setEmail}
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
placeholder=""
|
||||||
|
isRequired
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||||
|
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||||
|
<Button
|
||||||
|
text="Continue"
|
||||||
|
onButtonPressed={sendVerificationEmail}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step == 2 && (
|
||||||
|
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||||
|
<p className="text-xl md:text-2xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||||
|
Look for an email in your inbox.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
|
||||||
|
<p className="text-sm flex justify-center text-gray-400 w-max text-center">
|
||||||
|
An email with instructions has been sent to {email}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user