mirror of https://github.com/boxyhq/jackson.git
250 lines
7.8 KiB
TypeScript
250 lines
7.8 KiB
TypeScript
import { useState, type ReactElement } from 'react';
|
|
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
|
import { useRouter } from 'next/router';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { useSession, getCsrfToken, signIn, SessionProvider } from 'next-auth/react';
|
|
import { useTranslation, Trans } from 'next-i18next';
|
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|
import { errorToast, successToast } from '@components/Toaster';
|
|
import { ButtonOutline } from '@components/ButtonOutline';
|
|
import Loading from '@components/Loading';
|
|
import { Login as SSOLogin } from '@boxyhq/react-ui/sso';
|
|
import { adminPortalSSODefaults } from '@lib/env';
|
|
|
|
const Login = ({
|
|
csrfToken,
|
|
tenant,
|
|
product,
|
|
isEmailPasswordEnabled,
|
|
isMagicLinkEnabled,
|
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
|
const { t } = useTranslation('common');
|
|
const router = useRouter();
|
|
const { status } = useSession();
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [authMethod, setAuthMethod] = useState('credentials');
|
|
|
|
let { callbackUrl } = router.query as { callbackUrl: string };
|
|
|
|
callbackUrl = callbackUrl || '/admin/sso-connection';
|
|
|
|
if (status === 'loading') {
|
|
return <Loading />;
|
|
}
|
|
|
|
if (status === 'authenticated') {
|
|
router.push(callbackUrl);
|
|
return;
|
|
}
|
|
|
|
const onSSOSubmit = async ({ ssoIdentifier }) => {
|
|
await signIn('boxyhq-saml', undefined, { client_id: ssoIdentifier });
|
|
};
|
|
|
|
// Handle login with email and password
|
|
const onEmailPasswordLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
setLoading(true);
|
|
setAuthMethod('credentials');
|
|
|
|
const response = await signIn('credentials', {
|
|
email,
|
|
password,
|
|
redirect: false,
|
|
callbackUrl,
|
|
});
|
|
|
|
setLoading(false);
|
|
|
|
if (!response) {
|
|
return;
|
|
}
|
|
|
|
const { error } = response;
|
|
|
|
if (error) {
|
|
errorToast(error);
|
|
}
|
|
};
|
|
|
|
// Handle login with magic link
|
|
const onMagicLinkLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!email) {
|
|
errorToast(t('email_required'));
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setAuthMethod('email');
|
|
|
|
const response = await signIn('email', {
|
|
email,
|
|
csrfToken,
|
|
redirect: false,
|
|
});
|
|
|
|
setLoading(false);
|
|
|
|
if (!response) {
|
|
return;
|
|
}
|
|
|
|
const { error } = response;
|
|
|
|
if (error) {
|
|
errorToast(error);
|
|
} else {
|
|
successToast(t('login_success_toast'));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className='flex min-h-screen flex-col items-center justify-center'>
|
|
<div className='flex flex-col px-4 sm:mx-auto sm:w-full sm:max-w-[480px]'>
|
|
<div className='mt-4 border px-6 py-8 text-left shadow-md'>
|
|
<div className='space-y-3'>
|
|
<div className='flex justify-center'>
|
|
<Image src='/logo.png' alt='BoxyHQ logo' width={50} height={50} />
|
|
</div>
|
|
<h2 className='text-center text-3xl font-extrabold text-gray-900'>
|
|
{t('boxyhq_admin_portal')}
|
|
</h2>
|
|
<p className='text-center text-sm text-gray-600'>{t('boxyhq_tagline')}</p>
|
|
</div>
|
|
|
|
{(isEmailPasswordEnabled || isMagicLinkEnabled) && (
|
|
<form method='POST' onSubmit={onEmailPasswordLogin}>
|
|
<div className='mt-6'>
|
|
<div className='flex flex-col gap-3'>
|
|
<label className='block text-sm font-medium' htmlFor='email'>
|
|
{t('email')}
|
|
<label>
|
|
<input
|
|
type='email'
|
|
placeholder={t('email')}
|
|
className='input-bordered input mt-2 w-full rounded-md'
|
|
required
|
|
onChange={(e) => {
|
|
setEmail(e.target.value);
|
|
}}
|
|
value={email}
|
|
/>
|
|
</label>
|
|
</label>
|
|
{isEmailPasswordEnabled && (
|
|
<>
|
|
<label className='block text-sm font-medium' htmlFor='password'>
|
|
{t('password')}
|
|
<label>
|
|
<input
|
|
type='password'
|
|
placeholder={t('password')}
|
|
className='input-bordered input mt-2 w-full rounded-md'
|
|
required
|
|
onChange={(e) => {
|
|
setPassword(e.target.value);
|
|
}}
|
|
value={password}
|
|
/>
|
|
</label>
|
|
</label>
|
|
<ButtonOutline
|
|
loading={loading && authMethod === 'credentials'}
|
|
className='btn-block'
|
|
type='submit'>
|
|
{t('sign_in')}
|
|
</ButtonOutline>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* No login methods enabled */}
|
|
{!isEmailPasswordEnabled && !isMagicLinkEnabled && (
|
|
<div className='mt-10 text-center font-medium text-gray-600'>
|
|
<Trans
|
|
i18nKey='learn_to_enable_auth_methods'
|
|
t={t}
|
|
components={{
|
|
docLink: (
|
|
<a
|
|
href='https://boxyhq.com/docs/admin-portal/overview'
|
|
target='_blank'
|
|
rel='noopener noreferrer'
|
|
className='underline underline-offset-2'>
|
|
{t('documentation')}
|
|
</a>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className='mt-10 flex flex-col gap-3'>
|
|
{isMagicLinkEnabled && (
|
|
<ButtonOutline
|
|
loading={loading && authMethod === 'email'}
|
|
className='btn-block'
|
|
onClick={onMagicLinkLogin}
|
|
type='button'>
|
|
{t('send_magic_link')}
|
|
</ButtonOutline>
|
|
)}
|
|
<SSOLogin
|
|
buttonText={t('login_with_sso')}
|
|
ssoIdentifier={`tenant=${tenant}&product=${product}`}
|
|
onSubmit={onSSOSubmit}
|
|
classNames={{
|
|
button: 'btn-outline btn-block btn',
|
|
}}
|
|
innerProps={{
|
|
button: { 'data-testid': 'sso-login-button' },
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Link href='/.well-known' className='my-3 text-sm underline underline-offset-4' target='_blank'>
|
|
{t('bui-wku-heading')}
|
|
</Link>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
Login.getLayout = function getLayout(page: ReactElement) {
|
|
return <SessionProvider>{page}</SessionProvider>;
|
|
};
|
|
|
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
const { locale }: GetServerSidePropsContext = context;
|
|
|
|
const isMagicLinkEnabled = process.env.NEXTAUTH_ACL ? true : false;
|
|
const isEmailPasswordEnabled = process.env.NEXTAUTH_ADMIN_CREDENTIALS ? true : false;
|
|
|
|
const { tenant, product } = adminPortalSSODefaults;
|
|
|
|
return {
|
|
props: {
|
|
csrfToken: await getCsrfToken(context),
|
|
tenant,
|
|
product,
|
|
isMagicLinkEnabled,
|
|
isEmailPasswordEnabled,
|
|
...(locale ? await serverSideTranslations(locale, ['common']) : {}),
|
|
},
|
|
};
|
|
};
|
|
|
|
export default Login;
|