mirror of https://github.com/boxyhq/jackson.git
Add a new UI for the login screen (#647)
* Add a new UI for the login screen * Update the error page UI * text tweak, fixed active element in menu bar, updated logo to generic one * text tweaks * fixed unauth route, it needs to be the original one, not the redirected one * Display the list of well-known URLs on login screen * Display the well-known URLs on the dashboard * added description to links * tweak to login page Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
This commit is contained in:
parent
f95f8714ee
commit
031aac3e21
|
@ -1,4 +1,4 @@
|
||||||
import { ShieldCheckIcon, UsersIcon } from '@heroicons/react/20/solid';
|
import { ShieldCheckIcon, UsersIcon, HomeIcon } from '@heroicons/react/20/solid';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -12,11 +12,17 @@ export const Sidebar = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
const { asPath } = useRouter();
|
const { asPath } = useRouter();
|
||||||
|
|
||||||
const menus = [
|
const menus = [
|
||||||
|
{
|
||||||
|
href: '/admin/dashboard',
|
||||||
|
text: 'Dashboard',
|
||||||
|
icon: HomeIcon,
|
||||||
|
active: asPath.includes('/admin/dashboard'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/admin/connection',
|
href: '/admin/connection',
|
||||||
text: 'SSO Connections',
|
text: 'Enterprise SSO',
|
||||||
icon: ShieldCheckIcon,
|
icon: ShieldCheckIcon,
|
||||||
active: asPath.includes('/admin/saml'),
|
active: asPath.includes('/admin/connection'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/admin/directory-sync',
|
href: '/admin/directory-sync',
|
||||||
|
|
|
@ -268,14 +268,14 @@ const AddEdit = ({ connection }: AddEditProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link href='/admin/connection'>
|
<Link href='/admin/connection'>
|
||||||
<a className='btn btn-outline items-center space-x-2'>
|
<a className='btn-outline btn items-center space-x-2'>
|
||||||
<ArrowLeftIcon aria-hidden className='h-4 w-4' />
|
<ArrowLeftIcon aria-hidden className='h-4 w-4' />
|
||||||
<span>Back</span>
|
<span>Back</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
<h2 className='mb-5 mt-5 font-bold text-gray-700 dark:text-white md:text-xl'>
|
||||||
{isEditView ? 'Edit Connection' : 'Create Connection'}
|
{isEditView ? 'Edit SSO Connection' : 'Create SSO Connection'}
|
||||||
</h2>
|
</h2>
|
||||||
{!isEditView && (
|
{!isEditView && (
|
||||||
<div className='mb-4 flex'>
|
<div className='mb-4 flex'>
|
||||||
|
@ -388,7 +388,7 @@ const AddEdit = ({ connection }: AddEditProps) => {
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
onChange={getHandleChange()}
|
onChange={getHandleChange()}
|
||||||
className={`textarea textarea-bordered h-24 w-full ${
|
className={`textarea-bordered textarea h-24 w-full ${
|
||||||
isArray ? 'whitespace-pre' : ''
|
isArray ? 'whitespace-pre' : ''
|
||||||
}`}
|
}`}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
@ -408,7 +408,7 @@ const AddEdit = ({ connection }: AddEditProps) => {
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
onChange={getHandleChange({ key: 'checked' })}
|
onChange={getHandleChange({ key: 'checked' })}
|
||||||
className='checkbox checkbox-primary ml-5 align-middle'
|
className='checkbox-primary checkbox ml-5 align-middle'
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -421,7 +421,7 @@ const AddEdit = ({ connection }: AddEditProps) => {
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
onChange={getHandleChange()}
|
onChange={getHandleChange()}
|
||||||
className='input input-bordered w-full'
|
className='input-bordered input w-full'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -429,7 +429,7 @@ const AddEdit = ({ connection }: AddEditProps) => {
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<button type='submit' className='btn btn-primary'>
|
<button type='submit' className='btn-primary btn'>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
<p
|
<p
|
||||||
|
@ -461,7 +461,7 @@ const AddEdit = ({ connection }: AddEditProps) => {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-error'
|
className='btn-error btn'
|
||||||
onClick={toggleDelConfirm}
|
onClick={toggleDelConfirm}
|
||||||
data-modal-toggle='popup-modal'>
|
data-modal-toggle='popup-modal'>
|
||||||
Delete
|
Delete
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { ArrowRightOnRectangleIcon } from '@heroicons/react/20/solid';
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
title: 'SP Metadata',
|
||||||
|
description:
|
||||||
|
'The metadata file that your customers who use federated management systems like OpenAthens and Shibboleth will need to configure your service.',
|
||||||
|
href: '/.well-known/sp-metadata',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SAML Configuration',
|
||||||
|
description:
|
||||||
|
'The configuration setup guide that your customers will need to refer to when setting up SAML application with their Identity Provider.',
|
||||||
|
href: '/.well-known/saml-configuration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'OpenID Configuration',
|
||||||
|
description:
|
||||||
|
'Our OpenID configuration URI which your customers will need if they are connecting via OAuth 2.0 or Open ID Connect.',
|
||||||
|
href: '/.well-known/openid-configuration',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const WellKnownURLs = ({ className }: { className?: string }) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<p>Here are the set of URIs you might commonly need access to:</p>
|
||||||
|
<br />
|
||||||
|
<ul className='flex flex-col space-y-1'>
|
||||||
|
{links.map((link) => {
|
||||||
|
return (
|
||||||
|
<li key={link.href} className='text-sm'>
|
||||||
|
<p>{link.description}</p>
|
||||||
|
<a href={link.href} target='_blank' rel='noreferrer'>
|
||||||
|
<div className='link flex'>
|
||||||
|
<ArrowRightOnRectangleIcon className='mr-1 h-5 w-5' /> {link.title}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WellKnownURLs;
|
|
@ -17,7 +17,7 @@ export const AccountLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>SAML Jackson - BoxyHQ</title>
|
<title>Admin UI | BoxyHQ</title>
|
||||||
<link rel='icon' href='/favicon.ico' />
|
<link rel='icon' href='/favicon.ico' />
|
||||||
</Head>
|
</Head>
|
||||||
<Sidebar isOpen={isOpen} setIsOpen={setIsOpen} />
|
<Sidebar isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
|
|
14
lib/utils.ts
14
lib/utils.ts
|
@ -1,16 +1,16 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import micromatch from 'micromatch';
|
import micromatch from 'micromatch';
|
||||||
|
|
||||||
export const validateEmailWithACL = (email) => {
|
export const validateEmailWithACL = (email: string) => {
|
||||||
const NEXTAUTH_ACL = process.env.NEXTAUTH_ACL || undefined;
|
const NEXTAUTH_ACL = process.env.NEXTAUTH_ACL || undefined;
|
||||||
const acl = NEXTAUTH_ACL?.split(',');
|
|
||||||
|
|
||||||
if (acl) {
|
if (!NEXTAUTH_ACL) {
|
||||||
if (micromatch.isMatch(email, acl)) {
|
return false;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
const acl = NEXTAUTH_ACL.split(',');
|
||||||
|
|
||||||
|
return micromatch.isMatch(email, acl);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,33 +1,65 @@
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import type { Session } from 'next-auth';
|
import type { Session } from 'next-auth';
|
||||||
|
import type { NextPage } from 'next';
|
||||||
import { SessionProvider } from 'next-auth/react';
|
import { SessionProvider } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
|
import { ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
import { AccountLayout } from '@components/layouts';
|
import { AccountLayout } from '@components/layouts';
|
||||||
|
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
|
|
||||||
function MyApp({
|
const unauthenticatedRoutes = [
|
||||||
Component,
|
'/',
|
||||||
pageProps: { session, ...pageProps },
|
'/admin/auth/login',
|
||||||
}: AppProps<{
|
'/well-known/saml-configuration',
|
||||||
session: Session;
|
'/oauth/jwks',
|
||||||
}>) {
|
'/idp/select',
|
||||||
|
'/error',
|
||||||
|
];
|
||||||
|
|
||||||
|
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||||
const { pathname } = useRouter();
|
const { pathname } = useRouter();
|
||||||
|
|
||||||
if (pathname !== '/' && !pathname.startsWith('/admin')) {
|
const { session, ...props } = pageProps;
|
||||||
return <Component {...pageProps} />;
|
|
||||||
|
const getLayout = Component.getLayout;
|
||||||
|
|
||||||
|
// If a page level layout exists, use it
|
||||||
|
if (getLayout) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{getLayout(<Component {...props} />)}
|
||||||
|
<Toaster toastOptions={{ duration: 5000 }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unauthenticatedRoutes.includes(pathname)) {
|
||||||
|
return <Component {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<AccountLayout>
|
<AccountLayout>
|
||||||
<Component {...pageProps} />
|
<Component {...props} />
|
||||||
<Toaster />
|
<Toaster toastOptions={{ duration: 5000 }} />
|
||||||
</AccountLayout>
|
</AccountLayout>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default appWithTranslation<never>(MyApp);
|
export default appWithTranslation<never>(MyApp);
|
||||||
|
|
||||||
|
export type AppPropsWithLayout = AppProps & {
|
||||||
|
Component: NextPageWithLayout;
|
||||||
|
pageProps: {
|
||||||
|
session?: Session;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NextPageWithLayout<P = Record<string, unknown>> = NextPage<P> & {
|
||||||
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||||
|
import { useSession, getCsrfToken, signIn } from 'next-auth/react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { SessionProvider } from 'next-auth/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import WellKnownURLs from '@components/connection/WellKnownURLs';
|
||||||
|
|
||||||
|
const Login = ({ csrfToken }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { status } = useSession();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
if (status === 'authenticated') {
|
||||||
|
router.push('/admin/connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const response = await signIn('email', {
|
||||||
|
email,
|
||||||
|
csrfToken,
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = response;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('A sign in link has been sent to your email address.');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex min-h-screen flex-col items-center justify-center'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='mt-4 border p-6 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'>BoxyHQ Admin UI</h2>
|
||||||
|
<p className='text-center text-sm text-gray-600'>
|
||||||
|
Enterprise readiness for B2B SaaS, straight out of the box.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className='mt-8'>
|
||||||
|
<div>
|
||||||
|
<label className='block' htmlFor='email'>
|
||||||
|
Email
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
placeholder='Email'
|
||||||
|
className='input-bordered input mb-5 mt-2 w-full rounded-md'
|
||||||
|
required
|
||||||
|
onChange={(e) => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
}}
|
||||||
|
value={email}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-baseline justify-between'>
|
||||||
|
<button
|
||||||
|
className={classNames('btn-primary btn-block btn rounded-md', loading ? 'loading' : '')}
|
||||||
|
type='submit'>
|
||||||
|
Send Magic Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WellKnownURLs className='mt-5 border p-5' />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Login.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <SessionProvider>{page}</SessionProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
csrfToken: await getCsrfToken(context),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
|
@ -31,9 +31,9 @@ const Connections: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='mb-5 flex items-center justify-between'>
|
<div className='mb-5 flex items-center justify-between'>
|
||||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>Connections</h2>
|
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>Enterprise SSO</h2>
|
||||||
<Link href={`/admin/connection/new`}>
|
<Link href={`/admin/connection/new`}>
|
||||||
<a className='btn btn-primary' data-test-id='create-connection'>
|
<a className='btn-primary btn' data-test-id='create-connection'>
|
||||||
+ New Connection
|
+ New Connection
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -95,7 +95,7 @@ const Connections: NextPage = () => {
|
||||||
<div className='mt-4 flex justify-center'>
|
<div className='mt-4 flex justify-center'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-outline'
|
className='btn-outline btn'
|
||||||
disabled={paginate.page === 0}
|
disabled={paginate.page === 0}
|
||||||
aria-label='Previous'
|
aria-label='Previous'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -111,7 +111,7 @@ const Connections: NextPage = () => {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-outline'
|
className='btn-outline btn'
|
||||||
disabled={connections.length === 0 || connections.length < paginate.pageLimit}
|
disabled={connections.length === 0 || connections.length < paginate.pageLimit}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPaginate((curState) => ({
|
setPaginate((curState) => ({
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { NextPage } from 'next';
|
||||||
|
|
||||||
|
import WellKnownURLs from '@components/connection/WellKnownURLs';
|
||||||
|
|
||||||
|
const Dashboard: NextPage = () => {
|
||||||
|
return <WellKnownURLs />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
|
@ -38,16 +38,18 @@ export default NextAuth({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async signIn({ user }): Promise<boolean> {
|
async signIn({ user }): Promise<boolean> {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const email = user.email;
|
|
||||||
return validateEmailWithACL(email);
|
return validateEmailWithACL(user.email);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/admin/auth/login',
|
||||||
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
adapter: Adapter(),
|
adapter: Adapter(),
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default function Error() {
|
||||||
let statusText = '';
|
let statusText = '';
|
||||||
if (typeof statusCode === 'number') {
|
if (typeof statusCode === 'number') {
|
||||||
if (statusCode >= 400 && statusCode <= 499) {
|
if (statusCode >= 400 && statusCode <= 499) {
|
||||||
statusText = 'client-side error';
|
statusText = 'client error';
|
||||||
}
|
}
|
||||||
if (statusCode >= 500 && statusCode <= 599) {
|
if (statusCode >= 500 && statusCode <= 599) {
|
||||||
statusText = 'server error';
|
statusText = 'server error';
|
||||||
|
@ -32,19 +32,21 @@ export default function Error() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full'>
|
<div className='flex h-screen'>
|
||||||
<div className='h-[20%] translate-y-[100%] px-[20%] text-[hsl(152,56%,40%)]'>
|
<div className='m-auto'>
|
||||||
<svg className='mb-5 h-10 w-10' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
|
<section className='bg-white dark:bg-gray-900'>
|
||||||
<path
|
<div className='mx-auto max-w-screen-xl py-8 px-4 lg:py-16 lg:px-6'>
|
||||||
strokeLinecap='round'
|
<div className='mx-auto max-w-screen-sm text-center'>
|
||||||
strokeLinejoin='round'
|
<h1 className='mb-4 text-7xl font-extrabold tracking-tight text-primary lg:text-9xl'>
|
||||||
d='M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
|
{error.statusCode}
|
||||||
/>
|
</h1>
|
||||||
</svg>
|
<p className='mb-4 text-3xl font-bold tracking-tight text-gray-900 dark:text-white md:text-4xl'>
|
||||||
<h1 className='text-xl font-extrabold md:text-6xl'>{error.statusCode}</h1>
|
{statusText}
|
||||||
<h2 className='uppercase'>{statusText}</h2>
|
</p>
|
||||||
<p className='mt-6 inline-block'>SAML error: </p>
|
<p className='mb-4 text-lg font-light'>SAML error: {message}</p>
|
||||||
<p className='mr-2 text-xl font-bold'>{message}</p>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 22 KiB |
Loading…
Reference in New Issue