Skip to content

Instantly share code, notes, and snippets.

@andresgutgon
Last active October 28, 2023 22:59
Show Gist options
  • Save andresgutgon/dcbf135f890450f0a03eeb9b7891b268 to your computer and use it in GitHub Desktop.
Save andresgutgon/dcbf135f890450f0a03eeb9b7891b268 to your computer and use it in GitHub Desktop.
// db/schema/auth.ts
import {
int,
timestamp,
mysqlTable,
primaryKey,
varchar,
text
} from "drizzle-orm/mysql-core"
import type { AdapterAccount } from "@auth/core/adapters"
export const users = mysqlTable("users", {
id: varchar("id", { length: 255 }).notNull().primaryKey(),
name: varchar("name", { length: 255 }),
email: varchar("email", { length: 255 }).notNull(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: varchar("image", { length: 255 }),
})
export const accounts = mysqlTable(
"accounts",
{
userId: varchar("userId", { length: 255 }).notNull(),
type: varchar("type", { length: 255 }).$type<AdapterAccount["type"]>().notNull(),
provider: varchar("provider", { length: 255 }).notNull(),
providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(),
refresh_token: varchar("refresh_token", { length: 255 }),
refresh_token_expires_in: int("refresh_token_expires_in"),
access_token: varchar("access_token", { length: 255 }),
expires_at: int("expires_at"),
token_type: varchar("token_type", { length: 255 }),
scope: varchar("scope", { length: 255 }),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => ({
compoundKey: primaryKey(account.provider, account.providerAccountId),
})
)
export const sessions = mysqlTable("sessions", {
sessionToken: varchar("sessionToken", { length: 255 }).notNull().primaryKey(),
userId: varchar("userId", { length: 255 }).notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
})
export const verificationTokens = mysqlTable(
"verificationToken",
{
identifier: varchar("identifier", { length: 255 }).notNull(),
token: varchar("token", { length: 255 }).notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey(vt.identifier, vt.token),
})
)
// lib/auth/config.ts
import { type GetServerSidePropsContext } from "next"
import {
getServerSession,
type NextAuthOptions,
type DefaultSession,
} from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { db } from '@/db'
import { PlanetScaleAdapter } from "@/lib/auth/planetScaleAdapter"
import { Adapter } from "next-auth/adapters"
import { UserSession } from "@/lib/auth"
declare module "next-auth" {
interface Session extends DefaultSession {
user: UserSession
}
}
export const authOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id
}
return session
},
},
adapter: PlanetScaleAdapter(db) as Adapter,
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
}
export const getServerAuthSession = (ctx: {
req: GetServerSidePropsContext["req"]
res: GetServerSidePropsContext["res"]
}) => {
return getServerSession(ctx.req, ctx.res, authOptions)
}
// db/index.ts
import { drizzle } from "drizzle-orm/planetscale-serverless"
import { connect } from "@planetscale/database"
const connection = connect({
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
})
export const db = drizzle(connection)
export type DbClient = typeof db
// lib/auth/planetScaleAdapter
import { and, eq } from "drizzle-orm"
import crypto from 'node:crypto'
import { users, accounts, sessions, verificationTokens } from '@/db/schema/auth'
import { DbClient } from '@/db'
import type { Adapter } from "@auth/core/adapters"
export const defaultSchema = { users, accounts, sessions, verificationTokens }
export type DefaultSchema = typeof defaultSchema
interface CustomSchema extends DefaultSchema { }
export function PlanetScaleAdapter(
client: DbClient,
_schema?: Partial<CustomSchema>
): Adapter {
return {
createUser: async (data) => {
const id = crypto.randomUUID()
await client.insert(users).values({ ...data, id })
return client
.select()
.from(users)
.where(eq(users.id, id))
.then((res) => res[0])
},
getUser: async (data) => {
const user = await client
.select()
.from(users)
.where(eq(users.id, data))
.then((res) => res[0]) ?? null
return user
},
getUserByEmail: async (data) => {
const email = await client
.select()
.from(users)
.where(eq(users.email, data))
.then((res) => res[0]) ?? null
return email
},
createSession: async (data) => {
await client.insert(sessions).values(data)
const session = await client
.select()
.from(sessions)
.where(eq(sessions.sessionToken, data.sessionToken))
.then((res) => res[0])
return session
},
getSessionAndUser: async (data) => {
const sessionAndUser = await client
.select({
session: sessions,
user: users,
})
.from(sessions)
.where(eq(sessions.sessionToken, data))
.innerJoin(users, eq(users.id, sessions.userId))
.then((res) => res[0]) ?? null
return sessionAndUser
},
updateUser: async (data) => {
if (!data.id) {
throw new Error("No user id.")
}
await client
.update(users)
.set(data)
.where(eq(users.id, data.id))
const user = await client
.select()
.from(users)
.where(eq(users.id, data.id))
.then((res) => res[0])
return user
},
updateSession: async (data) => {
await client
.update(sessions)
.set(data)
.where(eq(sessions.sessionToken, data.sessionToken))
const session = await client
.select()
.from(sessions)
.where(eq(sessions.sessionToken, data.sessionToken))
.then((res) => res[0])
return session
},
linkAccount: async (rawAccount) => {
const account = await client.insert(accounts).values(rawAccount).then((res) => res.rows[0])
account
},
getUserByAccount: async (account) => {
const dbAccount = await client
.select()
.from(accounts)
.where(
and(
eq(accounts.providerAccountId, account.providerAccountId),
eq(accounts.provider, account.provider)
)
)
.leftJoin(users, eq(accounts.userId, users.id))
.then((res) => res[0])
return dbAccount?.users
},
deleteSession: async (sessionToken) => {
await client
.delete(sessions)
.where(eq(sessions.sessionToken, sessionToken))
},
createVerificationToken: async (token) => {
await client.insert(verificationTokens).values(token)
return client
.select()
.from(verificationTokens)
.where(eq(verificationTokens.identifier, token.identifier))
.then((res) => res[0])
},
useVerificationToken: async (token) => {
try {
const deletedToken =
(await client
.select()
.from(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, token.identifier),
eq(verificationTokens.token, token.token)
)
)
.then((res) => res[0])) ?? null
await client
.delete(verificationTokens)
.where(
and(
eq(verificationTokens.identifier, token.identifier),
eq(verificationTokens.token, token.token)
)
)
return deletedToken
} catch (err) {
throw new Error("No verification token found.")
}
},
deleteUser: async (id) => {
await client
.delete(users)
.where(eq(users.id, id))
.then((res) => res.rows[0])
},
unlinkAccount: async (account) => {
await client
.delete(accounts)
.where(
and(
eq(accounts.providerAccountId, account.providerAccountId),
eq(accounts.provider, account.provider)
)
)
return undefined
},
}
}
@Kinfe123
Copy link

Nice to see this

@fjohanssondev
Copy link

Is it a typo on line 110 in the lib/auth/planetScaleAdapter.ts? It should only return the single user, no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment