feat: setup magic link login via next auth
This commit is contained in:
@@ -5,6 +5,17 @@
|
||||
await import("./src/env.mjs");
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {};
|
||||
const config = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "storage.googleapis.com",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@trpc/react-query": "^10.37.1",
|
||||
"@trpc/server": "^10.37.1",
|
||||
"drizzle-orm": "^0.28.5",
|
||||
"formik": "^2.4.5",
|
||||
"next": "^13.5.4",
|
||||
"next-auth": "^4.24.4",
|
||||
"react": "18.2.0",
|
||||
|
||||
52
src/app/_components/auth.tsx
Normal file
52
src/app/_components/auth.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import { signIn } from "next-auth/react";
|
||||
|
||||
interface FormValues {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: "",
|
||||
}}
|
||||
onSubmit={async (values: FormValues) => {
|
||||
await signIn("email", { email: values.email });
|
||||
}}
|
||||
>
|
||||
<Form className="space-y-6">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-dark-1000 block text-sm font-normal leading-6"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<Field
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="bg-dark-500 focus:ring-dark-900 ring-dark-900 text-dark-1000 block w-full rounded-md border-0 bg-white/5 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-dark-1000 text-dark-50 focus-visible:outline-offset- flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold leading-6 shadow-sm focus-visible:outline focus-visible:outline-2"
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
20
src/app/auth/login/page.tsx
Normal file
20
src/app/auth/login/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Login } from "~/app/_components/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<main className="bg-dark-50 flex h-screen flex-col items-center justify-center">
|
||||
<h1 className="text-dark-1000 mb-6 text-lg font-normal tracking-tight">
|
||||
貫 kan
|
||||
</h1>
|
||||
<p className="text-dark-1000 mb-10 text-3xl">Get started</p>
|
||||
<div className="bg-dark-400 border-dark-600 w-full rounded-md border px-10 py-10 sm:max-w-md">
|
||||
<div className=" sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<Login />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-dark-1000 mt-10 text-center text-sm">
|
||||
{"Already have an account?"}
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
12
src/app/auth/verify/page.tsx
Normal file
12
src/app/auth/verify/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function VerifyPage() {
|
||||
return (
|
||||
<main className="bg-dark-50 flex h-screen flex-col items-center justify-center">
|
||||
<p className="text-dark-1000 mb-10 text-3xl">Check your inbox</p>
|
||||
<div className=" sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<p className="text-dark-1000 text-md mt-5 text-center">
|
||||
{`Click on the link we've sent to you@example.com to sign in.`}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const NavButton: React.FC<{
|
||||
href={href}
|
||||
className={classNames(
|
||||
current ? "bg-dark-400 text-white" : "bg-dark-400 text-white",
|
||||
"text-dark-1000 group flex items-center gap-x-3 rounded-md p-1.5 text-sm font-normal leading-6",
|
||||
"group flex items-center gap-x-3 rounded-md p-1.5 text-sm font-normal leading-6 text-dark-1000",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
@@ -34,26 +34,28 @@ export default async function Layout(props: { children: React.ReactNode }) {
|
||||
if (!session?.user) redirect("api/auth/signin");
|
||||
|
||||
return (
|
||||
<main className="bg-dark-50 flex h-screen flex-col items-center">
|
||||
<div className="border-dark-600 m-auto flex h-16 w-full justify-between border-b px-5 py-2 align-middle">
|
||||
<main className="flex h-screen flex-col items-center bg-dark-50">
|
||||
<div className="m-auto flex h-16 w-full justify-between border-b border-dark-600 px-5 py-2 align-middle">
|
||||
<div className="my-auto flex">
|
||||
<h1 className="text-dark-1000 text-lg font-normal tracking-tight">
|
||||
<h1 className="text-lg font-normal tracking-tight text-dark-1000">
|
||||
貫 kan
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full">
|
||||
<nav className="border-dark-600 w-64 border-r px-5 py-5">
|
||||
<nav className="w-64 border-r border-dark-600 px-5 py-5">
|
||||
<button className="flex items-center">
|
||||
<Image
|
||||
src={session?.user.image}
|
||||
className="h-8 w-8 rounded-full bg-gray-50"
|
||||
width={30}
|
||||
height={30}
|
||||
alt=""
|
||||
/>
|
||||
<p className="text-dark-1000 ml-2 text-sm">{session?.user.name}</p>
|
||||
{session?.user.image ? (
|
||||
<Image
|
||||
src={session?.user.image ?? ""}
|
||||
className="h-8 w-8 rounded-full bg-gray-50"
|
||||
width={30}
|
||||
height={30}
|
||||
alt=""
|
||||
/>
|
||||
) : null}
|
||||
<p className="ml-2 text-sm text-dark-1000">{session?.user.name}</p>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -45,16 +45,42 @@ export const authOptions: NextAuthOptions = {
|
||||
}),
|
||||
},
|
||||
adapter: DrizzleAdapter(db, mysqlTable),
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
// signOut: "/auth/signout",
|
||||
// error: "/auth/error",
|
||||
verifyRequest: "/auth/verify",
|
||||
},
|
||||
providers: [
|
||||
/**
|
||||
* ...add more providers here.
|
||||
*
|
||||
* Most other providers require a bit more work than the Discord provider. For example, the
|
||||
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
|
||||
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
|
||||
*
|
||||
* @see https://next-auth.js.org/providers/github
|
||||
*/
|
||||
{
|
||||
id: 'email',
|
||||
type: 'email',
|
||||
from: process.env.EMAIL_FROM ?? '',
|
||||
server: {},
|
||||
maxAge: 24 * 60 * 60,
|
||||
name: 'Email',
|
||||
options: {},
|
||||
async sendVerificationRequest({ identifier: email, url }) {
|
||||
await fetch(process.env.EMAIL_URL ?? '', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.EMAIL_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: process.env.EMAIL_FROM,
|
||||
to: [email],
|
||||
subject: 'Login via email',
|
||||
html: `<strong>${url}</strong>`,
|
||||
}),
|
||||
})
|
||||
|
||||
// if (!response.ok) {
|
||||
// const { errors } = await response.json()
|
||||
// throw new Error(JSON.stringify(errors))
|
||||
// }
|
||||
},
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user