Switch to email-based login
This commit is contained in:
parent
0ce0cceb5b
commit
e9db8b7a7a
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
40
src/auth.ts
40
src/auth.ts
@ -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
47
src/auth/SignInForm.tsx
Normal 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
57
src/auth/index.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export { auth as middleware } from './auth';
|
||||
|
||||
export const config = {
|
||||
matcher: ['/admin/:path*'],
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user