Skip to content

Commit

Permalink
NextAuth magic emails; user data in checkouts
Browse files Browse the repository at this point in the history
  • Loading branch information
danrowden committed Aug 11, 2023
1 parent 6f9d39e commit 2f5c394
Show file tree
Hide file tree
Showing 20 changed files with 1,551 additions and 188 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ This Next.js demo app can be used as a base for building subscription-based SaaS
- Lemon Squeezy billing
- NextAuth.js auth
- Prisma ORM
- Tailwind CSS
- Tailwind CSS
- Resend emails
8 changes: 5 additions & 3 deletions app/billing/page.tsx → app/(app)/billing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { getSession } from "@/lib/auth";
import type { Metadata } from 'next';
import prisma from "lib/prisma";
import prisma from "@/lib/prisma";

import Plans from 'components/plan';
import Plans from '@/components/plan';

import { LemonSqueezy } from "./lemonsqueezy";
import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'
const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY);


Expand Down Expand Up @@ -35,6 +36,7 @@ async function getSubscription(userId) {


export default async function Billing() {
const session = await getSession();

const plans = await getPlans()

Expand Down
File renamed without changes.
File renamed without changes.
23 changes: 23 additions & 0 deletions app/(app)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'

import { signIn } from "next-auth/react"

const handleSubmit = (event) => {
event.preventDefault()
signIn("email", { email: event.target.email.value })
}

export default function SignIn() {
return (
<div>
<h1>Sign in with a magic link</h1>
<form method="post" onSubmit={handleSubmit}>
<label>
Email address
<input type="email" id="email" name="email" />
</label>
<button type="submit">Sign in with Email</button>
</form>
</div>
)
}
9 changes: 9 additions & 0 deletions app/(app)/login/verify/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function Verify() {
return (
<>
<p>Magic link sent!</p>
<p>Please check your email inbox for an email from <code>{process.env.NEXT_PUBLIC_EMAIL_FROM_DEFAULT}</code>.</p>
<p>Return to <a href="/">Homepage</a></p>
</>
)
}
8 changes: 8 additions & 0 deletions app/(marketing)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function Home() {
return (
<div>
<h1>Demo Billing app</h1>
<p>Marketing home</p>
</div>
)
}
6 changes: 6 additions & 0 deletions app/api/auth/[...nextauth]/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST }
9 changes: 6 additions & 3 deletions app/api/checkouts/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { LemonSqueezy } from "../../billing/lemonsqueezy";
import { getSession } from "@/lib/auth";

import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'
const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY);


export async function POST(request: Request) {
const session = await getSession();

const res = await request.json()

Expand All @@ -19,9 +22,9 @@ export async function POST(request: Request) {
'button_color': '#fde68a'
},
'checkout_data': {
'email': "[email protected]", // Displays in the checkout form eg session.user.email with NextAuth.js
'email': session.user.email, // Displays in the checkout form eg session.user.email with NextAuth.js
'custom': {
'user_id': "1" // Sent in the background; visible in webhooks and API calls eg session.user.id with NextAuth.js
'user_id': session.user.id // Sent in the background; visible in webhooks and API calls eg session.user.id with NextAuth.js
}
},
'product_options': {
Expand Down
94 changes: 0 additions & 94 deletions app/billing/lemonsqueezy/index.js

This file was deleted.

36 changes: 19 additions & 17 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
import { Providers } from "./providers"

import './globals.css'

Expand All @@ -19,26 +20,27 @@ export default function RootLayout({
</head>

<body className="flex flex-col h-full">
<Providers>

<header className="bg-amber-200 p-3 mb-5 text-center">
<h1 className="text-lg text-gray-800">Lemonstand</h1>
</header>
<header className="bg-amber-200 p-3 mb-5 text-center">
<h1 className="text-lg text-gray-800">Lemonstand</h1>
</header>

<main className="p-3 grow text-gray-900">
{children}
</main>

<footer className="p-3 text-sm text-gray-400 md:flex md:justify-between">
<div>
<span className="text-gray-500">Lemonsqueezy Demo App</span> &middot;&nbsp;
<a href="" target="_blank">View on GitHub ↗</a> &middot;&nbsp;
<a href="https://docs.lemonsqueezy.com" target="_blank">Lemon Squeezy Docs ↗</a>
</div>
<div className="mt-2 md:mt-0">
<a href="https://lemonsqueezy.com" target="_blank">lemonsqueezy.com ↗</a>
</div>
</footer>
<main className="p-3 grow text-gray-900">
{children}
</main>

<footer className="p-3 text-sm text-gray-400 md:flex md:justify-between">
<div>
<span className="text-gray-500">Lemonsqueezy Demo App</span> &middot;&nbsp;
<a href="" target="_blank">View on GitHub ↗</a> &middot;&nbsp;
<a href="https://docs.lemonsqueezy.com" target="_blank">Lemon Squeezy Docs ↗</a>
</div>
<div className="mt-2 md:mt-0">
<a href="https://lemonsqueezy.com" target="_blank">lemonsqueezy.com ↗</a>
</div>
</footer>
</Providers>
</body>
</html>
)
Expand Down
13 changes: 13 additions & 0 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { SessionProvider } from "next-auth/react";
import { Toaster } from "sonner";

export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<Toaster />
{children}
</SessionProvider>
);
};
39 changes: 39 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getServerSession } from "next-auth/next";
import { NextAuthOptions } from 'next-auth';
import EmailProvider from 'next-auth/providers/email';
import { PrismaAdapter } from "@auth/prisma-adapter";
import prisma from "@/lib/prisma";
import { sendVerificationRequest } from '@/utils/send-verification-request';

export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
EmailProvider({
name: 'email',
server: '',
from: '[email protected]',
sendVerificationRequest,
}),
],
pages: {
signIn: '/login',
verifyRequest: '/login/verify',
},
session: {
strategy: 'jwt'
},
callbacks: {
// Add user ID to session from token
session: async ({ session, token }) => {
if (session?.user) {
session.user.id = token.sub;
}
return session;
}
}
}


export function getSession() {
return getServerSession(authOptions)
}
19 changes: 12 additions & 7 deletions lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from '@prisma/client'

declare global {
var prisma: PrismaClient | undefined;
}
// PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit.
//
// Learn more:
// https://pris.ly/d/help/next-js-best-practices

const prisma = global.prisma || new PrismaClient({ log: ["info"] });
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
const globalForPrisma = global as unknown as { prisma: PrismaClient }

export default prisma;
export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

export default prisma
3 changes: 3 additions & 0 deletions lib/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Resend } from 'resend';

export const resend = new Resend(process.env.RESEND_API_KEY!);
61 changes: 61 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getToken } from "next-auth/jwt"
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"

export default withAuth(
async function middleware(req) {
const token = await getToken({ req })
const isAuth = !!token
const isAuthPage = req.nextUrl.pathname.startsWith("/login")

// Redirect auth pages (/login) to dashboard when logged in
if (isAuthPage) {
if (isAuth) {
return NextResponse.redirect(new URL("/dashboard", req.url))
}

return null
}

if (!isAuth) {
let from = req.nextUrl.pathname;
if (req.nextUrl.search) {
from += req.nextUrl.search;
}

// Load the homepage (/home) at / when not logged in
if (req.nextUrl.pathname == '/') {
return NextResponse.rewrite(new URL('/', req.url));
}

return NextResponse.redirect(
new URL(`/login?from=${encodeURIComponent(from)}`, req.url)
);
} else {
// Redirect / to /dashboard when logged in
// if (req.nextUrl.pathname == '/') {
// return NextResponse.redirect(new URL('/dashboard', req.url));
// }
}
},
{
callbacks: {
async authorized() {
// This is a work-around for handling redirect on auth pages.
// We return true here so that the middleware function above
// is always called.
return true
},
},
}
)

export const config = {
// /login + all locked pages should be listed here
matcher: [
'/',
'/dashboard',
'/billing',
'/login'
],
}
Loading

0 comments on commit 2f5c394

Please sign in to comment.