Switch to email-based login

This commit is contained in:
Sam Becker 2023-09-06 15:03:59 -05:00
parent 0ce0cceb5b
commit e9db8b7a7a
9 changed files with 121 additions and 63 deletions

View File

@ -1,21 +1,21 @@
import { auth } from '@/auth';
import LoginButton from '@/components/LoginButton';
import SignInForm from '@/auth/SignInForm';
import { cc } from '@/utility/css';
import Link from 'next/link';
import { redirect } from 'next/navigation';
export default async function SignInPage() {
const session = await auth();
if (session?.user) {
redirect('/');
}
return (
<div className={cc(
'fixed top-0 left-0 right-0 bottom-0',
'flex items-center justify-center flex-col gap-8',
)}>
<LoginButton />
<Link href="/">Home</Link>
<SignInForm />
</div>
);
}

View File

@ -1,40 +0,0 @@
import NextAuth, { type DefaultSession } from 'next-auth';
import GitHub from 'next-auth/providers/github';
declare module 'next-auth' {
interface Session {
user: {
id: string
} & DefaultSession['user']
}
}
export const {
handlers: { GET, POST },
auth,
} = NextAuth({
providers: [GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
})],
callbacks: {
jwt({ token, profile }) {
if (profile) {
token.id = profile.id;
token.image = profile.avatar_url || profile.picture;
}
return token;
},
authorized({ auth }) {
// this ensures there is a logged in user for -every- request
return (
process.env.GITHUB_ADMIN_EMAIL !== undefined &&
process.env.GITHUB_ADMIN_EMAIL === auth?.user?.email
);
},
},
pages: {
// overrides the next-auth default sign-in page
signIn: '/sign-in',
},
});

47
src/auth/SignInForm.tsx Normal file
View File

@ -0,0 +1,47 @@
'use client';
import FieldSet from '@/components/FieldSet';
import InfoBlock from '@/components/InfoBlock';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
export default function SignInForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<InfoBlock
className="space-y-8"
padding="loose"
>
<div className="space-y-4">
<FieldSet
id="email"
label="Admin Email"
value={email}
onChange={setEmail}
/>
<FieldSet
id="password"
label="Admin Password"
value={password}
onChange={setPassword}
type="password"
/>
</div>
<button
onClick={() => signIn(
'credentials',
{
email,
password,
callbackUrl: '/admin/photos',
},
)}
>
Sign in
</button>
</InfoBlock>
);
}
//

57
src/auth/index.ts Normal file
View File

@ -0,0 +1,57 @@
import NextAuth, { User, type DefaultSession } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { NextResponse } from 'next/server';
declare module 'next-auth' {
interface Session {
user: {
id: string
} & DefaultSession['user']
}
}
export const {
handlers: { GET, POST },
auth,
CSRF_experimental,
} = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' },
},
async authorize({ email, password }) {
if (
process.env.ADMIN_EMAIL && process.env.ADMIN_EMAIL === email &&
process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD === password
) {
const user: User = { id: '1', email, name: 'Admin User' };
return user;
} else {
return null;
}
},
}),
],
callbacks: {
authorized({ auth, request }) {
const url = new URL(request.url);
const { pathname } = url;
const isUrlProtected = pathname.startsWith('/admin');
const isLoggedIn = !!auth?.user;
const isAuthorized = !isUrlProtected || isLoggedIn;
if (pathname === '/admin') {
url.pathname = '/admin/photos';
return NextResponse.redirect(url);
}
return isAuthorized;
},
},
pages: {
signIn: '/sign-in',
},
});

View File

@ -5,6 +5,7 @@ export default function FieldSet({
onChange,
required,
readOnly,
type = 'text',
}: {
id: string
label: string
@ -12,6 +13,7 @@ export default function FieldSet({
onChange?: (value: string) => void
required?: boolean
readOnly?: boolean
type?: 'text' | 'password'
}) {
return (
<div className="space-y-1">
@ -30,7 +32,7 @@ export default function FieldSet({
name={id}
value={value}
onChange={e => onChange?.(e.target.value)}
type="text"
type={type}
autoComplete="off"
readOnly={readOnly}
className="w-full"

View File

@ -3,13 +3,18 @@ import { ReactNode } from 'react';
export default function InfoBlock({
children,
className,
padding = 'loose',
}: {
children: ReactNode
className?: string
padding?: 'loose' | 'normal';
} ) {
return (
<div className={cc(
'flex flex-col items-center justify-center',
'px-8 py-24 rounded-lg',
'px-8 rounded-lg',
padding === 'loose' ? 'py-24' : 'py-8',
'border',
'bg-gray-50 border-gray-200',
'dark:bg-gray-900/40 dark:border-gray-800',
@ -19,6 +24,7 @@ export default function InfoBlock({
'flex flex-col items-center justify-center',
'space-y-4',
'text-gray-500 dark:text-gray-400',
className,
)}>
{children}
</div>

View File

@ -1,14 +0,0 @@
'use client';
import { signIn } from 'next-auth/react';
export default function LoginButton() {
return (
<div
className="button"
onClick={() => signIn('github', { callbackUrl: '/' })}
>
Sign in
</div>
);
}

View File

@ -1,5 +1,5 @@
export { auth as middleware } from './auth';
export const config = {
matcher: ['/admin/:path*'],
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

View File

@ -16,7 +16,7 @@
tracking-wider
}
button, .button,
input[type=text] {
input[type=text], input[type=password] {
@apply
px-2 py-1.5
border rounded-md
@ -25,7 +25,7 @@
font-mono text-base leading-none
min-h-[2.25rem]
}
input[type=text] {
input[type=text], input[type=password] {
@apply
min-w-[20rem] read-only:cursor-default
read-only:bg-gray-100