mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
email -> tokenId in some places
This commit is contained in:
237
apps/dev/nextjs/lib/db.ts
Normal file
237
apps/dev/nextjs/lib/db.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
} from "@auth/core/adapters"
|
||||
|
||||
export interface LocalStorageAdapterOptions {
|
||||
/**
|
||||
* 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: LocalStorageAdapterOptions = {}
|
||||
): 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,13 +39,16 @@ 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"
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||
const client = globalThis.prisma || new PrismaClient()
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||
const adapter = PrismaAdapter(client)
|
||||
// import { PrismaClient } from "@prisma/client"
|
||||
// import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||
// const client = globalThis.prisma || new PrismaClient()
|
||||
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||
// const adapter = PrismaAdapter(client)
|
||||
|
||||
// // Fauna
|
||||
// import { Client as FaunaClient } from "faunadb"
|
||||
@@ -71,8 +74,22 @@ const adapter = PrismaAdapter(client)
|
||||
// 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",
|
||||
@@ -140,53 +157,15 @@ if (authConfig.adapter) {
|
||||
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" }),
|
||||
{
|
||||
id: "sendgrid",
|
||||
type: "token",
|
||||
name: "SendGrid",
|
||||
async sendVerificationRequest({ identifier: email, url }) {
|
||||
// Call the cloud Email provider API for sending emails
|
||||
// See https://docs.sendgrid.com/api-reference/mail-send/mail-send
|
||||
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
|
||||
// The body format will vary depending on provider, please see their documentation
|
||||
// for further details.
|
||||
body: JSON.stringify({
|
||||
personalizations: [{ to: [{ email }] }],
|
||||
from: { email: "noreply@authjs.dev" },
|
||||
subject: "Sign in by SendGrid",
|
||||
content: [
|
||||
{
|
||||
type: "text/plain",
|
||||
value: `Please click here to authenticate - ${url}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
// Authentication will also vary from provider to provider, please see their docs.
|
||||
Authorization: `Bearer ${process.env.SENDGRID_API}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const { errors } = await response.json()
|
||||
throw new Error(JSON.stringify(errors))
|
||||
}
|
||||
},
|
||||
options: {},
|
||||
},
|
||||
{
|
||||
// SmtpEmail({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" }),
|
||||
Token({
|
||||
id: "token",
|
||||
type: "token",
|
||||
name: "Token",
|
||||
sendVerificationRequest(params) {
|
||||
console.log("sendVerificationRequest", params)
|
||||
return Promise.resolve()
|
||||
type: "token",
|
||||
async sendVerificationRequest(params) {
|
||||
console.log({ verificationUrl: params.url })
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -208,4 +187,4 @@ function AuthHandler(...args: any[]) {
|
||||
|
||||
export default AuthHandler(authConfig)
|
||||
|
||||
// export const config = { runtime: "edge" }
|
||||
export const config = { runtime: "edge" }
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function handleLogin(
|
||||
// Input validation
|
||||
if (!_account?.providerAccountId || !_account.type)
|
||||
throw new Error("Missing or invalid provider account")
|
||||
if (!["email", "oauth", "oidc"].includes(_account.type))
|
||||
if (!["token", "oauth", "oidc"].includes(_account.type))
|
||||
throw new Error("Provider not supported")
|
||||
|
||||
const {
|
||||
|
||||
@@ -51,8 +51,8 @@ export default function renderPage(params: RenderPageParams) {
|
||||
// We only want to render providers
|
||||
providers: params.providers?.filter(
|
||||
(provider) =>
|
||||
// Always render oauth and email type providers
|
||||
["email", "oauth", "oidc"].includes(provider.type) ||
|
||||
// Always render oauth and token type providers
|
||||
["token", "oauth", "oidc"].includes(provider.type) ||
|
||||
// Only render credentials type provider if credentials are defined
|
||||
(provider.type === "credentials" && provider.credentials) ||
|
||||
// Don't render other provider types
|
||||
|
||||
@@ -140,17 +140,17 @@ export default function SigninPage(props: {
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
<label
|
||||
className="section-header"
|
||||
htmlFor={`input-email-for-${provider.id}-provider`}
|
||||
htmlFor={`input-for-${provider.id}-provider`}
|
||||
>
|
||||
Token
|
||||
</label>
|
||||
<input
|
||||
id={`input-email-for-${provider.id}-provider`}
|
||||
id={`input-for-${provider.id}-provider`}
|
||||
autoFocus
|
||||
type="token"
|
||||
name="token"
|
||||
type="tokenId"
|
||||
name="tokenId"
|
||||
value={token}
|
||||
placeholder="email@example.com / + 1 555 123 4567"
|
||||
placeholder="email@example.com"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Sign in with {provider.name}</button>
|
||||
|
||||
@@ -23,8 +23,8 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
|
||||
)}
|
||||
<div className="card">
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<h1>Check your email</h1>
|
||||
<p>A sign in link has been sent to your email address.</p>
|
||||
<h1>Verification sent</h1>
|
||||
<p>A sign in link has been sent to you.</p>
|
||||
<p>
|
||||
<a className="site" href={url.origin}>
|
||||
{url.host}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createHash, randomString, toRequest } from "../web.js"
|
||||
|
||||
import type { InternalOptions, RequestInternal } from "../../types.js"
|
||||
/**
|
||||
* Starts an e-mail login flow, by generating a token,
|
||||
* and sending it to the user's e-mail (with the help of a DB adapter)
|
||||
* Starts an token login flow, by generating a token,
|
||||
* and sending it to the user's token identifier (with the help of a DB adapter)
|
||||
*/
|
||||
export default async function token(
|
||||
identifier: string,
|
||||
@@ -20,7 +20,11 @@ export default async function token(
|
||||
)
|
||||
|
||||
// Generate a link with token, unhashed token and callback url
|
||||
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
|
||||
const params = new URLSearchParams({
|
||||
callbackUrl,
|
||||
token,
|
||||
tokenId: identifier,
|
||||
})
|
||||
const _url = `${url}/callback/${provider.id}?${params}`
|
||||
|
||||
const secret = provider.secret ?? options.secret
|
||||
|
||||
@@ -45,7 +45,7 @@ export type TokenConfig<ProviderConfig = {}> = CommonProviderOptions &
|
||||
* Normalizes the user input before sending the verification request.
|
||||
*/
|
||||
normalizeIdentifier?: (identifier: string) => string
|
||||
options: ProviderConfig
|
||||
options?: ProviderConfig
|
||||
}
|
||||
|
||||
export default function Token(config: TokenConfig): TokenConfig {
|
||||
|
||||
Reference in New Issue
Block a user