Full Stack LinkedIn Prototype With Next.js Course Preview

👉 Full Course

Build And Deploy A Professional Network App With Next.js, Drizzle ORM, PostgreSQL, TailwindCSS, Mantine, NextAuth, And Vercel

Welcome to the first few chapters of the Full Stack LinkedIn Prototype Course.

This article contains the notes for setting up the authentication using NextAuth.

Create a Postgres Database

create database next_auth_drizzle;

Create a Next.js App

npx create-next-app@latest

Clear out the home page and default styles

Remove the content in /app/page.tsx and classes in /app/globals.css.

Install Dependencies

npm install next-auth
npm install drizzle-orm @auth/drizzle-adapter
npm install drizzle-kit --save-dev
npm install pg
npm install @types/pg --save-dev

Copy the database schema from Next Auth Docs

/lib/schema.ts

import {
  timestamp,
  pgTable,
  text,
  primaryKey,
  integer,
} from "drizzle-orm/pg-core";
import type { AdapterAccount } from "@auth/core/adapters";

export const users = pgTable("user", {
  id: text("id").notNull().primaryKey(),
  name: text("name"),
  email: text("email").notNull(),
  emailVerified: timestamp("emailVerified", { mode: "date" }),
  image: text("image"),
});

export const accounts = pgTable(
  "account",
  {
    userId: text("userId")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
    type: text("type").$type<AdapterAccount["type"]>().notNull(),
    provider: text("provider").notNull(),
    providerAccountId: text("providerAccountId").notNull(),
    refresh_token: text("refresh_token"),
    access_token: text("access_token"),
    expires_at: integer("expires_at"),
    token_type: text("token_type"),
    scope: text("scope"),
    id_token: text("id_token"),
    session_state: text("session_state"),
  },
  (account) => ({
    compoundKey: primaryKey({
      columns: [account.provider, account.providerAccountId],
    }),
  })
);

export const sessions = pgTable("session", {
  sessionToken: text("sessionToken").notNull().primaryKey(),
  userId: text("userId")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  expires: timestamp("expires", { mode: "date" }).notNull(),
});

export const verificationTokens = pgTable(
  "verificationToken",
  {
    identifier: text("identifier").notNull(),
    token: text("token").notNull(),
    expires: timestamp("expires", { mode: "date" }).notNull(),
  },
  (vt) => ({
    compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
  })
);

Note: This can be found in the Next-Auth docs: https://authjs.dev/reference/adapter/drizzle

Create a config file for loading environment variables

/lib/config.ts

import { loadEnvConfig } from "@next/env";

const projectDir = process.cwd();
loadEnvConfig(projectDir);

const config = {
  POSTGRES_URL: process.env.POSTGRES_URL!,
  NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET!,
  GITHUB_ID: process.env.GITHUB_ID!,
  GITHUB_SECRET: process.env.GITHUB_SECRET!,
  APP_ENV: process.env.APP_ENV!,
};

export default config;

Create the Drizzle DB instance

/lib/db.ts

import config from "@/lib/config";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

let sslmode = "";
if (config.APP_ENV === "prod") {
  sslmode = "?sslmode=require";
}

export const pool = new Pool({
  connectionString: config.POSTGRES_URL + sslmode,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

export const db = drizzle(pool, { schema, logger: true });

Note: We are preemptively dealing with the sslmode require problem in Vercel production.

Create an auth options file for Next-Auth

/lib/auth.ts

import { DrizzleAdapter } from "@auth/drizzle-adapter";
import GithubProvider from "next-auth/providers/github";
import { db } from "./db";
import config from "./config";
import { AuthOptions } from "next-auth";

export const authOptions: AuthOptions = {
  adapter: DrizzleAdapter(db) as any,
  secret: config.NEXTAUTH_SECRET,
  providers: [
    GithubProvider({
      clientId: config.GITHUB_ID,
      clientSecret: config.GITHUB_SECRET,
    }),
  ],
  callbacks: {
    async session({ session, user, token }) {
      session.user.id = token.sub;
      return session;
    },
  },
  session: {
    strategy: "jwt",
  },
};

Note:

  • The as any is used for fixing a typescript error.
  • secret must be defined.

Monkey patch the typescript error

/lib/next-auth.d.ts

import NextAuth from "next-auth/next";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
    } & DefaultSession["user"];
  }
}

Create the Next-Auth route

/app/api/auth/[...nextauth]/route.ts

import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

Create GitHub OAuth App

Go to GitHub > Settings > Developer Settings > OAuth Apps.

Click on "New OAuth App".

Create a new client secret.

The home page can be http://localhost:3000 for local development.

The authorization callback should be http://localhost:3000/api/auth/callback/github.

Copy environment variables into .env.local

Copy the client id and client secret into .env.local.

GITHUB_ID="copy client id here"
GITHUB_SECRET="copy client secret here"
NEXTAUTH_SECRET="a random string"
NEXTAUTH_URL="http://localhost:3000/api/auth"
POSTGRES_URL="a postgres url"

Note: NEXTAUTH_URL does not need to be defined if deploying to Vercel.

Create a Drizzle Config

/drizzle.config.ts

import config from "@/lib/config";
import { defineConfig } from "drizzle-kit";

let sslmode = "";
if (config.APP_ENV === "prod") {
  sslmode = "?sslmode=require";
}

export default defineConfig({
  schema: "./lib/schema.ts",
  out: "./drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: config.POSTGRES_URL + sslmode,
  },
  verbose: true,
  strict: true,
});

Update tsconfig.json compiler target

Update the target from es5 to es6. If not, there will be an error when you run the push command. If you're already on es6 or esnext, then no action is required.

Run the push command

npx drizzle-kit push:pg

This command will sync the schema to the database.

Create SessionProvider component

/components/session-provider.tsx

"use client";
import { SessionProvider } from "next-auth/react";
export default SessionProvider;

Note: Must be a client component or else there will be a React Context error.

Use the SessionProvider component

/app/(dynamic)/layout.tsx

import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { ReactNode } from "react";
import SessionProvider from "@/components/session-provider";

export default async function Layout({ children }: { children: ReactNode }) {
  const session = await getServerSession(authOptions);
  return (
    <SessionProvider session={session}>
      <div>{children}</div>
    </SessionProvider>
  );
}

Note: authOptions must be passed into getServerSession.

Note: We are not using the session provider in the root layout. Instead, We are using (dynamic) route group to separate static marketing pages and dynamic pages. The session makes a page dynamic because it is using cookies. This is to prevent losing the benefits of caching for static marketing pages.

Create a Login Button component

/components/login-btn.tsx

"use client";
import { useSession, signIn, signOut } from "next-auth/react";

export default function LoginBtn() {
  const { data: session } = useSession();
  if (session) {
    return (
      <>
        Signed in as {session.user?.name} <br />
        <button onClick={() => signOut()}>Sign out</button>
      </>
    );
  }
  return (
    <>
      Not signed in <br />
      <button onClick={() => signIn()}>Sign in</button>
    </>
  );
}

Use the Login Button component

/app/(dynamic)/signin/page.tsx

import LoginBtn from "@/components/login-btn";

export default function Page() {
  return (
    <div>
      <p>Sign In</p>
      <LoginBtn />
    </div>
  );
}

Test the login by going to the sign in page and signing in.

Create a restricted route

/app/api/restricted/route.ts

import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";

import { authOptions } from "@/lib/auth";

export async function GET() {
  const session = await getServerSession(authOptions);

  return NextResponse.json({ name: session?.user?.name ?? "Not Logged In" });
}

Test by sending a GET request to http://localhost:3000/api/restricted.

Create a restricted page

/app/restricted/page.tsx

import { getServerSession } from "next-auth";

import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function Page() {
  const session = await getServerSession(authOptions);

  if (!session) {
    redirect("/");
  }

  return (
    <div>
      <h1>Restricted Page</h1>
    </div>
  );
}

Test by visiting http://localhost:3000/restricted.

Add a logo to the automatically generated signin page

Add an image file to the /public folder.

Add a theme option to authOptions.

/lib/auth.ts

  theme: {
    logo: "/logo.svg",
  },

Create a custom signin button and signin page

/components/signin-btn.tsx

"use client";

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

export default function SigninBtn({ provider }: { provider: any }) {
  return (
    <button onClick={() => signIn(provider.id)}>
      Sign in with {provider.name}
    </button>
  );
}

/app/signin/page.tsx

import { getProviders } from "next-auth/react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
import SigninBtn from "@/components/signin-btn";
import { redirect } from "next/navigation";

export default async function SignIn() {
  const session = await getServerSession(authOptions);

  if (session) {
    redirect("/dashboard");
  }

  const providers: any = await getProviders();

  return (
    <>
      {Object.values(providers).map((provider: any) => (
        <div key={provider.name}>
          <SigninBtn provider={provider} />
        </div>
      ))}
    </>
  );
}

References

  1. https://authjs.dev/reference/adapter/drizzle
  2. https://github.com/nextauthjs/next-auth/issues/7727#issuecomment-1688714579
  3. https://stackoverflow.com/questions/71385330/next-auth-jwedecryptionfailed
  4. https://next-auth.js.org/warnings#nextauth_url
  5. https://github.com/nextauthjs/next-auth/discussions/4255
  6. https://authjs.dev/reference/adapter/pg