mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
7 Commits
@auth/core
...
@auth/d1-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0cc046e2d | ||
|
|
c818d028aa | ||
|
|
3ba8a0e40a | ||
|
|
770d3565f8 | ||
|
|
ed32236712 | ||
|
|
307f7b5eb9 | ||
|
|
120d7a29ee |
@@ -61,7 +61,7 @@ pnpm install
|
|||||||
4. Start the development server
|
4. Start the development server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pnpm dev:docs
|
pnpm dev:docs
|
||||||
```
|
```
|
||||||
|
|
||||||
And thats all! Now you should have a local copy of this docs site running at [localhost:3000](http://localhost:3000)!
|
And thats all! Now you should have a local copy of this docs site running at [localhost:3000](http://localhost:3000)!
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ NEXTAUTH_SECRET="This is an example"
|
|||||||
`NEXTAUTH_SECRET` is a random string used by the library to encrypt tokens and email verification hashes, and **it's mandatory to keep things secure**! 🔥 🔐 . You can use:
|
`NEXTAUTH_SECRET` is a random string used by the library to encrypt tokens and email verification hashes, and **it's mandatory to keep things secure**! 🔥 🔐 . You can use:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
```
|
```
|
||||||
|
|
||||||
or https://generate-secret.vercel.app/32 to generate a random value for it.
|
or https://generate-secret.vercel.app/32 to generate a random value for it.
|
||||||
@@ -242,7 +242,7 @@ AUTH_SECRET="This is an example"
|
|||||||
`AUTH_SECRET` is a random string used by the library to encrypt tokens and email verification hashes, and **it's mandatory to keep things secure**! 🔥 🔐 . You can use:
|
`AUTH_SECRET` is a random string used by the library to encrypt tokens and email verification hashes, and **it's mandatory to keep things secure**! 🔥 🔐 . You can use:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
```
|
```
|
||||||
|
|
||||||
or https://generate-secret.vercel.app/32 to generate a random value for it.
|
or https://generate-secret.vercel.app/32 to generate a random value for it.
|
||||||
@@ -429,7 +429,7 @@ export default NextAuth({
|
|||||||
Great! We're now ready to run our application locally. Start the Next.js app by running on your terminal the following command and navigating to [`http://localhost:3000`](http://localhost:3000):
|
Great! We're now ready to run our application locally. Start the Next.js app by running on your terminal the following command and navigating to [`http://localhost:3000`](http://localhost:3000):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run next dev
|
npm run next dev
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
@@ -448,7 +448,7 @@ export const handle = SvelteKitAuth({
|
|||||||
Great! We're now ready to run our application locally. Start the Svelte app by running on your terminal the following command and navigating to [`http://localhost:5173`](http://localhost:5173):
|
Great! We're now ready to run our application locally. Start the Svelte app by running on your terminal the following command and navigating to [`http://localhost:5173`](http://localhost:5173):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run vite dev
|
npm run vite dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -580,7 +580,7 @@ Auth.js used to generate a secret for convenience, when the user did not define
|
|||||||
You can generate a secret to be placed in the `secret` configuration option via the following command:
|
You can generate a secret to be placed in the `secret` configuration option via the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
```
|
```
|
||||||
|
|
||||||
Therefore, your Auth.js config should look something like this:
|
Therefore, your Auth.js config should look something like this:
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ title: Overview
|
|||||||
Using an Auth.js / NextAuth.js adapter you can connect to any database service or even several different services at the same time. The following listed official adapters are created and maintained by the community:
|
Using an Auth.js / NextAuth.js adapter you can connect to any database service or even several different services at the same time. The following listed official adapters are created and maintained by the community:
|
||||||
|
|
||||||
<div class="adapter-card-list">
|
<div class="adapter-card-list">
|
||||||
|
<a href="/reference/adapter/d1" class="adapter-card">
|
||||||
|
<img src="/img/adapters/d1.svg" width="40" />
|
||||||
|
<h4 class="adapter-card__title">D1 Adapter</h4>
|
||||||
|
</a>
|
||||||
<a href="/reference/adapter/edgedb" class="adapter-card">
|
<a href="/reference/adapter/edgedb" class="adapter-card">
|
||||||
<img src="/img/adapters/edgedb.svg" width="30" />
|
<img src="/img/adapters/edgedb.svg" width="30" />
|
||||||
<h4 class="adapter-card__title">EdgeDB Adapter</h4>
|
<h4 class="adapter-card__title">EdgeDB Adapter</h4>
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ const docusaurusConfig = {
|
|||||||
...(process.env.TYPEDOC_SKIP_ADAPTERS
|
...(process.env.TYPEDOC_SKIP_ADAPTERS
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
|
typedocAdapter("D1"),
|
||||||
typedocAdapter("EdgeDb"),
|
typedocAdapter("EdgeDb"),
|
||||||
typedocAdapter("Dgraph"),
|
typedocAdapter("Dgraph"),
|
||||||
typedocAdapter("Drizzle"),
|
typedocAdapter("Drizzle"),
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ module.exports = {
|
|||||||
label: "Database Adapters",
|
label: "Database Adapters",
|
||||||
link: { type: "doc", id: "reference/adapters/index" },
|
link: { type: "doc", id: "reference/adapters/index" },
|
||||||
items: [
|
items: [
|
||||||
|
{ type: "doc", id: "reference/adapter/edgedb/index" },
|
||||||
{ type: "doc", id: "reference/adapter/dgraph/index" },
|
{ type: "doc", id: "reference/adapter/dgraph/index" },
|
||||||
{ type: "doc", id: "reference/adapter/drizzle/index" },
|
{ type: "doc", id: "reference/adapter/drizzle/index" },
|
||||||
{ type: "doc", id: "reference/adapter/dynamodb/index" },
|
{ type: "doc", id: "reference/adapter/dynamodb/index" },
|
||||||
|
|||||||
5
docs/static/img/adapters/d1.svg
vendored
Normal file
5
docs/static/img/adapters/d1.svg
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="49" viewBox="0 0 48 49">
|
||||||
|
<path d="m18.63 37.418-9.645-12.9 9.592-12.533-1.852-2.527L5.917 23.595l-.015 1.808 10.86 14.542 1.868-2.527z" fill="rgb(243, 128, 32)"></path>
|
||||||
|
<path d="M21.997 6.503h-3.712l13.387 18.3-13.072 17.7h3.735L35.4 24.81 21.997 6.503z" fill="rgb(243, 128, 32)"></path>
|
||||||
|
<path d="M29.175 6.503h-3.758l13.598 18.082-13.598 17.918h3.765l12.908-17.01v-1.808L29.175 6.503z" fill="rgb(243, 128, 32)"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 497 B |
28
packages/adapter-d1/README.md
Normal file
28
packages/adapter-d1/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<p align="center">
|
||||||
|
<br/>
|
||||||
|
<a href="https://authjs.dev" target="_blank">
|
||||||
|
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" />
|
||||||
|
</a>
|
||||||
|
<a href="https://developers.cloudflare.com/d1/" target="_blank">
|
||||||
|
<img height="64px" src="https://authjs.dev/img/adapters/d1.svg"/>
|
||||||
|
</a>
|
||||||
|
<h3 align="center"><b>Cloudflare D1 Adapter</b> - NextAuth.js / Auth.js</a></h3>
|
||||||
|
<p align="center" style="align: center;">
|
||||||
|
<a href="https://npm.im/@auth/drizzle-adapter">
|
||||||
|
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
|
||||||
|
</a>
|
||||||
|
<a href="https://npm.im/@auth/d1-adapter">
|
||||||
|
<img alt="npm" src="https://img.shields.io/npm/v/@auth/d1-adapter?color=green&label=@auth/d1-adapter&style=flat-square">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmtrends.com/@auth/d1-adapter">
|
||||||
|
<img src="https://img.shields.io/npm/dm/@auth/d1-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
||||||
|
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/d1).
|
||||||
59
packages/adapter-d1/package.json
Normal file
59
packages/adapter-d1/package.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@auth/d1-adapter",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "A Cloudflare D1 adapter for Auth.js",
|
||||||
|
"homepage": "https://authjs.dev",
|
||||||
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/nextauthjs/next-auth/issues"
|
||||||
|
},
|
||||||
|
"author": "Josh Schlesser <josh@schlesser.dev>",
|
||||||
|
"contributors": [
|
||||||
|
"Thang Huu Vu <hi@thvu.dev>"
|
||||||
|
],
|
||||||
|
"license": "ISC",
|
||||||
|
"keywords": [
|
||||||
|
"next-auth",
|
||||||
|
"@auth",
|
||||||
|
"Auth.js",
|
||||||
|
"next.js",
|
||||||
|
"oauth",
|
||||||
|
"d1"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"import": "./index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"*.d.ts*",
|
||||||
|
"*.js",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"private": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@auth/adapter-test": "workspace:*",
|
||||||
|
"@auth/tsconfig": "workspace:*",
|
||||||
|
"@cloudflare/workers-types": "^4.20230321.0",
|
||||||
|
"@miniflare/d1": "^2.12.2",
|
||||||
|
"better-sqlite3": "^7.0.0",
|
||||||
|
"jest": "^29.3.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "@auth/adapter-test/jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
432
packages/adapter-d1/src/index.ts
Normal file
432
packages/adapter-d1/src/index.ts
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
/**
|
||||||
|
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
||||||
|
* <p style={{fontWeight: "normal"}}>An unofficial <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> adapter for Auth.js / NextAuth.js.</p>
|
||||||
|
* <a href="https://developers.cloudflare.com/d1/">
|
||||||
|
* <img style={{display: "block"}} src="/img/adapters/d1.svg" width="48" />
|
||||||
|
* </a>
|
||||||
|
* </div>
|
||||||
|
*
|
||||||
|
* ## Warning
|
||||||
|
* This adapter is not developed or maintained by Clouflare and they haven't declared the D1 api stable. The author will make an effort to keep this adapter up to date.
|
||||||
|
* The adapter is compatible with the D1 api as of March 22, 2023.
|
||||||
|
*
|
||||||
|
* ## Installation
|
||||||
|
*
|
||||||
|
* ```bash npm2yarn2pnpm
|
||||||
|
* npm install next-auth @auth/d1-adapter
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @module @auth/d1-adapter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { D1Database as WorkerDatabase } from "@cloudflare/workers-types"
|
||||||
|
import type { D1Database as MiniflareD1Database } from "@miniflare/d1"
|
||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
AdapterSession,
|
||||||
|
AdapterUser,
|
||||||
|
AdapterAccount,
|
||||||
|
VerificationToken as AdapterVerificationToken,
|
||||||
|
} from "@auth/core/adapters"
|
||||||
|
|
||||||
|
export { up } from "./migrations"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type @cloudflare/workers-types.D1Database | @miniflare/d1.D1Database
|
||||||
|
*/
|
||||||
|
export type D1Database = WorkerDatabase | MiniflareD1Database
|
||||||
|
|
||||||
|
// all the sqls
|
||||||
|
// USER
|
||||||
|
export const CREATE_USER_SQL = `INSERT INTO users (id, name, email, emailVerified, image) VALUES (?, ?, ?, ?, ?)`
|
||||||
|
export const GET_USER_BY_ID_SQL = `SELECT * FROM users WHERE id = ?`
|
||||||
|
export const GET_USER_BY_EMAIL_SQL = `SELECT * FROM users WHERE email = ?`
|
||||||
|
export const GET_USER_BY_ACCOUNTL_SQL = `
|
||||||
|
SELECT u.*
|
||||||
|
FROM users u JOIN accounts a ON a.userId = u.id
|
||||||
|
WHERE a.providerAccountId = ? AND a.provider = ?`
|
||||||
|
export const UPDATE_USER_BY_ID_SQL = `
|
||||||
|
UPDATE users
|
||||||
|
SET name = ?, email = ?, emailVerified = ?, image = ?
|
||||||
|
WHERE id = ? `
|
||||||
|
export const DELETE_USER_SQL = `DELETE FROM users WHERE id = ?`
|
||||||
|
|
||||||
|
// SESSION
|
||||||
|
export const CREATE_SESSION_SQL =
|
||||||
|
"INSERT INTO sessions (id, sessionToken, userId, expires) VALUES (?,?,?,?)"
|
||||||
|
export const GET_SESSION_BY_TOKEN_SQL = `
|
||||||
|
SELECT id, sessionToken, userId, expires
|
||||||
|
FROM sessions
|
||||||
|
WHERE sessionToken = ?`
|
||||||
|
export const UPDATE_SESSION_BY_SESSION_TOKEN_SQL = `UPDATE sessions SET expires = ? WHERE sessionToken = ?`
|
||||||
|
export const DELETE_SESSION_SQL = `DELETE FROM sessions WHERE sessionToken = ?`
|
||||||
|
export const DELETE_SESSION_BY_USER_ID_SQL = `DELETE FROM sessions WHERE userId = ?`
|
||||||
|
|
||||||
|
// ACCOUNT
|
||||||
|
export const CREATE_ACCOUNT_SQL = `
|
||||||
|
INSERT INTO accounts (
|
||||||
|
id, userId, type, provider,
|
||||||
|
providerAccountId, refresh_token, access_token,
|
||||||
|
expires_at, token_type, scope, id_token, session_state,
|
||||||
|
oauth_token, oauth_token_secret
|
||||||
|
)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
export const GET_ACCOUNT_BY_ID_SQL = `SELECT * FROM accounts WHERE id = ? `
|
||||||
|
export const GET_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = `SELECT * FROM accounts WHERE provider = ? AND providerAccountId = ?`
|
||||||
|
export const DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = `DELETE FROM accounts WHERE provider = ? AND providerAccountId = ?`
|
||||||
|
export const DELETE_ACCOUNT_BY_USER_ID_SQL = `DELETE FROM accounts WHERE userId = ?`
|
||||||
|
|
||||||
|
// VERIFICATION_TOKEN
|
||||||
|
export const GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL = `SELECT * FROM verification_tokens WHERE identifier = ? AND token = ?`
|
||||||
|
export const CREATE_VERIFICATION_TOKEN_SQL = `INSERT INTO verification_tokens (identifier, expires, token) VALUES (?,?,?)`
|
||||||
|
export const DELETE_VERIFICATION_TOKEN_SQL = `DELETE FROM verification_tokens WHERE identifier = ? and token = ?`
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
|
||||||
|
// isDate is borrowed from the supabase adapter, graciously
|
||||||
|
// depending on error messages ("Invalid Date") is always precarious, but probably fine for a built in native like Date
|
||||||
|
function isDate(date: any) {
|
||||||
|
return (
|
||||||
|
new Date(date).toString() !== "Invalid Date" && !isNaN(Date.parse(date))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// format is borrowed from the supabase adapter, graciously
|
||||||
|
function format<T>(obj: Record<string, any>): T {
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value === null) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
delete obj[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDate(value)) {
|
||||||
|
obj[key] = new Date(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj as T
|
||||||
|
}
|
||||||
|
|
||||||
|
// D1 doesnt like undefined, it wants null when calling bind
|
||||||
|
function cleanBindings(bindings: any[]) {
|
||||||
|
return bindings.map((e) => (e === undefined ? null : e))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRecord<RecordType>(
|
||||||
|
db: D1Database,
|
||||||
|
CREATE_SQL: string,
|
||||||
|
bindings: any[],
|
||||||
|
GET_SQL: string,
|
||||||
|
getBindings: any[]
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
bindings = cleanBindings(bindings)
|
||||||
|
await db
|
||||||
|
.prepare(CREATE_SQL)
|
||||||
|
.bind(...bindings)
|
||||||
|
.run()
|
||||||
|
return await getRecord<RecordType>(db, GET_SQL, getBindings)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.message, e.cause?.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecord<RecordType>(
|
||||||
|
db: D1Database,
|
||||||
|
SQL: string,
|
||||||
|
bindings: any[]
|
||||||
|
): Promise<RecordType | null> {
|
||||||
|
try {
|
||||||
|
bindings = cleanBindings(bindings)
|
||||||
|
const res: any = await db
|
||||||
|
.prepare(SQL)
|
||||||
|
.bind(...bindings)
|
||||||
|
.first()
|
||||||
|
if (res) {
|
||||||
|
return format<RecordType>(res)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.message, e.cause?.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRecord(
|
||||||
|
db: D1Database,
|
||||||
|
SQL: string,
|
||||||
|
bindings: any[]
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
bindings = cleanBindings(bindings)
|
||||||
|
return await db
|
||||||
|
.prepare(SQL)
|
||||||
|
.bind(...bindings)
|
||||||
|
.run()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.message, e.cause?.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRecord(
|
||||||
|
db: D1Database,
|
||||||
|
SQL: string,
|
||||||
|
bindings: any[]
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line no-useless-catch
|
||||||
|
try {
|
||||||
|
bindings = cleanBindings(bindings)
|
||||||
|
await db
|
||||||
|
.prepare(SQL)
|
||||||
|
.bind(...bindings)
|
||||||
|
.run()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.message, e.cause?.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* ## Setup
|
||||||
|
*
|
||||||
|
* This is the D1 Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||||
|
*
|
||||||
|
* ### Configure Auth.js
|
||||||
|
*
|
||||||
|
* ```javascript title="pages/api/auth/[...nextauth].js"
|
||||||
|
* import NextAuth from "next-auth"
|
||||||
|
* import { D1Adapter, up } from "@auth/d1-adapter"
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* // For more information on each option (and a full list of options) go to
|
||||||
|
* // https://authjs.dev/reference/configuration/auth-options
|
||||||
|
* export default NextAuth({
|
||||||
|
* // https://authjs.dev/reference/providers/
|
||||||
|
* providers: [],
|
||||||
|
* adapter: D1Adapter(env.db)
|
||||||
|
* ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Migrations
|
||||||
|
*
|
||||||
|
* Somewhere in the initialization of your application you need to run the `up(env.db)` function to create the tables in D1.
|
||||||
|
* It will create 4 tables if they don't already exist:
|
||||||
|
* `accounts`, `sessions`, `users`, `verification_tokens`.
|
||||||
|
*
|
||||||
|
* The table prefix "" is not configurable at this time.
|
||||||
|
*
|
||||||
|
* You can use something like the following to attempt the migration once each time your worker starts up. Running migrations more than once will not erase your existing tables.
|
||||||
|
* ```javascript
|
||||||
|
* import { up } from "@auth/d1-adapter"
|
||||||
|
*
|
||||||
|
* let migrated = false;
|
||||||
|
* async function migrationHandle({event, resolve}) {
|
||||||
|
* if(!migrated) {
|
||||||
|
* try {
|
||||||
|
* await up(event.platform.env.db)
|
||||||
|
* migrated = true
|
||||||
|
* } catch(e) {
|
||||||
|
* console.log(e.cause.message, e.message)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* return resolve(event)
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* You can also initialize your tables manually. Look in [init.ts](https://github.com/nextauthjs/next-auth/packages/adapter-d1/src/migrations/init.ts) for the relevant sql.
|
||||||
|
* Paste and run the SQL into your D1 dashboard query tool.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
export function D1Adapter(db: D1Database): Adapter {
|
||||||
|
// we need to run migrations if we dont have the right tables
|
||||||
|
|
||||||
|
return {
|
||||||
|
async createUser(user) {
|
||||||
|
const id: string = crypto.randomUUID()
|
||||||
|
const createBindings = [
|
||||||
|
id,
|
||||||
|
user.name,
|
||||||
|
user.email,
|
||||||
|
user.emailVerified?.toISOString(),
|
||||||
|
user.image,
|
||||||
|
]
|
||||||
|
const getBindings = [id]
|
||||||
|
|
||||||
|
const newUser = await createRecord<AdapterUser>(
|
||||||
|
db,
|
||||||
|
CREATE_USER_SQL,
|
||||||
|
createBindings,
|
||||||
|
GET_USER_BY_ID_SQL,
|
||||||
|
getBindings
|
||||||
|
)
|
||||||
|
if (newUser) return newUser
|
||||||
|
throw new Error("Error creating user: Cannot get user after creation.")
|
||||||
|
},
|
||||||
|
async getUser(id) {
|
||||||
|
return await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [id])
|
||||||
|
},
|
||||||
|
async getUserByEmail(email) {
|
||||||
|
return await getRecord<AdapterUser>(db, GET_USER_BY_EMAIL_SQL, [email])
|
||||||
|
},
|
||||||
|
async getUserByAccount({ providerAccountId, provider }) {
|
||||||
|
return await getRecord<AdapterUser>(db, GET_USER_BY_ACCOUNTL_SQL, [
|
||||||
|
providerAccountId,
|
||||||
|
provider,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
async updateUser(user) {
|
||||||
|
const params = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
|
||||||
|
user.id,
|
||||||
|
])
|
||||||
|
if (params) {
|
||||||
|
// copy any properties not in the update into the existing one and use that for bind params
|
||||||
|
// covers the scenario where the user arg doesnt have all of the current users properties
|
||||||
|
Object.assign(params, user)
|
||||||
|
const res = await updateRecord(db, UPDATE_USER_BY_ID_SQL, [
|
||||||
|
params.name,
|
||||||
|
params.email,
|
||||||
|
params.emailVerified?.toISOString(),
|
||||||
|
params.image,
|
||||||
|
params.id,
|
||||||
|
])
|
||||||
|
if (res.success) {
|
||||||
|
// we could probably just return
|
||||||
|
const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
|
||||||
|
params.id,
|
||||||
|
])
|
||||||
|
if (user) return user
|
||||||
|
throw new Error(
|
||||||
|
"Error updating user: Cannot get user after updating."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Error updating user: Failed to run the update SQL.")
|
||||||
|
},
|
||||||
|
async deleteUser(userId) {
|
||||||
|
// this should probably be in a db.batch but batch has problems right now in miniflare
|
||||||
|
// no multi line sql statements
|
||||||
|
await deleteRecord(db, DELETE_ACCOUNT_BY_USER_ID_SQL, [userId])
|
||||||
|
await deleteRecord(db, DELETE_SESSION_BY_USER_ID_SQL, [userId])
|
||||||
|
await deleteRecord(db, DELETE_USER_SQL, [userId])
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
async linkAccount(a) {
|
||||||
|
// convert user_id to userId and provider_account_id to providerAccountId
|
||||||
|
const id = crypto.randomUUID()
|
||||||
|
const createBindings = [
|
||||||
|
id,
|
||||||
|
a.userId,
|
||||||
|
a.type,
|
||||||
|
a.provider,
|
||||||
|
a.providerAccountId,
|
||||||
|
a.refresh_token,
|
||||||
|
a.access_token,
|
||||||
|
a.expires_at,
|
||||||
|
a.token_type,
|
||||||
|
a.scope,
|
||||||
|
a.id_token,
|
||||||
|
a.session_state,
|
||||||
|
a.oauth_token ?? null,
|
||||||
|
a.oauth_token_secret ?? null,
|
||||||
|
]
|
||||||
|
const getBindings = [id]
|
||||||
|
return await createRecord<AdapterAccount>(
|
||||||
|
db,
|
||||||
|
CREATE_ACCOUNT_SQL,
|
||||||
|
createBindings,
|
||||||
|
GET_ACCOUNT_BY_ID_SQL,
|
||||||
|
getBindings
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async unlinkAccount({ providerAccountId, provider }) {
|
||||||
|
await deleteRecord(
|
||||||
|
db,
|
||||||
|
DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL,
|
||||||
|
[provider, providerAccountId]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async createSession({ sessionToken, userId, expires }) {
|
||||||
|
const id = crypto.randomUUID()
|
||||||
|
const createBindings = [id, sessionToken, userId, expires.toISOString()]
|
||||||
|
const getBindings = [sessionToken]
|
||||||
|
const session = await createRecord<AdapterSession>(
|
||||||
|
db,
|
||||||
|
CREATE_SESSION_SQL,
|
||||||
|
createBindings,
|
||||||
|
GET_SESSION_BY_TOKEN_SQL,
|
||||||
|
getBindings
|
||||||
|
)
|
||||||
|
if (session) return session
|
||||||
|
throw new Error(`Couldn't create session`)
|
||||||
|
},
|
||||||
|
async getSessionAndUser(sessionToken) {
|
||||||
|
const session: any = await getRecord<AdapterSession>(
|
||||||
|
db,
|
||||||
|
GET_SESSION_BY_TOKEN_SQL,
|
||||||
|
[sessionToken]
|
||||||
|
)
|
||||||
|
// no session? no user!
|
||||||
|
if (session === null) return null
|
||||||
|
|
||||||
|
// this shouldnt happen, but just in case
|
||||||
|
const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
|
||||||
|
session.userId,
|
||||||
|
])
|
||||||
|
if (user === null) return null
|
||||||
|
|
||||||
|
return { session, user }
|
||||||
|
},
|
||||||
|
async updateSession({ sessionToken, expires }) {
|
||||||
|
// kinda strange that we have to deal with an undefined expires,
|
||||||
|
// we dont have any policy to enforce, lets just expire it now.
|
||||||
|
if (expires === undefined) {
|
||||||
|
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken])
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const session = await getRecord<AdapterSession>(
|
||||||
|
db,
|
||||||
|
GET_SESSION_BY_TOKEN_SQL,
|
||||||
|
[sessionToken]
|
||||||
|
)
|
||||||
|
if (!session) return null
|
||||||
|
session.expires = expires
|
||||||
|
await updateRecord(db, UPDATE_SESSION_BY_SESSION_TOKEN_SQL, [
|
||||||
|
expires?.toISOString(),
|
||||||
|
sessionToken,
|
||||||
|
])
|
||||||
|
return await db
|
||||||
|
.prepare(UPDATE_SESSION_BY_SESSION_TOKEN_SQL)
|
||||||
|
.bind(expires?.toISOString(), sessionToken)
|
||||||
|
.first()
|
||||||
|
},
|
||||||
|
async deleteSession(sessionToken) {
|
||||||
|
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken])
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
async createVerificationToken({ identifier, expires, token }) {
|
||||||
|
return await createRecord(
|
||||||
|
db,
|
||||||
|
CREATE_VERIFICATION_TOKEN_SQL,
|
||||||
|
[identifier, expires.toISOString(), token],
|
||||||
|
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
|
||||||
|
[identifier, token]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async useVerificationToken({ identifier, token }) {
|
||||||
|
const verificationToken = await getRecord<AdapterVerificationToken>(
|
||||||
|
db,
|
||||||
|
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
|
||||||
|
[identifier, token]
|
||||||
|
)
|
||||||
|
if (!verificationToken) return null
|
||||||
|
await deleteRecord(db, DELETE_VERIFICATION_TOKEN_SQL, [identifier, token])
|
||||||
|
return verificationToken
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/adapter-d1/src/migrations.ts
Normal file
66
packages/adapter-d1/src/migrations.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { D1Database } from "."
|
||||||
|
|
||||||
|
export const upSQLStatements = [
|
||||||
|
`CREATE TABLE IF NOT EXISTS "accounts" (
|
||||||
|
"id" text NOT NULL,
|
||||||
|
"userId" text NOT NULL DEFAULT NULL,
|
||||||
|
"type" text NOT NULL DEFAULT NULL,
|
||||||
|
"provider" text NOT NULL DEFAULT NULL,
|
||||||
|
"providerAccountId" text NOT NULL DEFAULT NULL,
|
||||||
|
"refresh_token" text DEFAULT NULL,
|
||||||
|
"access_token" text DEFAULT NULL,
|
||||||
|
"expires_at" number DEFAULT NULL,
|
||||||
|
"token_type" text DEFAULT NULL,
|
||||||
|
"scope" text DEFAULT NULL,
|
||||||
|
"id_token" text DEFAULT NULL,
|
||||||
|
"session_state" text DEFAULT NULL,
|
||||||
|
"oauth_token_secret" text DEFAULT NULL,
|
||||||
|
"oauth_token" text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS "sessions" (
|
||||||
|
"id" text NOT NULL,
|
||||||
|
"sessionToken" text NOT NULL,
|
||||||
|
"userId" text NOT NULL DEFAULT NULL,
|
||||||
|
"expires" datetime NOT NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (sessionToken)
|
||||||
|
);`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS "users" (
|
||||||
|
"id" text NOT NULL DEFAULT '',
|
||||||
|
"name" text DEFAULT NULL,
|
||||||
|
"email" text DEFAULT NULL,
|
||||||
|
"emailVerified" datetime DEFAULT NULL,
|
||||||
|
"image" text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS "verification_tokens" (
|
||||||
|
"identifier" text NOT NULL,
|
||||||
|
"token" text NOT NULL DEFAULT NULL,
|
||||||
|
"expires" datetime NOT NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (token)
|
||||||
|
);`,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const down = [
|
||||||
|
`DROP TABLE IF EXISTS "accounts";`,
|
||||||
|
`DROP TABLE IF EXISTS "sessions";`,
|
||||||
|
`DROP TABLE IF EXISTS "users";`,
|
||||||
|
`DROP TABLE IF EXISTS "verification_token";`,
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param db
|
||||||
|
*/
|
||||||
|
async function up(db: D1Database) {
|
||||||
|
// run the migration
|
||||||
|
upSQLStatements.forEach(async (sql) => {
|
||||||
|
try {
|
||||||
|
const res = await db.prepare(sql).run()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.cause?.message, e.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { up }
|
||||||
56
packages/adapter-d1/tests/index.test.ts
Normal file
56
packages/adapter-d1/tests/index.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
D1Adapter,
|
||||||
|
up,
|
||||||
|
getRecord,
|
||||||
|
GET_USER_BY_ID_SQL,
|
||||||
|
GET_SESSION_BY_TOKEN_SQL,
|
||||||
|
GET_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL,
|
||||||
|
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
|
||||||
|
} from "../src"
|
||||||
|
import {
|
||||||
|
AdapterSession,
|
||||||
|
AdapterUser,
|
||||||
|
AdapterAccount,
|
||||||
|
} from "@auth/core/adapters"
|
||||||
|
import { D1Database, D1DatabaseAPI } from "@miniflare/d1"
|
||||||
|
import { runBasicTests } from "@auth/adapter-test"
|
||||||
|
import Database from "better-sqlite3"
|
||||||
|
|
||||||
|
globalThis.crypto ??= require("node:crypto").webcrypto
|
||||||
|
|
||||||
|
if (process.env.CI) {
|
||||||
|
// TODO: Fix this
|
||||||
|
test('Skipping D1Adapter tests in CI because of "Error: Must use import to load ES Module: next-auth/node_modules/.pnpm/undici@5.20.0/node_modules/undici/lib/llhttp/llhttp.wasm" errors. Should revisit', () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqliteDB = new Database(":memory:")
|
||||||
|
let db = new D1Database(new D1DatabaseAPI(sqliteDB as any))
|
||||||
|
let adapter = D1Adapter(db)
|
||||||
|
|
||||||
|
// put stuff here if we need some async init
|
||||||
|
beforeAll(async () => await up(db))
|
||||||
|
runBasicTests({
|
||||||
|
adapter,
|
||||||
|
db: {
|
||||||
|
user: async (id) =>
|
||||||
|
await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [id]),
|
||||||
|
session: async (sessionToken) =>
|
||||||
|
await getRecord<AdapterSession>(db, GET_SESSION_BY_TOKEN_SQL, [
|
||||||
|
sessionToken,
|
||||||
|
]),
|
||||||
|
account: async ({ provider, providerAccountId }) =>
|
||||||
|
await getRecord<AdapterAccount>(
|
||||||
|
db,
|
||||||
|
GET_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL,
|
||||||
|
[provider, providerAccountId]
|
||||||
|
),
|
||||||
|
verificationToken: async ({ identifier, token }) =>
|
||||||
|
await getRecord(db, GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL, [
|
||||||
|
identifier,
|
||||||
|
token,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
})
|
||||||
20
packages/adapter-d1/tsconfig.json
Normal file
20
packages/adapter-d1/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "@auth/tsconfig/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": ".",
|
||||||
|
"rootDir": "src",
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"stripInternal": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["*.js", "*.d.ts"]
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
"url": "https://github.com/nextauthjs/next-auth/issues"
|
"url": "https://github.com/nextauthjs/next-auth/issues"
|
||||||
},
|
},
|
||||||
"author": "Bruno Crosier",
|
"author": "Bruno Crosier",
|
||||||
|
"contributors": [
|
||||||
|
"Thang Huu Vu <hi@thvu.dev>"
|
||||||
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./index.d.ts",
|
"types": "./index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -37,9 +40,11 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "workspace:*"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"edgedb": "^1.0.1",
|
"edgedb": "^1.0.1"
|
||||||
"@auth/core": "^0.3.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@auth/adapter-test": "workspace:^0.0.0",
|
"@auth/adapter-test": "workspace:^0.0.0",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"Balázs Orbán <info@balazsorban.com>",
|
"Balázs Orbán <info@balazsorban.com>",
|
||||||
"Nico Domino <yo@ndo.dev>",
|
"Nico Domino <yo@ndo.dev>",
|
||||||
"Lluis Agusti <hi@llu.lu>",
|
"Lluis Agusti <hi@llu.lu>",
|
||||||
"Thang Huu Vu <thvu@hey.com>"
|
"Thang Huu Vu <hi@thvu.dev>"
|
||||||
],
|
],
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"module": "index.js",
|
"module": "index.js",
|
||||||
|
|||||||
1832
pnpm-lock.yaml
generated
1832
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user