build ok,

This commit is contained in:
louiscklaw
2025-04-14 09:26:24 +08:00
commit 6c931c1fe8
770 changed files with 63959 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import type { NextRequest } from 'next/server';
import type { AppRouteHandlerFnContext } from '@auth0/nextjs-auth0';
import { auth0 } from '@/lib/auth/auth0/server';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest, ctx: AppRouteHandlerFnContext): Promise<Response> {
return auth0.handleCallback(req, ctx);
}

View File

@@ -0,0 +1,10 @@
import type { NextRequest } from 'next/server';
import type { AppRouteHandlerFnContext } from '@auth0/nextjs-auth0';
import { auth0 } from '@/lib/auth/auth0/server';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest, ctx: AppRouteHandlerFnContext): Promise<Response> {
return auth0.handleProfile(req, ctx, {});
}

View File

@@ -0,0 +1,15 @@
import type { NextRequest } from 'next/server';
import type { AppRouteHandlerFnContext } from '@auth0/nextjs-auth0';
import { config } from '@/config';
import { paths } from '@/paths';
import { auth0 } from '@/lib/auth/auth0/server';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest, ctx: AppRouteHandlerFnContext): Promise<Response> {
return auth0.handleLogin(req, ctx, {
authorizationParams: { redirect_uri: new URL(paths.auth.auth0.callback, config.auth0.baseUrl).toString() },
returnTo: new URL(paths.dashboard.overview, config.auth0.baseUrl).toString(),
});
}

View File

@@ -0,0 +1,10 @@
import type { NextRequest } from 'next/server';
import type { AppRouteHandlerFnContext } from '@auth0/nextjs-auth0';
import { auth0 } from '@/lib/auth/auth0/server';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest, ctx: AppRouteHandlerFnContext): Promise<Response> {
return auth0.handleLogout(req, ctx);
}

View File

@@ -0,0 +1,13 @@
import type { NextRequest } from 'next/server';
import type { AppRouteHandlerFnContext } from '@auth0/nextjs-auth0';
import { config } from '@/config';
import { auth0 } from '@/lib/auth/auth0/server';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest, ctx: AppRouteHandlerFnContext): Promise<Response> {
return auth0.handleLogin(req, ctx, {
authorizationParams: { redirect_uri: `${config.auth0.baseUrl}/auth/auth0/callback`, login_hint: 'signup' },
});
}

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import { AuthStrategy } from '@/lib/auth/strategy';
import { StrategyGuard } from '@/components/auth/strategy-guard';
// We are not adding the auth check because there might be individual pages that require the user to be authenticated.
// Another reason is that layouts get cached and loaded only once for all children.
interface LayoutProps {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps): React.JSX.Element {
return <StrategyGuard expected={AuthStrategy.COGNITO}>{children}</StrategyGuard>;
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { NewPasswordRequiredForm } from '@/components/auth/cognito/new-password-required-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `New password required | Cognito | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<NewPasswordRequiredForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { ResetPasswordForm } from '@/components/auth/cognito/reset-password-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Reset password | Cognito | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<ResetPasswordForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { SignInForm } from '@/components/auth/cognito/sign-in-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign in | Cognito | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignInForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import type { Metadata } from 'next';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import { config } from '@/config';
import { SignUpConfirmForm } from '@/components/auth/cognito/sign-up-confirm-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign up confirm | Cognito | Auth | ${config.site.name}` };
interface PageProps {
searchParams: { email?: string };
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email } = searchParams;
if (!email) {
return (
<Box sx={{ p: 3 }}>
<Alert color="error">Email is required</Alert>
</Box>
);
}
return (
<GuestGuard>
<SplitLayout>
<SignUpConfirmForm email={email} />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { SignUpForm } from '@/components/auth/cognito/sign-up-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign up | Cognito | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignUpForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import type { Metadata } from 'next';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import { config } from '@/config';
import { UpdatePasswordForm } from '@/components/auth/cognito/update-password-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Update password | Cognito | Auth | ${config.site.name}` };
interface PageProps {
searchParams: { email?: string };
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email } = searchParams;
if (!email) {
return (
<Box sx={{ p: 3 }}>
<Alert color="error">Email is required</Alert>
</Box>
);
}
return (
<GuestGuard>
<SplitLayout>
<UpdatePasswordForm email={email} />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import { AuthStrategy } from '@/lib/auth/strategy';
import { StrategyGuard } from '@/components/auth/strategy-guard';
// We are not adding the auth check because there might be individual pages that require the user to be authenticated.
// Another reason is that layouts get cached and loaded only once for all children.
interface LayoutProps {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps): React.JSX.Element {
return <StrategyGuard expected={AuthStrategy.CUSTOM}>{children}</StrategyGuard>;
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { ResetPasswordForm } from '@/components/auth/custom/reset-password-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Reset password | Custom | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<ResetPasswordForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { SignInForm } from '@/components/auth/custom/sign-in-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign in | Custom | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignInForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { SignUpForm } from '@/components/auth/custom/sign-up-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign up | Custom | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignUpForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import { AuthStrategy } from '@/lib/auth/strategy';
import { StrategyGuard } from '@/components/auth/strategy-guard';
// We are not adding the auth check because there might be individual pages that require the user to be authenticated.
// Another reason is that layouts get cached and loaded only once for all children.
interface LayoutProps {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps): React.JSX.Element {
return <StrategyGuard expected={AuthStrategy.FIREBASE}>{children}</StrategyGuard>;
}

View File

@@ -0,0 +1,63 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { config } from '@/config';
import { paths } from '@/paths';
import { ResetPasswordButton } from '@/components/auth/firebase/reset-password-button';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Recovery link sent | Firebase | Auth | ${config.site.name}` };
interface PageProps {
searchParams: { email?: string };
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email } = searchParams;
if (!email) {
return (
<Box sx={{ p: 3 }}>
<Alert color="error">Email is required</Alert>
</Box>
);
}
return (
<GuestGuard>
<SplitLayout>
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Recovery link sent</Typography>
<Stack spacing={1}>
<Typography>
If an account exists with email{' '}
<Typography component="span" variant="subtitle1">
&quot;{email}&quot;
</Typography>
, you will receive a recovery email.
</Typography>
<div>
<Link component={RouterLink} href={paths.auth.firebase.resetPassword} variant="subtitle2">
Use another email
</Link>
</div>
</Stack>
<ResetPasswordButton email={email}>Resend</ResetPasswordButton>
</Stack>
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { ResetPasswordForm } from '@/components/auth/firebase/reset-password-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Reset password | Firebase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<ResetPasswordForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { SignInForm } from '@/components/auth/firebase/sign-in-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign in | Firebase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignInForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { SignUpForm } from '@/components/auth/firebase/sign-up-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Sign up | Firebase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignUpForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import type { Metadata } from 'next';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import { config } from '@/config';
import { UpdatePasswordForm } from '@/components/auth/firebase/update-password-form';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
export const metadata: Metadata = { title: `Update password | Firebase | Auth | ${config.site.name}` };
interface PageProps {
searchParams: { oobCode?: string };
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { oobCode } = searchParams;
if (!oobCode) {
return (
<Box sx={{ p: 3 }}>
<Alert color="error">Code is required</Alert>
</Box>
);
}
return (
<GuestGuard>
<SplitLayout>
<UpdatePasswordForm oobCode={oobCode} />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { CenteredLayout } from '@/components/auth/centered-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Reset password | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<CenteredLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Card>
<CardHeader title="Reset password" />
<CardContent>
<Stack spacing={2}>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" />
</FormControl>
<Button type="submit" variant="contained">
Send recovery link
</Button>
</Stack>
</CardContent>
</Card>
</Stack>
</CenteredLayout>
);
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { SplitLayout } from '@/components/auth/split-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Reset password | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<SplitLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Reset Password</Typography>
<Stack spacing={2}>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" />
</FormControl>
<Button type="submit" variant="contained">
Send recovery link
</Button>
</Stack>
</Stack>
</SplitLayout>
);
}

View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { CenteredLayout } from '@/components/auth/centered-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Sign in | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<CenteredLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Card>
<CardHeader
subheader={
<Typography color="text.secondary" variant="body2">
Don&apos;t have an account? <Link variant="subtitle2">Sign up</Link>
</Typography>
}
title="Sign in"
/>
<CardContent>
<Stack spacing={2}>
<Stack spacing={2}>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" />
</FormControl>
<FormControl>
<InputLabel>Password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<Button type="submit" variant="contained">
Sign in
</Button>
</Stack>
<div>
<Link variant="subtitle2">Forgot password?</Link>
</div>
</Stack>
</CardContent>
</Card>
</Stack>
</CenteredLayout>
);
}

View File

@@ -0,0 +1,69 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { SplitLayout } from '@/components/auth/split-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Sign in | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<SplitLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Stack spacing={1}>
<Typography variant="h5">Sign in</Typography>
<Typography color="text.secondary" variant="body2">
Don&apos;t have an account? <Link variant="subtitle2">Sign up</Link>
</Typography>
</Stack>
<Stack spacing={2}>
<Stack spacing={2}>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" />
</FormControl>
<FormControl>
<InputLabel>Password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<Button type="submit" variant="contained">
Sign in
</Button>
</Stack>
<div>
<Link variant="subtitle2">Forgot password?</Link>
</div>
</Stack>
</Stack>
</SplitLayout>
);
}

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { CenteredLayout } from '@/components/auth/centered-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Sign up | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<CenteredLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Card>
<CardHeader
subheader={
<Typography color="text.secondary" variant="body2">
Already have an account? <Link variant="subtitle2">Sign in</Link>
</Typography>
}
title="Sign up"
/>
<CardContent>
<Stack spacing={2}>
<FormControl>
<InputLabel>Name</InputLabel>
<OutlinedInput name="name" />
</FormControl>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" />
</FormControl>
<FormControl>
<InputLabel>Password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<div>
<FormControlLabel
control={<Checkbox name="terms" />}
label={
<React.Fragment>
I have read the <Link>terms and conditions</Link>
</React.Fragment>
}
/>
</div>
<Button type="submit" variant="contained">
Create account
</Button>
</Stack>
</CardContent>
</Card>
</Stack>
</CenteredLayout>
);
}

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { SplitLayout } from '@/components/auth/split-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Sign up | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<SplitLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Stack spacing={1}>
<Typography variant="h5">Sign up</Typography>
<Typography color="text.secondary" variant="body2">
Already have an account? <Link variant="subtitle2">Sign in</Link>
</Typography>
</Stack>
<Stack spacing={2}>
<FormControl>
<InputLabel>Name</InputLabel>
<OutlinedInput name="name" />
</FormControl>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" />
</FormControl>
<FormControl>
<InputLabel>Password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<div>
<FormControlLabel
control={<Checkbox />}
label={
<React.Fragment>
I have read the <Link>terms and conditions</Link>
</React.Fragment>
}
/>
</div>
<Button type="submit" variant="contained">
Create account
</Button>
</Stack>
</Stack>
</SplitLayout>
);
}

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { CenteredLayout } from '@/components/auth/centered-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Update password | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<CenteredLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Card>
<CardHeader title="Update password" />
<CardContent>
<Stack spacing={2}>
<FormControl>
<InputLabel>Password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<FormControl>
<InputLabel>Confirm password</InputLabel>
<OutlinedInput name="confirmPassword" type="password" />
</FormControl>
<Button type="submit" variant="contained">
Update
</Button>
</Stack>
</CardContent>
</Card>
</Stack>
</CenteredLayout>
);
}

View File

@@ -0,0 +1,59 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { SplitLayout } from '@/components/auth/split-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Update password | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<SplitLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Update Password</Typography>
<Stack spacing={2}>
<FormControl>
<InputLabel>Password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<FormControl>
<InputLabel>Confirm password</InputLabel>
<OutlinedInput name="confirmPassword" type="password" />
</FormControl>
<Button type="submit" variant="contained">
Update
</Button>
</Stack>
</Stack>
</SplitLayout>
);
}

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { CenteredLayout } from '@/components/auth/centered-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Verify code | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<CenteredLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Card>
<CardHeader title="Verify code" />
<CardContent>
<Stack spacing={2}>
<FormControl>
<InputLabel>Code</InputLabel>
<OutlinedInput name="code" />
</FormControl>
<Button type="submit" variant="contained">
Verify
</Button>
</Stack>
</CardContent>
</Card>
</Stack>
</CenteredLayout>
);
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { config } from '@/config';
import { paths } from '@/paths';
import { SplitLayout } from '@/components/auth/split-layout';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Verify code | Samples | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<SplitLayout>
<Stack spacing={4}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.overview}
sx={{ alignItems: 'center', display: 'inline-flex', gap: 1 }}
variant="subtitle2"
>
<ArrowLeftIcon fontSize="var(--icon-fontSize-md)" />
Dashboard
</Link>
</div>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Verify code</Typography>
<Stack spacing={2}>
<FormControl>
<InputLabel>Code</InputLabel>
<OutlinedInput name="code" />
</FormControl>
<Button type="submit" variant="contained">
Verify
</Button>
</Stack>
</Stack>
</SplitLayout>
);
}

View File

@@ -0,0 +1,75 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import Alert from '@mui/material/Alert';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { createClient as createSupabaseClient } from '@/lib/supabase/client';
import { toast } from '@/components/core/toaster';
// NOTE: This is a `Page` and not a `GET` route because
// Supabase has endpoints that still use
// Implicit Flow instead of PKCE Flow, such as `resend sign-up email` process.
export default function Page(): React.JSX.Element | null {
const [supabaseClient] = React.useState(createSupabaseClient());
const router = useRouter();
const executedRef = React.useRef<boolean>(false);
const [displayError, setDisplayError] = React.useState<string | null>(null);
const handle = React.useCallback(async (): Promise<void> => {
// Prevent rerun on DEV mode
if (executedRef.current) {
return;
}
executedRef.current = true;
// Callback `error` is received as a URL hash `#error=value`
// Callback `access_token` is received as a URL hash `#access_token=value`
const hash = window.location.hash || '#';
const hashParams = new URLSearchParams(hash.split('#')[1]);
const searchParams = new URLSearchParams(window.location.search);
if (hashParams.get('error')) {
logger.debug(hashParams.get('error_description'));
setDisplayError('Something went wrong');
return;
}
const accessToken = hashParams.get('access_token');
const refreshToken = hashParams.get('refresh_token');
if (!accessToken || !refreshToken) {
setDisplayError('Access token or refresh token is missing');
return;
}
const { error } = await supabaseClient.auth.setSession({ access_token: accessToken, refresh_token: refreshToken });
if (error) {
logger.debug(error.message);
toast.error('Something went wrong');
router.push(paths.auth.supabase.signIn);
return;
}
const next = searchParams.get('next') || paths.dashboard.overview;
router.push(next);
}, [supabaseClient, router]);
React.useEffect((): void => {
handle().catch(logger.error);
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
}, []);
if (displayError) {
return <Alert color="error">{displayError}</Alert>;
}
return null;
}

View File

@@ -0,0 +1,53 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { AuthApiError } from '@supabase/supabase-js';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { createClient } from '@/lib/supabase/server';
export const dynamic = 'force-dynamic';
// NOTE: If you have a proxy in front of this app
// the request origin might be a local address.
// Consider using `config.site.url` from `@/config` instead.
// NOTE: This is not a `Page` because we only redirect and it will never render React content.
export async function GET(req: NextRequest): Promise<NextResponse> {
const { searchParams, origin } = req.nextUrl;
if (searchParams.get('error')) {
return NextResponse.json({ error: searchParams.get('error_description') || 'Something went wrong' });
}
const code = searchParams.get('code');
if (!code) {
return NextResponse.json({ error: 'Code is missing' });
}
const cookieStore = cookies();
const supabaseClient = createClient(cookieStore);
try {
const { error } = await supabaseClient.auth.exchangeCodeForSession(code);
if (error) {
return NextResponse.json({ error: error.message });
}
} catch (err) {
if (err instanceof AuthApiError && err.message.includes('code and code verifier should be non-empty')) {
return NextResponse.json({ error: 'Please open the link in the same browser' });
}
logger.error('Callback error', err);
return NextResponse.json({ error: 'Something went wrong' });
}
const next = searchParams.get('next') || paths.home;
return NextResponse.redirect(new URL(next, origin));
}

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import { AuthStrategy } from '@/lib/auth/strategy';
import { StrategyGuard } from '@/components/auth/strategy-guard';
// We are not adding the auth check because there might be individual pages that require the user to be authenticated.
// Another reason is that layouts get cached and loaded only once for all children.
interface LayoutProps {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps): React.JSX.Element {
return <StrategyGuard expected={AuthStrategy.SUPABASE}>{children}</StrategyGuard>;
}

View File

@@ -0,0 +1,63 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { config } from '@/config';
import { paths } from '@/paths';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { ResetPasswordButton } from '@/components/auth/supabase/reset-password-button';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Recovery link sent | Supabase | Auth | ${config.site.name}` };
interface PageProps {
searchParams: { email?: string };
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email } = searchParams;
if (!email) {
return (
<Box sx={{ p: 3 }}>
<Alert color="error">Email is required</Alert>
</Box>
);
}
return (
<GuestGuard>
<SplitLayout>
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Recovery link sent</Typography>
<Stack spacing={1}>
<Typography>
If an account exists with email{' '}
<Typography component="span" variant="subtitle1">
&quot;{email}&quot;
</Typography>
, you will receive a recovery email.
</Typography>
<div>
<Link component={RouterLink} href={paths.auth.supabase.resetPassword} variant="subtitle2">
Use another email
</Link>
</div>
</Stack>
<ResetPasswordButton email={email}>Resend</ResetPasswordButton>
</Stack>
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { config } from '@/config';
import { paths } from '@/paths';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { ResetPasswordForm } from '@/components/auth/supabase/reset-password-form';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Reset password | Supabase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Reset password</Typography>
<ResetPasswordForm />
</Stack>
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { SignInForm } from '@/components/auth/supabase/sign-in-form';
export const metadata: Metadata = { title: `Sign in | Supabase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignInForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import type { Metadata } from 'next';
import RouterLink from 'next/link';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { config } from '@/config';
import { paths } from '@/paths';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { SignUpResendButton } from '@/components/auth/supabase/sign-up-resend-button';
import { DynamicLogo } from '@/components/core/logo';
export const metadata: Metadata = { title: `Sign up confirm | Supabase | Auth | ${config.site.name}` };
interface PageProps {
searchParams: { email?: string };
}
export default function Page({ searchParams }: PageProps): React.JSX.Element {
const { email } = searchParams;
if (!email) {
return (
<Box sx={{ p: 3 }}>
<Alert color="error">Email is required</Alert>
</Box>
);
}
return (
<GuestGuard>
<SplitLayout>
<Stack spacing={4}>
<div>
<Box component={RouterLink} href={paths.home} sx={{ display: 'inline-block', fontSize: 0 }}>
<DynamicLogo colorDark="light" colorLight="dark" height={32} width={122} />
</Box>
</div>
<Typography variant="h5">Confirm your email</Typography>
<Typography>
We&apos;ve sent a verification email to{' '}
<Typography component="span" variant="subtitle1">
&quot;{email}&quot;
</Typography>
.
</Typography>
<SignUpResendButton email={email}>Resend</SignUpResendButton>
</Stack>
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { GuestGuard } from '@/components/auth/guest-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { SignUpForm } from '@/components/auth/supabase/sign-up-form';
export const metadata: Metadata = { title: `Sign up | Supabase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<GuestGuard>
<SplitLayout>
<SignUpForm />
</SplitLayout>
</GuestGuard>
);
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
import type { Metadata } from 'next';
import { config } from '@/config';
import { AuthGuard } from '@/components/auth/auth-guard';
import { SplitLayout } from '@/components/auth/split-layout';
import { UpdatePasswordForm } from '@/components/auth/supabase/update-password-form';
export const metadata: Metadata = { title: `Update password | Supabase | Auth | ${config.site.name}` };
export default function Page(): React.JSX.Element {
return (
<AuthGuard>
<SplitLayout>
<UpdatePasswordForm />
</SplitLayout>
</AuthGuard>
);
}