Cleanup
This commit is contained in:
48
apps/web/app/auth/callback/error/page.tsx
Normal file
48
apps/web/app/auth/callback/error/page.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
interface Params {
|
||||
searchParams: {
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
|
||||
function AuthCallbackErrorPage({ searchParams }: Params) {
|
||||
const { error } = searchParams;
|
||||
|
||||
// if there is no error, redirect the user to the sign-in page
|
||||
if (!error) {
|
||||
redirect('/auth/sign-in');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4 py-4'}>
|
||||
<div>
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'auth:authenticationErrorAlertHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={error} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<ResendLinkForm />
|
||||
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<Button variant={'ghost'}>
|
||||
<a href={'/auth/sign-in'}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthCallbackErrorPage;
|
||||
143
apps/web/app/auth/callback/route.ts
Normal file
143
apps/web/app/auth/callback/route.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestUrl = new URL(request.url);
|
||||
const searchParams = requestUrl.searchParams;
|
||||
|
||||
const authCode = searchParams.get('code');
|
||||
const inviteCode = searchParams.get('inviteCode');
|
||||
const error = searchParams.get('error');
|
||||
const nextUrl = searchParams.get('next') ?? pathsConfig.app.home;
|
||||
|
||||
let userId: string | undefined = undefined;
|
||||
|
||||
if (authCode) {
|
||||
const client = getSupabaseRouteHandlerClient();
|
||||
|
||||
try {
|
||||
const { error, data } =
|
||||
await client.auth.exchangeCodeForSession(authCode);
|
||||
|
||||
// if we have an error, we redirect to the error page
|
||||
if (error) {
|
||||
return onError({ error: error.message });
|
||||
}
|
||||
|
||||
userId = data.user.id;
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{
|
||||
error,
|
||||
},
|
||||
`An error occurred while exchanging code for session`,
|
||||
);
|
||||
|
||||
const message = error instanceof Error ? error.message : error;
|
||||
|
||||
return onError({ error: message as string });
|
||||
}
|
||||
|
||||
if (inviteCode && userId) {
|
||||
try {
|
||||
Logger.info(
|
||||
{
|
||||
userId,
|
||||
inviteCode,
|
||||
},
|
||||
`Attempting to accept user invite...`,
|
||||
);
|
||||
|
||||
// if we have an invite code, we accept the invite
|
||||
await acceptInviteFromEmailLink({ inviteCode, userId });
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{
|
||||
userId,
|
||||
inviteCode,
|
||||
error,
|
||||
},
|
||||
`An error occurred while accepting user invite`,
|
||||
);
|
||||
|
||||
const message = error instanceof Error ? error.message : error;
|
||||
|
||||
return onError({ error: message as string });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return onError({ error });
|
||||
}
|
||||
|
||||
return redirect(nextUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name acceptInviteFromEmailLink
|
||||
* @description If we find an invite code, we try to accept the invite
|
||||
* received from the email link method
|
||||
* @param params
|
||||
*/
|
||||
async function acceptInviteFromEmailLink(params: {
|
||||
inviteCode: string;
|
||||
userId: string | undefined;
|
||||
}) {
|
||||
if (!params.userId) {
|
||||
Logger.error(params, `Attempted to accept invite, but no user id provided`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info(params, `Found invite code. Accepting invite...`);
|
||||
|
||||
await acceptInviteToOrganization(
|
||||
getSupabaseRouteHandlerClient({
|
||||
admin: true,
|
||||
}),
|
||||
{
|
||||
code: params.inviteCode,
|
||||
userId: params.userId,
|
||||
},
|
||||
);
|
||||
|
||||
Logger.info(params, `Invite successfully accepted`);
|
||||
}
|
||||
|
||||
function onError({ error }: { error: string }) {
|
||||
const errorMessage = getAuthErrorMessage(error);
|
||||
|
||||
Logger.error(
|
||||
{
|
||||
error,
|
||||
},
|
||||
`An error occurred while signing user in`,
|
||||
);
|
||||
|
||||
const redirectUrl = `/auth/callback/error?error=${errorMessage}`;
|
||||
|
||||
return redirect(redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given error message indicates a verifier error.
|
||||
* We check for this specific error because it's highly likely that the
|
||||
* user is trying to sign in using a different browser than the one they
|
||||
* used to request the sign in link. This is a common mistake, so we
|
||||
* want to provide a helpful error message.
|
||||
*/
|
||||
function isVerifierError(error: string) {
|
||||
return error.includes('both auth code and code verifier should be non-empty');
|
||||
}
|
||||
|
||||
function getAuthErrorMessage(error: string) {
|
||||
return isVerifierError(error)
|
||||
? `auth:errors.codeVerifierMismatch`
|
||||
: `auth:authenticationErrorAlertBody`;
|
||||
}
|
||||
9
apps/web/app/auth/layout.tsx
Normal file
9
apps/web/app/auth/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
|
||||
}
|
||||
|
||||
export default AuthLayout;
|
||||
3
apps/web/app/auth/loading.tsx
Normal file
3
apps/web/app/auth/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||
|
||||
export default GlobalLoader;
|
||||
44
apps/web/app/auth/password-reset/page.tsx
Normal file
44
apps/web/app/auth/password-reset/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: i18n.t('auth:passwordResetLabel'),
|
||||
};
|
||||
};
|
||||
|
||||
function PasswordResetPage() {
|
||||
return (
|
||||
<>
|
||||
<Heading level={5}>
|
||||
<Trans i18nKey={'auth:passwordResetLabel'} />
|
||||
</Heading>
|
||||
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<PasswordResetRequestContainer
|
||||
redirectTo={pathsConfig.auth.passwordUpdate}
|
||||
/>
|
||||
|
||||
<div className={'flex justify-center text-xs'}>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Button variant={'link'} size={'sm'}>
|
||||
<Trans i18nKey={'auth:passwordRecoveredQuestion'} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(PasswordResetPage);
|
||||
46
apps/web/app/auth/sign-in/page.tsx
Normal file
46
apps/web/app/auth/sign-in/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SignInMethodsContainer } from '@kit/auth/sign-in';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: i18n.t('auth:signIn'),
|
||||
};
|
||||
};
|
||||
|
||||
const paths = {
|
||||
callback: pathsConfig.auth.callback,
|
||||
home: pathsConfig.app.home,
|
||||
};
|
||||
|
||||
function SignInPage() {
|
||||
return (
|
||||
<>
|
||||
<Heading level={5}>
|
||||
<Trans i18nKey={'auth:signInHeading'} />
|
||||
</Heading>
|
||||
|
||||
<SignInMethodsContainer paths={paths} providers={authConfig.providers} />
|
||||
|
||||
<div className={'flex justify-center'}>
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Button variant={'link'} size={'sm'}>
|
||||
<Trans i18nKey={'auth:doNotHaveAccountYet'} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(SignInPage);
|
||||
44
apps/web/app/auth/sign-up/page.tsx
Normal file
44
apps/web/app/auth/sign-up/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: i18n.t('auth:signUp'),
|
||||
};
|
||||
};
|
||||
|
||||
function SignUpPage() {
|
||||
return (
|
||||
<>
|
||||
<Heading level={5}>
|
||||
<Trans i18nKey={'auth:signUpHeading'} />
|
||||
</Heading>
|
||||
|
||||
<SignUpMethodsContainer
|
||||
providers={authConfig.providers}
|
||||
callbackPath={pathsConfig.auth.callback}
|
||||
/>
|
||||
|
||||
<div className={'justify-centers flex'}>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Button variant={'link'} size={'sm'}>
|
||||
<Trans i18nKey={'auth:alreadyHaveAnAccount'} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(SignUpPage);
|
||||
36
apps/web/app/auth/verify/page.tsx
Normal file
36
apps/web/app/auth/verify/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
|
||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: i18n.t('auth:signIn'),
|
||||
};
|
||||
};
|
||||
|
||||
async function VerifyPage() {
|
||||
const client = getSupabaseServerComponentClient();
|
||||
const needsMfa = await checkRequiresMultiFactorAuthentication(client);
|
||||
|
||||
if (!needsMfa) {
|
||||
redirect(pathsConfig.auth.signIn);
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiFactorChallengeContainer
|
||||
onSuccess={() => {
|
||||
console.log('2');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(VerifyPage);
|
||||
Reference in New Issue
Block a user