Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: token provider #6927

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions apps/dev/nextjs/lib/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Copy from RedisUpstashAdapter
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
} from "@auth/core/adapters"

export interface AdapterOptions {
/**
* The base prefix for your keys
*/
baseKeyPrefix?: string
/**
* The prefix for the `account` key
*/
accountKeyPrefix?: string
/**
* The prefix for the `accountByUserId` key
*/
accountByUserIdPrefix?: string
/**
* The prefix for the `emailKey` key
*/
emailKeyPrefix?: string
/**
* The prefix for the `sessionKey` key
*/
sessionKeyPrefix?: string
/**
* The prefix for the `sessionByUserId` key
*/
sessionByUserIdKeyPrefix?: string
/**
* The prefix for the `user` key
*/
userKeyPrefix?: string
/**
* The prefix for the `verificationToken` key
*/
verificationTokenKeyPrefix?: string
}

export const defaultOptions = {
baseKeyPrefix: "",
accountKeyPrefix: "user:account:",
accountByUserIdPrefix: "user:account:by-user-id:",
emailKeyPrefix: "user:email:",
sessionKeyPrefix: "user:session:",
sessionByUserIdKeyPrefix: "user:session:by-user-id:",
userKeyPrefix: "user:",
verificationTokenKeyPrefix: "user:token:",
}

const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}

export function hydrateDates(text: string) {
return Object.entries(JSON.parse(text)).reduce((acc, [key, val]) => {
acc[key] = isDate(val) ? new Date(val as string) : val
return acc
}, {} as any)
}

export function TestAdapter(
client: {
getItem: (key: string) => Promise<string | null>
setItem: (key: string, value: string) => Promise<void>
deleteItems: (...keys: string[]) => Promise<void>
},
options: AdapterOptions = {}
): Adapter {
const mergedOptions = {
...defaultOptions,
...options,
}

const { baseKeyPrefix } = mergedOptions
const accountKeyPrefix = baseKeyPrefix + mergedOptions.accountKeyPrefix
const accountByUserIdPrefix =
baseKeyPrefix + mergedOptions.accountByUserIdPrefix
const emailKeyPrefix = baseKeyPrefix + mergedOptions.emailKeyPrefix
const sessionKeyPrefix = baseKeyPrefix + mergedOptions.sessionKeyPrefix
const sessionByUserIdKeyPrefix =
baseKeyPrefix + mergedOptions.sessionByUserIdKeyPrefix
const userKeyPrefix = baseKeyPrefix + mergedOptions.userKeyPrefix
const verificationTokenKeyPrefix =
baseKeyPrefix + mergedOptions.verificationTokenKeyPrefix

const setObjectAsJson = async (key: string, obj: any) =>
await client.setItem(key, JSON.stringify(obj))

const setAccount = async (id: string, account: AdapterAccount) => {
const accountKey = accountKeyPrefix + id
await setObjectAsJson(accountKey, account)
await client.setItem(accountByUserIdPrefix + account.userId, accountKey)
return account
}

const getAccount = async (id: string) => {
const account = await client.getItem(accountKeyPrefix + id)
if (!account) return null
return hydrateDates(account)
}

const setSession = async (
id: string,
session: AdapterSession
): Promise<AdapterSession> => {
const sessionKey = sessionKeyPrefix + id
await setObjectAsJson(sessionKey, session)
await client.setItem(sessionByUserIdKeyPrefix + session.userId, sessionKey)
return session
}

const getSession = async (id: string) => {
const session = await client.getItem(sessionKeyPrefix + id)
if (!session) return null
return hydrateDates(session)
}

const setUser = async (
id: string,
user: AdapterUser
): Promise<AdapterUser> => {
await setObjectAsJson(userKeyPrefix + id, user)
await client.setItem(`${emailKeyPrefix}${user.email as string}`, id)
return user
}

const getUser = async (id: string) => {
const user = await client.getItem(userKeyPrefix + id)
if (!user) return null
return hydrateDates(user)
}

return {
async createUser(user) {
const id = crypto.randomUUID()
// TypeScript thinks the emailVerified field is missing
// but all fields are copied directly from user, so it's there
return await setUser(id, { ...user, id })
},
getUser,
async getUserByTokenId(email) {
const userId = await client.getItem(emailKeyPrefix + email)
if (!userId) {
return null
}
return await getUser(userId)
},
async getUserByAccount(account) {
const dbAccount = await getAccount(
`${account.provider}:${account.providerAccountId}`
)
if (!dbAccount) return null
return await getUser(dbAccount.userId)
},
async updateUser(updates) {
const userId = updates.id as string
const user = await getUser(userId)
return await setUser(userId, { ...(user as AdapterUser), ...updates })
},
async linkAccount(account) {
const id = `${account.provider}:${account.providerAccountId}`
return await setAccount(id, { ...account, id })
},
createSession: (session) => setSession(session.sessionToken, session),
async getSessionAndUser(sessionToken) {
const session = await getSession(sessionToken)
if (!session) return null
const user = await getUser(session.userId)
if (!user) return null
return { session, user }
},
async updateSession(updates) {
const session = await getSession(updates.sessionToken)
if (!session) return null
return await setSession(updates.sessionToken, { ...session, ...updates })
},
async deleteSession(sessionToken) {
await client.deleteItems(sessionKeyPrefix + sessionToken)
},
async createVerificationToken(verificationToken) {
await setObjectAsJson(
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token,
verificationToken
)
return verificationToken
},
async useVerificationToken(verificationToken) {
const tokenKey =
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token

const token = await client.getItem(tokenKey)
if (!token) return null

await client.deleteItems(tokenKey)
return hydrateDates(token)
// return reviveFromJson(token)
},
async unlinkAccount(account) {
const id = `${account.provider}:${account.providerAccountId}`
const dbAccount = await getAccount(id)
if (!dbAccount) return
const accountKey = `${accountKeyPrefix}${id}`
await client.deleteItems(
accountKey,
`${accountByUserIdPrefix} + ${dbAccount.userId as string}`
)
},
async deleteUser(userId) {
const user = await getUser(userId)
if (!user) return
const accountByUserKey = accountByUserIdPrefix + userId
const accountKey = await client.getItem(accountByUserKey)
const sessionByUserIdKey = sessionByUserIdKeyPrefix + userId
const sessionKey = await client.getItem(sessionByUserIdKey)
await client.deleteItems(
userKeyPrefix + userId,
`${emailKeyPrefix}${user.email as string}`,
accountKey as string,
accountByUserKey,
sessionKey as string,
sessionByUserIdKey
)
},
}
}
39 changes: 32 additions & 7 deletions apps/dev/nextjs/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Credentials from "@auth/core/providers/credentials"
import Descope from "@auth/core/providers/descope"
import Discord from "@auth/core/providers/discord"
import DuendeIDS6 from "@auth/core/providers/duende-identity-server6"
// import Email from "@auth/core/providers/email"
import SmtpEmail from "@auth/core/providers/email-smtp"
import Facebook from "@auth/core/providers/facebook"
import Foursquare from "@auth/core/providers/foursquare"
import Freshbooks from "@auth/core/providers/freshbooks"
Expand All @@ -39,6 +39,9 @@ import Yandex from "@auth/core/providers/yandex"
import Vk from "@auth/core/providers/vk"
import Wikimedia from "@auth/core/providers/wikimedia"
import WorkOS from "@auth/core/providers/workos"
import { AdapterUser } from "next-auth/adapters"
import Token from "@auth/core/providers/token"
import { TestAdapter } from "lib/db"

// // Prisma
// import { PrismaClient } from "@prisma/client"
Expand Down Expand Up @@ -71,8 +74,22 @@ import WorkOS from "@auth/core/providers/workos"
// secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
// })

const db = {}

export const authConfig: AuthConfig = {
// adapter,
adapter: TestAdapter({
getItem(key) {
return db[key]
},
setItem: function (key: string, value: string): Promise<void> {
db[key] = value
return Promise.resolve()
},
deleteItems: function (...keys: string[]): Promise<void> {
keys.forEach((key) => delete db[key])
return Promise.resolve()
},
}),
debug: process.env.NODE_ENV !== "production",
theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
Expand Down Expand Up @@ -137,11 +154,19 @@ export const authConfig: AuthConfig = {

if (authConfig.adapter) {
// TODO:
// authOptions.providers.unshift(
// // NOTE: You can start a fake e-mail server with `pnpm email`
// // and then go to `http://localhost:1080` in the browser
// Email({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" })
// )
authConfig.providers.unshift(
// NOTE: You can start a fake e-mail server with `pnpm email`
// and then go to `http://localhost:1080` in the browser
// SmtpEmail({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" }),
Token({
id: "token",
name: "Token",
type: "token",
async sendVerificationRequest(params) {
console.log({ verificationUrl: params.url })
},
})
)
}

// TODO: move to next-auth/edge
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-dgraph/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export function DgraphAdapter(

return format.from<any>(result)
},
async getUserByEmail(email) {
async getUserByTokenId(email) {
const [user] = await c.run<any>(
/* GraphQL */ `
query ($email: String = "") {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-drizzle/src/lib/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function mySqlDrizzleAdapter(

return thing
},
async getUserByEmail(data) {
async getUserByTokenId(data) {
const user =
(await client
.select()
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-drizzle/src/lib/pg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function pgDrizzleAdapter(
.where(eq(users.id, data))
.then((res) => res[0] ?? null)
},
async getUserByEmail(data) {
async getUserByTokenId(data) {
return await client
.select()
.from(users)
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-drizzle/src/lib/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function SQLiteDrizzleAdapter(
getUser(data) {
return client.select().from(users).where(eq(users.id, data)).get() ?? null
},
getUserByEmail(data) {
getUserByTokenId(data) {
return (
client.select().from(users).where(eq(users.email, data)).get() ?? null
)
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-dynamodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export function DynamoDBAdapter(
})
return format.from<AdapterUser>(data.Item)
},
async getUserByEmail(email) {
async getUserByTokenId(email) {
const data = await client.query({
TableName,
IndexName,
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-fauna/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export function FaunaAdapter(f: FaunaClient): Adapter {
return {
createUser: async (data) => (await q(Create(Users, { data: to(data) })))!,
getUser: async (id) => await q(Get(Ref(Users, id))),
getUserByEmail: async (email) => await q(Get(Match(UserByEmail, email))),
getUserByTokenId: async (email) => await q(Get(Match(UserByEmail, email))),
async getUserByAccount({ provider, providerAccountId }) {
const key = [provider, providerAccountId]
const ref = Match(AccountByProviderAndProviderAccountId, key)
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-firebase/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function FirestoreAdapter(
return await getDoc(C.users.doc(id))
},

async getUserByEmail(email) {
async getUserByTokenId(email) {
return await getOneDoc(C.users.where("email", "==", email))
},

Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-kysely/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export function KyselyAdapter(db: Kysely<Database>): Adapter {
if (!result) return null
return to(result, "emailVerified")
},
async getUserByEmail(email) {
async getUserByTokenId(email) {
const result =
(await db
.selectFrom("User")
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-mikro-orm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export function MikroOrmAdapter<

return wrap(user).toObject()
},
async getUserByEmail(email) {
async getUserByTokenId(email) {
const em = await getEM()
const user = await em.findOne(UserModel, { email })
if (!user) return null
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-mongodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function MongoDBAdapter(
if (!user) return null
return from<AdapterUser>(user)
},
async getUserByEmail(email) {
async getUserByTokenId(email) {
const user = await (await db).U.findOne({ email })
if (!user) return null
return from<AdapterUser>(user)
Expand Down
Loading
Loading