mirror of https://github.com/boxyhq/jackson.git
Error pages for Jackson browser based flows (#134)
* Add cookie serialization/parse utils * Escape hatch for error page * Draft impl for error page * Error page redirect for /saml * Const for cookie name and delete cookie once read * Error page layout * Uncomment code * Render nothing if statusCode is null * Minor formatting fix * Remove cookie npm dependency * Remove types for cookie npm * Remove deleteCookie op * Simplify error page layout * Move hooks to lib/ui * Sort class name * Separate ui/server utils * Use setErrorCookie & getErrorCookie * updated package-lock.json Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
This commit is contained in:
parent
3313580ed8
commit
58b9769373
|
@ -4,17 +4,17 @@ import Link from 'next/link';
|
|||
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Logo from '../public/logo.png';
|
||||
import { LogoutIcon, MenuIcon, ShieldCheckIcon } from '@heroicons/react/outline';
|
||||
import useOnClickOutside from 'hooks/useOnClickOutside';
|
||||
import ActiveLink from './ActiveLink';
|
||||
import useKeyPress from 'hooks/useKeyPress';
|
||||
import useMediaQuery from 'hooks/useMediaQuery';
|
||||
import useOnClickOutside from '@lib/ui/hooks/useOnClickOutside';
|
||||
import useKeyPress from '@lib/ui/hooks/useKeyPress';
|
||||
import useMediaQuery from '@lib/ui/hooks/useMediaQuery';
|
||||
import { useSession, signOut } from 'next-auth/react';
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
path: '/admin/saml/config',
|
||||
text: <span className='ml-4'>SAML Configurations</span>,
|
||||
icon: <ShieldCheckIcon className='w-5 h-5' aria-hidden />,
|
||||
icon: <ShieldCheckIcon className='h-5 w-5' aria-hidden />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -67,10 +67,10 @@ function Layout({ children }: { children: ReactNode }) {
|
|||
</Head>
|
||||
<header
|
||||
role='banner'
|
||||
className='fixed left-0 right-0 z-10 p-5 bg-white border-b border-gray-900/10 dark:border-gray-300/10 dark:bg-gray-900 md:px-12'>
|
||||
className='fixed left-0 right-0 z-10 border-b border-gray-900/10 bg-white p-5 dark:border-gray-300/10 dark:bg-gray-900 md:px-12'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Link href='/'>
|
||||
<a title='Go to dashboard' className='flex items-center ml-10 font-bold leading-10 md:ml-0'>
|
||||
<a title='Go to dashboard' className='ml-10 flex items-center font-bold leading-10 md:ml-0'>
|
||||
<Image src={Logo} alt='BoxyHQ' layout='fixed' width={36} height={36} />
|
||||
<h1 className='ml-2 text-secondary hover:text-primary dark:text-white'>SAML Jackson</h1>
|
||||
</a>
|
||||
|
@ -78,7 +78,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||
<div className='relative'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center w-8 h-8 uppercase rounded-full bg-secondary text-cyan-50'
|
||||
className='flex h-8 w-8 items-center justify-center rounded-full bg-secondary uppercase text-cyan-50'
|
||||
aria-label='user settings'
|
||||
aria-expanded={isOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}>
|
||||
|
@ -86,14 +86,14 @@ function Layout({ children }: { children: ReactNode }) {
|
|||
</button>
|
||||
{isOpen && (
|
||||
<ul
|
||||
className='absolute right-0 z-50 py-1 overflow-hidden text-sm font-semibold bg-white rounded-lg shadow-lg dark:highlight-white/5 top-full w-36 text-slate-700 ring-1 ring-slate-900/10 dark:bg-slate-800 dark:text-slate-300 dark:ring-0'
|
||||
className='dark:highlight-white/5 absolute right-0 top-full z-50 w-36 overflow-hidden rounded-lg bg-white py-1 text-sm font-semibold text-slate-700 shadow-lg ring-1 ring-slate-900/10 dark:bg-slate-800 dark:text-slate-300 dark:ring-0'
|
||||
ref={userDropDownRef}>
|
||||
<li>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center w-full h-8 px-2 py-1 cursor-pointer'
|
||||
className='flex h-8 w-full cursor-pointer items-center justify-center px-2 py-1'
|
||||
onClick={() => signOut()}>
|
||||
<LogoutIcon className='w-5 h-5' aria-hidden />
|
||||
<LogoutIcon className='h-5 w-5' aria-hidden />
|
||||
Log out
|
||||
</button>
|
||||
</li>
|
||||
|
@ -114,7 +114,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||
aria-controls='menu'
|
||||
onClick={() => setIsSideNavOpen((curState) => !curState)}>
|
||||
<span className='sr-only'>Menu</span>
|
||||
<MenuIcon aria-hidden='true' className='w-6 h-6 text-black dark:text-slate-50'></MenuIcon>
|
||||
<MenuIcon aria-hidden='true' className='h-6 w-6 text-black dark:text-slate-50'></MenuIcon>
|
||||
</button>
|
||||
<ul
|
||||
className={`fixed top-0 bottom-0 left-0 w-60 border-r border-gray-900/10 p-6 transition-transform dark:border-gray-300/10 ${
|
||||
|
@ -140,9 +140,6 @@ function Layout({ children }: { children: ReactNode }) {
|
|||
className='relative top-[81px] h-[calc(100%_-_81px)] overflow-auto p-6 md:left-60 md:w-[calc(100%_-_theme(space.60))]'>
|
||||
{children}
|
||||
</main>
|
||||
{/* <footer role="contentinfo">
|
||||
<p>© 2022 BoxyHQ, Inc.</p>
|
||||
</footer> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// returns the cookie with the given name,
|
||||
// or undefined if not found
|
||||
export function getErrorCookie() {
|
||||
const matches = document.cookie.match(
|
||||
new RegExp('(?:^|; )' + 'jackson_error'.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1') + '=([^;]*)')
|
||||
);
|
||||
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||
}
|
||||
|
||||
export interface APIError extends Error {
|
||||
info?: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export const fetcher = async (url: string, queryParams = '') => {
|
||||
const res = await fetch(`${url}${queryParams}`);
|
||||
let resContent;
|
||||
try {
|
||||
resContent = await res.clone().json();
|
||||
} catch (e) {
|
||||
resContent = await res.clone().text();
|
||||
}
|
||||
if (!res.ok) {
|
||||
const error = new Error('An error occurred while fetching the data.') as APIError;
|
||||
// Attach extra info to the error object.
|
||||
error.info = resContent;
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return resContent;
|
||||
};
|
38
lib/utils.ts
38
lib/utils.ts
|
@ -1,4 +1,4 @@
|
|||
import { NextApiRequest } from 'next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import env from '@lib/env';
|
||||
import micromatch from 'micromatch';
|
||||
|
||||
|
@ -16,30 +16,6 @@ export const extractAuthToken = (req: NextApiRequest) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
export interface APIError extends Error {
|
||||
info?: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export const fetcher = async (url: string, queryParams = '') => {
|
||||
const res = await fetch(`${url}${queryParams}`);
|
||||
let resContent;
|
||||
try {
|
||||
resContent = await res.clone().json();
|
||||
} catch (e) {
|
||||
resContent = await res.clone().text();
|
||||
}
|
||||
if (!res.ok) {
|
||||
const error = new Error('An error occurred while fetching the data.') as APIError;
|
||||
// Attach extra info to the error object.
|
||||
error.info = resContent;
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return resContent;
|
||||
};
|
||||
|
||||
export const validateEmailWithACL = (email) => {
|
||||
const NEXTAUTH_ACL = process.env.NEXTAUTH_ACL || undefined;
|
||||
const acl = NEXTAUTH_ACL?.split(',');
|
||||
|
@ -51,3 +27,15 @@ export const validateEmailWithACL = (email) => {
|
|||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* This sets `cookie` using the `res` object
|
||||
*/
|
||||
export const setErrorCookie = (res: NextApiResponse, value: unknown, options: { path?: string } = {}) => {
|
||||
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
||||
let cookieContents = 'jackson_error' + '=' + stringValue;
|
||||
if (options.path) {
|
||||
cookieContents += '; Path=' + options.path;
|
||||
}
|
||||
res.setHeader('Set-Cookie', cookieContents);
|
||||
};
|
||||
|
|
|
@ -86,4 +86,4 @@
|
|||
"engines": {
|
||||
"node": ">=14.18.1 <=16.x"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import Layout from '@components/Layout';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
import '../styles/globals.css';
|
||||
|
||||
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
||||
const { pathname } = useRouter();
|
||||
|
||||
if (pathname === '/error') {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Layout>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NextPage } from 'next';
|
||||
import useSWR from 'swr';
|
||||
import { fetcher } from '@lib/utils';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import AddEdit from '@components/saml/AddEdit';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NextPage } from 'next';
|
||||
import useSWR from 'swr';
|
||||
import { fetcher } from '@lib/utils';
|
||||
import { fetcher } from '@lib/ui/utils';
|
||||
import Link from 'next/link';
|
||||
import { ArrowSmLeftIcon, ArrowSmRightIcon, PencilAltIcon } from '@heroicons/react/outline';
|
||||
import { useState } from 'react';
|
||||
|
|
|
@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||
|
||||
import jackson from '@lib/jackson';
|
||||
import { OAuthReqBody } from '@boxyhq/saml-jackson';
|
||||
import { setErrorCookie } from '@lib/utils';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
|
@ -22,7 +23,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
} catch (err: any) {
|
||||
console.error('authorize error:', err);
|
||||
const { message, statusCode = 500 } = err;
|
||||
|
||||
res.status(statusCode).send(message);
|
||||
// set error in cookie redirect to error page
|
||||
setErrorCookie(res, { message, statusCode }, { path: '/error' });
|
||||
res.redirect('/error');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import { setErrorCookie } from '@lib/utils';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
|
@ -15,7 +16,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
} catch (err: any) {
|
||||
console.error('callback error:', err);
|
||||
const { message, statusCode = 500 } = err;
|
||||
|
||||
res.status(statusCode).send(message);
|
||||
// set error in cookie redirect to error page
|
||||
setErrorCookie(res, { message, statusCode }, { path: '/error' });
|
||||
res.redirect('/error');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { getErrorCookie } from '@lib/ui/utils';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Error() {
|
||||
const [error, setError] = useState({ statusCode: null, message: '' });
|
||||
const { pathname } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const _error = getErrorCookie() || '';
|
||||
try {
|
||||
const { statusCode, message } = JSON.parse(_error);
|
||||
setError({ statusCode, message });
|
||||
} catch (err) {
|
||||
console.error('Unknown error format');
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
const { statusCode, message } = error;
|
||||
let statusText = '';
|
||||
if (typeof statusCode === 'number') {
|
||||
if (statusCode >= 400 && statusCode <= 499) {
|
||||
statusText = 'client-side error';
|
||||
}
|
||||
if (statusCode >= 500 && statusCode <= 599) {
|
||||
statusText = 'server error';
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full'>
|
||||
<div className='h-[20%] translate-y-[100%] px-[20%] text-[hsl(152,56%,40%)]'>
|
||||
<svg className='mb-5 h-10 w-10' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
d='M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
|
||||
/>
|
||||
</svg>
|
||||
<h1 className='text-xl font-extrabold md:text-6xl'>{error.statusCode}</h1>
|
||||
<h2 className='uppercase'>{statusText}</h2>
|
||||
<p className='mt-6 inline-block'>SAML error: </p>
|
||||
<p className='mr-2 text-xl font-bold'>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -18,7 +18,6 @@ module.exports = {
|
|||
primary: withOpacityValue('--color-primary'),
|
||||
secondary: withOpacityValue('--color-secondary'),
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue