developed initial version of new login page

This commit is contained in:
Sheen Capadngan
2023-04-27 23:10:27 +08:00
parent 2dd1570200
commit dfb84e9932
7 changed files with 239 additions and 44 deletions

View File

@@ -84,7 +84,7 @@ router.post(
);
router.get(
'/login/federated/google',
'/login/google',
authLimiter,
passport.authenticate('google', {
scope: ['profile', 'email'],

View File

@@ -47,7 +47,10 @@ export default function LoginStep ({
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin(email, password);
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful

View File

@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import SecurityClient from '../utilities/SecurityClient';
export default function PasswordInputStep({
userId,
email,
password,
setPassword,
setProviderAuthToken,
setStep
}: {
email: string;
userId: string;
password: string;
setPassword: (password: string) => void;
setProviderAuthToken: (value: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
if (!userId || !password) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin({
userId,
email,
password
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
};
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
<p className="mx-auto mb-6 flex w-max justify-center text-3xl font-semibold text-bunker-100">
{t('login:login')}
</p>
<div className="relative mt-6 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-2 md:max-h-28 md:p-2">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 cursor-pointer text-sm text-primary-700 duration-200 hover:text-primary">
<Link href="/verify-email">
<button
type="button"
className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
<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"
text={t('login:login') ?? ''}
onButtonPressed={async () => handleLogin()}
loading={isLoading}
size="lg"
/>
</div>
</div>
</div>
<div className="mt-4 flex flex-row items-center justify-center md:pb-4">
<p className="flex w-max justify-center text-sm text-gray-400">{t('login:need-account')}</p>
<Link href="/signup">
<button
type="button"
className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary"
>
{t('login:create-account')}
</button>
</Link>
</div>
<div className="flex flex-row items-center justify-center">
<button
onClick={(e) => {
e.preventDefault();
SecurityClient.setProviderAuthToken('');
setProviderAuthToken('');
}}
type="button"
className="ml-1.5 text-sm font-normal text-primary-700 underline-offset-4 duration-200 hover:text-primary"
>
Log in with other options.
</button>
</div>
</form>
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);

View File

@@ -26,20 +26,33 @@ interface IsLoginSuccessful {
* @param {string} password - password of user to log in
*/
const attemptLogin = async (
email: string,
password: string
{
email,
password,
userId,
}: {
email: string;
userId?: string;
password: string;
}
): Promise<IsLoginSuccessful> => {
const username = userId ?? email;
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
client.init(
{
username: email,
username,
password
},
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
const { serverPublicKey, salt } = await login1({
email,
clientPublicKey,
userId,
});
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
@@ -57,8 +70,11 @@ const attemptLogin = async (
iv,
tag
} = await login2(
email,
clientProof
{
email,
userId,
clientProof,
}
);
if (mfaEnabled) {
@@ -137,4 +153,4 @@ const attemptLogin = async (
});
};
export default attemptLogin;
export default attemptLogin;

View File

@@ -9,16 +9,17 @@ interface Login1 {
* @param {*} clientPublicKey
* @returns
*/
const login1 = async (email: string, clientPublicKey: string) => {
const login1 = async (loginDetails: {
email: string;
clientPublicKey: string;
userId?: string;
}) => {
const response = await fetch("/api/v2/auth/login1", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
clientPublicKey,
}),
body: JSON.stringify(loginDetails),
});
// need precise error handling about the status code
if (response?.status === 200) {

View File

@@ -17,16 +17,17 @@ interface Login2Response {
* @param {*} clientPublicKey
* @returns
*/
const login2 = async (email: string, clientProof: string) => {
const login2 = async (loginDetails: {
email: string;
clientProof: string;
userId?: string;
}) => {
const response = await fetch('/api/v2/auth/login2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
clientProof
}),
body: JSON.stringify(loginDetails),
credentials: 'include'
});
// need precise error handling about the status code

View File

@@ -8,7 +8,9 @@ import { useTranslation } from 'next-i18next';
import ListBox from '@app/components/basic/Listbox';
import LoginStep from '@app/components/login/LoginStep';
import MFAStep from '@app/components/login/MFAStep';
import PasswordInputStep from '@app/components/login/PasswordInputStep';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { useProviderAuth } from '@app/hooks/useProviderAuth';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
@@ -20,8 +22,14 @@ export default function Login() {
const [step, setStep] = useState(1);
const { t } = useTranslation();
const lang = router.locale ?? 'en';
const [isLoginWithEmail, setIsLoginWithEmail] = useState(false);
const {
providerAuthToken,
userId,
email: providerEmail,
setProviderAuthToken
} = useProviderAuth();
const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to });
localStorage.setItem('lang', to);
@@ -44,30 +52,58 @@ export default function Login() {
}
}, []);
const renderStep = (loginStep: number) => {
// TODO: add MFA step
switch (loginStep) {
case 1:
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
// TODO: add MFA step
return (
<MFAStep
email={email}
password={password}
/>
);
default:
return <div />
const renderView = (loginStep: number) => {
if (providerAuthToken && step === 1) {
return (
<PasswordInputStep
userId={userId}
email={providerEmail}
password={password}
setPassword={setPassword}
setProviderAuthToken={setProviderAuthToken}
setStep={setStep}
/>
)
}
if (isLoginWithEmail && loginStep === 1) {
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
)
}
if (!isLoginWithEmail && loginStep === 1) {
return (
<>
<button type='button' className='text-white' onClick={() => {
window.open('/api/v1/auth/login/google')
}}>
Continue with Google
</button>
<button type='button' className='text-white' onClick={() => {
setIsLoginWithEmail(true);
}}>
Continue with Email
</button>
</>
)
}
if (step === 2) {
<MFAStep
email={email || providerEmail}
password={password}
/>
}
return <div />
}
return (
@@ -84,7 +120,7 @@ export default function Login() {
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
</div>
</Link>
{renderStep(step)}
{renderView(step)}
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="w-48 mx-auto">
<ListBox