Last active
October 28, 2023 22:59
-
-
Save andresgutgon/dcbf135f890450f0a03eeb9b7891b268 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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), | |
}) | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
}, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is it a typo on
line 110
in thelib/auth/planetScaleAdapter.ts
? It should only return the single user, no?