@auth/d1-adapter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +28 -0
- package/index.d.ts +107 -0
- package/index.d.ts.map +1 -0
- package/index.js +346 -0
- package/migrations.d.ts +10 -0
- package/migrations.d.ts.map +1 -0
- package/migrations.js +62 -0
- package/package.json +59 -0
- package/src/index.ts +432 -0
- package/src/migrations.ts +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2023, Balázs Orbán
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<br/>
|
|
3
|
+
<a href="https://authjs.dev" target="_blank">
|
|
4
|
+
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" />
|
|
5
|
+
</a>
|
|
6
|
+
<a href="https://developers.cloudflare.com/d1/" target="_blank">
|
|
7
|
+
<img height="64px" src="https://authjs.dev/img/adapters/d1.svg"/>
|
|
8
|
+
</a>
|
|
9
|
+
<h3 align="center"><b>Cloudflare D1 Adapter</b> - NextAuth.js / Auth.js</a></h3>
|
|
10
|
+
<p align="center" style="align: center;">
|
|
11
|
+
<a href="https://npm.im/@auth/drizzle-adapter">
|
|
12
|
+
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://npm.im/@auth/d1-adapter">
|
|
15
|
+
<img alt="npm" src="https://img.shields.io/npm/v/@auth/d1-adapter?color=green&label=@auth/d1-adapter&style=flat-square">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://www.npmtrends.com/@auth/d1-adapter">
|
|
18
|
+
<img src="https://img.shields.io/npm/dm/@auth/d1-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
|
21
|
+
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />
|
|
22
|
+
</a>
|
|
23
|
+
</p>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/d1).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>An unofficial <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://developers.cloudflare.com/d1/">
|
|
5
|
+
* <img style={{display: "block"}} src="/img/adapters/d1.svg" width="48" />
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Warning
|
|
10
|
+
* 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.
|
|
11
|
+
* The adapter is compatible with the D1 api as of March 22, 2023.
|
|
12
|
+
*
|
|
13
|
+
* ## Installation
|
|
14
|
+
*
|
|
15
|
+
* ```bash npm2yarn2pnpm
|
|
16
|
+
* npm install next-auth @auth/d1-adapter
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module @auth/d1-adapter
|
|
20
|
+
*/
|
|
21
|
+
import type { D1Database as WorkerDatabase } from "@cloudflare/workers-types";
|
|
22
|
+
import type { D1Database as MiniflareD1Database } from "@miniflare/d1";
|
|
23
|
+
import type { Adapter } from "@auth/core/adapters";
|
|
24
|
+
export { up } from "./migrations";
|
|
25
|
+
/**
|
|
26
|
+
* @type @cloudflare/workers-types.D1Database | @miniflare/d1.D1Database
|
|
27
|
+
*/
|
|
28
|
+
export type D1Database = WorkerDatabase | MiniflareD1Database;
|
|
29
|
+
export declare const CREATE_USER_SQL = "INSERT INTO users (id, name, email, emailVerified, image) VALUES (?, ?, ?, ?, ?)";
|
|
30
|
+
export declare const GET_USER_BY_ID_SQL = "SELECT * FROM users WHERE id = ?";
|
|
31
|
+
export declare const GET_USER_BY_EMAIL_SQL = "SELECT * FROM users WHERE email = ?";
|
|
32
|
+
export declare const GET_USER_BY_ACCOUNTL_SQL = "\n SELECT u.*\n FROM users u JOIN accounts a ON a.userId = u.id\n WHERE a.providerAccountId = ? AND a.provider = ?";
|
|
33
|
+
export declare const UPDATE_USER_BY_ID_SQL = "\n UPDATE users \n SET name = ?, email = ?, emailVerified = ?, image = ?\n WHERE id = ? ";
|
|
34
|
+
export declare const DELETE_USER_SQL = "DELETE FROM users WHERE id = ?";
|
|
35
|
+
export declare const CREATE_SESSION_SQL = "INSERT INTO sessions (id, sessionToken, userId, expires) VALUES (?,?,?,?)";
|
|
36
|
+
export declare const GET_SESSION_BY_TOKEN_SQL = "\n SELECT id, sessionToken, userId, expires\n FROM sessions\n WHERE sessionToken = ?";
|
|
37
|
+
export declare const UPDATE_SESSION_BY_SESSION_TOKEN_SQL = "UPDATE sessions SET expires = ? WHERE sessionToken = ?";
|
|
38
|
+
export declare const DELETE_SESSION_SQL = "DELETE FROM sessions WHERE sessionToken = ?";
|
|
39
|
+
export declare const DELETE_SESSION_BY_USER_ID_SQL = "DELETE FROM sessions WHERE userId = ?";
|
|
40
|
+
export declare const CREATE_ACCOUNT_SQL = "\n INSERT INTO accounts (\n id, userId, type, provider, \n providerAccountId, refresh_token, access_token, \n expires_at, token_type, scope, id_token, session_state,\n oauth_token, oauth_token_secret\n ) \n VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
|
|
41
|
+
export declare const GET_ACCOUNT_BY_ID_SQL = "SELECT * FROM accounts WHERE id = ? ";
|
|
42
|
+
export declare const GET_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = "SELECT * FROM accounts WHERE provider = ? AND providerAccountId = ?";
|
|
43
|
+
export declare const DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = "DELETE FROM accounts WHERE provider = ? AND providerAccountId = ?";
|
|
44
|
+
export declare const DELETE_ACCOUNT_BY_USER_ID_SQL = "DELETE FROM accounts WHERE userId = ?";
|
|
45
|
+
export declare const GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL = "SELECT * FROM verification_tokens WHERE identifier = ? AND token = ?";
|
|
46
|
+
export declare const CREATE_VERIFICATION_TOKEN_SQL = "INSERT INTO verification_tokens (identifier, expires, token) VALUES (?,?,?)";
|
|
47
|
+
export declare const DELETE_VERIFICATION_TOKEN_SQL = "DELETE FROM verification_tokens WHERE identifier = ? and token = ?";
|
|
48
|
+
export declare function createRecord<RecordType>(db: D1Database, CREATE_SQL: string, bindings: any[], GET_SQL: string, getBindings: any[]): Promise<RecordType | null>;
|
|
49
|
+
export declare function getRecord<RecordType>(db: D1Database, SQL: string, bindings: any[]): Promise<RecordType | null>;
|
|
50
|
+
export declare function updateRecord(db: D1Database, SQL: string, bindings: any[]): Promise<import("@miniflare/d1").D1Result<unknown> | import("@cloudflare/workers-types").D1Result<unknown>>;
|
|
51
|
+
export declare function deleteRecord(db: D1Database, SQL: string, bindings: any[]): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
* ## Setup
|
|
55
|
+
*
|
|
56
|
+
* 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.
|
|
57
|
+
*
|
|
58
|
+
* ### Configure Auth.js
|
|
59
|
+
*
|
|
60
|
+
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
61
|
+
* import NextAuth from "next-auth"
|
|
62
|
+
* import { D1Adapter, up } from "@auth/d1-adapter"
|
|
63
|
+
*
|
|
64
|
+
*
|
|
65
|
+
* // For more information on each option (and a full list of options) go to
|
|
66
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
67
|
+
* export default NextAuth({
|
|
68
|
+
* // https://authjs.dev/reference/providers/
|
|
69
|
+
* providers: [],
|
|
70
|
+
* adapter: D1Adapter(env.db)
|
|
71
|
+
* ...
|
|
72
|
+
* })
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* ### Migrations
|
|
76
|
+
*
|
|
77
|
+
* Somewhere in the initialization of your application you need to run the `up(env.db)` function to create the tables in D1.
|
|
78
|
+
* It will create 4 tables if they don't already exist:
|
|
79
|
+
* `accounts`, `sessions`, `users`, `verification_tokens`.
|
|
80
|
+
*
|
|
81
|
+
* The table prefix "" is not configurable at this time.
|
|
82
|
+
*
|
|
83
|
+
* 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.
|
|
84
|
+
* ```javascript
|
|
85
|
+
* import { up } from "@auth/d1-adapter"
|
|
86
|
+
*
|
|
87
|
+
* let migrated = false;
|
|
88
|
+
* async function migrationHandle({event, resolve}) {
|
|
89
|
+
* if(!migrated) {
|
|
90
|
+
* try {
|
|
91
|
+
* await up(event.platform.env.db)
|
|
92
|
+
* migrated = true
|
|
93
|
+
* } catch(e) {
|
|
94
|
+
* console.log(e.cause.message, e.message)
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* return resolve(event)
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
*
|
|
102
|
+
* 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.
|
|
103
|
+
* Paste and run the SQL into your D1 dashboard query tool.
|
|
104
|
+
*
|
|
105
|
+
**/
|
|
106
|
+
export declare function D1Adapter(db: D1Database): Adapter;
|
|
107
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC7E,OAAO,KAAK,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACtE,OAAO,KAAK,EACV,OAAO,EAKR,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAEjC;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,mBAAmB,CAAA;AAI7D,eAAO,MAAM,eAAe,qFAAqF,CAAA;AACjH,eAAO,MAAM,kBAAkB,qCAAqC,CAAA;AACpE,eAAO,MAAM,qBAAqB,wCAAwC,CAAA;AAC1E,eAAO,MAAM,wBAAwB,0HAGc,CAAA;AACnD,eAAO,MAAM,qBAAqB,gGAGlB,CAAA;AAChB,eAAO,MAAM,eAAe,mCAAmC,CAAA;AAG/D,eAAO,MAAM,kBAAkB,8EAC8C,CAAA;AAC7E,eAAO,MAAM,wBAAwB,4FAGZ,CAAA;AACzB,eAAO,MAAM,mCAAmC,2DAA2D,CAAA;AAC3G,eAAO,MAAM,kBAAkB,gDAAgD,CAAA;AAC/E,eAAO,MAAM,6BAA6B,0CAA0C,CAAA;AAGpF,eAAO,MAAM,kBAAkB,uQAOQ,CAAA;AACvC,eAAO,MAAM,qBAAqB,yCAAyC,CAAA;AAC3E,eAAO,MAAM,mDAAmD,wEAAwE,CAAA;AACxI,eAAO,MAAM,sDAAsD,sEAAsE,CAAA;AACzI,eAAO,MAAM,6BAA6B,0CAA0C,CAAA;AAGpF,eAAO,MAAM,kDAAkD,yEAAyE,CAAA;AACxI,eAAO,MAAM,6BAA6B,gFAAgF,CAAA;AAC1H,eAAO,MAAM,6BAA6B,uEAAuE,CAAA;AAiCjH,wBAAsB,YAAY,CAAC,UAAU,EAC3C,EAAE,EAAE,UAAU,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,EAAE,EACf,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,GAAG,EAAE,8BAanB;AAED,wBAAsB,SAAS,CAAC,UAAU,EACxC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,GAAG,EAAE,GACd,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAgB5B;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,GAAG,EAAE,8GAYhB;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,GAAG,EAAE,iBAahB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAqDI;AACJ,wBAAgB,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CA0LjD"}
|
package/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>An unofficial <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://developers.cloudflare.com/d1/">
|
|
5
|
+
* <img style={{display: "block"}} src="/img/adapters/d1.svg" width="48" />
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Warning
|
|
10
|
+
* 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.
|
|
11
|
+
* The adapter is compatible with the D1 api as of March 22, 2023.
|
|
12
|
+
*
|
|
13
|
+
* ## Installation
|
|
14
|
+
*
|
|
15
|
+
* ```bash npm2yarn2pnpm
|
|
16
|
+
* npm install next-auth @auth/d1-adapter
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module @auth/d1-adapter
|
|
20
|
+
*/
|
|
21
|
+
export { up } from "./migrations";
|
|
22
|
+
// all the sqls
|
|
23
|
+
// USER
|
|
24
|
+
export const CREATE_USER_SQL = `INSERT INTO users (id, name, email, emailVerified, image) VALUES (?, ?, ?, ?, ?)`;
|
|
25
|
+
export const GET_USER_BY_ID_SQL = `SELECT * FROM users WHERE id = ?`;
|
|
26
|
+
export const GET_USER_BY_EMAIL_SQL = `SELECT * FROM users WHERE email = ?`;
|
|
27
|
+
export const GET_USER_BY_ACCOUNTL_SQL = `
|
|
28
|
+
SELECT u.*
|
|
29
|
+
FROM users u JOIN accounts a ON a.userId = u.id
|
|
30
|
+
WHERE a.providerAccountId = ? AND a.provider = ?`;
|
|
31
|
+
export const UPDATE_USER_BY_ID_SQL = `
|
|
32
|
+
UPDATE users
|
|
33
|
+
SET name = ?, email = ?, emailVerified = ?, image = ?
|
|
34
|
+
WHERE id = ? `;
|
|
35
|
+
export const DELETE_USER_SQL = `DELETE FROM users WHERE id = ?`;
|
|
36
|
+
// SESSION
|
|
37
|
+
export const CREATE_SESSION_SQL = "INSERT INTO sessions (id, sessionToken, userId, expires) VALUES (?,?,?,?)";
|
|
38
|
+
export const GET_SESSION_BY_TOKEN_SQL = `
|
|
39
|
+
SELECT id, sessionToken, userId, expires
|
|
40
|
+
FROM sessions
|
|
41
|
+
WHERE sessionToken = ?`;
|
|
42
|
+
export const UPDATE_SESSION_BY_SESSION_TOKEN_SQL = `UPDATE sessions SET expires = ? WHERE sessionToken = ?`;
|
|
43
|
+
export const DELETE_SESSION_SQL = `DELETE FROM sessions WHERE sessionToken = ?`;
|
|
44
|
+
export const DELETE_SESSION_BY_USER_ID_SQL = `DELETE FROM sessions WHERE userId = ?`;
|
|
45
|
+
// ACCOUNT
|
|
46
|
+
export const CREATE_ACCOUNT_SQL = `
|
|
47
|
+
INSERT INTO accounts (
|
|
48
|
+
id, userId, type, provider,
|
|
49
|
+
providerAccountId, refresh_token, access_token,
|
|
50
|
+
expires_at, token_type, scope, id_token, session_state,
|
|
51
|
+
oauth_token, oauth_token_secret
|
|
52
|
+
)
|
|
53
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
|
|
54
|
+
export const GET_ACCOUNT_BY_ID_SQL = `SELECT * FROM accounts WHERE id = ? `;
|
|
55
|
+
export const GET_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = `SELECT * FROM accounts WHERE provider = ? AND providerAccountId = ?`;
|
|
56
|
+
export const DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = `DELETE FROM accounts WHERE provider = ? AND providerAccountId = ?`;
|
|
57
|
+
export const DELETE_ACCOUNT_BY_USER_ID_SQL = `DELETE FROM accounts WHERE userId = ?`;
|
|
58
|
+
// VERIFICATION_TOKEN
|
|
59
|
+
export const GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL = `SELECT * FROM verification_tokens WHERE identifier = ? AND token = ?`;
|
|
60
|
+
export const CREATE_VERIFICATION_TOKEN_SQL = `INSERT INTO verification_tokens (identifier, expires, token) VALUES (?,?,?)`;
|
|
61
|
+
export const DELETE_VERIFICATION_TOKEN_SQL = `DELETE FROM verification_tokens WHERE identifier = ? and token = ?`;
|
|
62
|
+
// helper functions
|
|
63
|
+
// isDate is borrowed from the supabase adapter, graciously
|
|
64
|
+
// depending on error messages ("Invalid Date") is always precarious, but probably fine for a built in native like Date
|
|
65
|
+
function isDate(date) {
|
|
66
|
+
return (new Date(date).toString() !== "Invalid Date" && !isNaN(Date.parse(date)));
|
|
67
|
+
}
|
|
68
|
+
// format is borrowed from the supabase adapter, graciously
|
|
69
|
+
function format(obj) {
|
|
70
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
71
|
+
if (value === null) {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
73
|
+
delete obj[key];
|
|
74
|
+
}
|
|
75
|
+
if (isDate(value)) {
|
|
76
|
+
obj[key] = new Date(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return obj;
|
|
80
|
+
}
|
|
81
|
+
// D1 doesnt like undefined, it wants null when calling bind
|
|
82
|
+
function cleanBindings(bindings) {
|
|
83
|
+
return bindings.map((e) => (e === undefined ? null : e));
|
|
84
|
+
}
|
|
85
|
+
export async function createRecord(db, CREATE_SQL, bindings, GET_SQL, getBindings) {
|
|
86
|
+
try {
|
|
87
|
+
bindings = cleanBindings(bindings);
|
|
88
|
+
await db
|
|
89
|
+
.prepare(CREATE_SQL)
|
|
90
|
+
.bind(...bindings)
|
|
91
|
+
.run();
|
|
92
|
+
return await getRecord(db, GET_SQL, getBindings);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.error(e.message, e.cause?.message);
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function getRecord(db, SQL, bindings) {
|
|
100
|
+
try {
|
|
101
|
+
bindings = cleanBindings(bindings);
|
|
102
|
+
const res = await db
|
|
103
|
+
.prepare(SQL)
|
|
104
|
+
.bind(...bindings)
|
|
105
|
+
.first();
|
|
106
|
+
if (res) {
|
|
107
|
+
return format(res);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
console.error(e.message, e.cause?.message);
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function updateRecord(db, SQL, bindings) {
|
|
119
|
+
try {
|
|
120
|
+
bindings = cleanBindings(bindings);
|
|
121
|
+
return await db
|
|
122
|
+
.prepare(SQL)
|
|
123
|
+
.bind(...bindings)
|
|
124
|
+
.run();
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
console.error(e.message, e.cause?.message);
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function deleteRecord(db, SQL, bindings) {
|
|
132
|
+
// eslint-disable-next-line no-useless-catch
|
|
133
|
+
try {
|
|
134
|
+
bindings = cleanBindings(bindings);
|
|
135
|
+
await db
|
|
136
|
+
.prepare(SQL)
|
|
137
|
+
.bind(...bindings)
|
|
138
|
+
.run();
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.error(e.message, e.cause?.message);
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
*
|
|
147
|
+
* ## Setup
|
|
148
|
+
*
|
|
149
|
+
* 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.
|
|
150
|
+
*
|
|
151
|
+
* ### Configure Auth.js
|
|
152
|
+
*
|
|
153
|
+
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
154
|
+
* import NextAuth from "next-auth"
|
|
155
|
+
* import { D1Adapter, up } from "@auth/d1-adapter"
|
|
156
|
+
*
|
|
157
|
+
*
|
|
158
|
+
* // For more information on each option (and a full list of options) go to
|
|
159
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
160
|
+
* export default NextAuth({
|
|
161
|
+
* // https://authjs.dev/reference/providers/
|
|
162
|
+
* providers: [],
|
|
163
|
+
* adapter: D1Adapter(env.db)
|
|
164
|
+
* ...
|
|
165
|
+
* })
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* ### Migrations
|
|
169
|
+
*
|
|
170
|
+
* Somewhere in the initialization of your application you need to run the `up(env.db)` function to create the tables in D1.
|
|
171
|
+
* It will create 4 tables if they don't already exist:
|
|
172
|
+
* `accounts`, `sessions`, `users`, `verification_tokens`.
|
|
173
|
+
*
|
|
174
|
+
* The table prefix "" is not configurable at this time.
|
|
175
|
+
*
|
|
176
|
+
* 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.
|
|
177
|
+
* ```javascript
|
|
178
|
+
* import { up } from "@auth/d1-adapter"
|
|
179
|
+
*
|
|
180
|
+
* let migrated = false;
|
|
181
|
+
* async function migrationHandle({event, resolve}) {
|
|
182
|
+
* if(!migrated) {
|
|
183
|
+
* try {
|
|
184
|
+
* await up(event.platform.env.db)
|
|
185
|
+
* migrated = true
|
|
186
|
+
* } catch(e) {
|
|
187
|
+
* console.log(e.cause.message, e.message)
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
* return resolve(event)
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
*
|
|
195
|
+
* 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.
|
|
196
|
+
* Paste and run the SQL into your D1 dashboard query tool.
|
|
197
|
+
*
|
|
198
|
+
**/
|
|
199
|
+
export function D1Adapter(db) {
|
|
200
|
+
// we need to run migrations if we dont have the right tables
|
|
201
|
+
return {
|
|
202
|
+
async createUser(user) {
|
|
203
|
+
const id = crypto.randomUUID();
|
|
204
|
+
const createBindings = [
|
|
205
|
+
id,
|
|
206
|
+
user.name,
|
|
207
|
+
user.email,
|
|
208
|
+
user.emailVerified?.toISOString(),
|
|
209
|
+
user.image,
|
|
210
|
+
];
|
|
211
|
+
const getBindings = [id];
|
|
212
|
+
const newUser = await createRecord(db, CREATE_USER_SQL, createBindings, GET_USER_BY_ID_SQL, getBindings);
|
|
213
|
+
if (newUser)
|
|
214
|
+
return newUser;
|
|
215
|
+
throw new Error("Error creating user: Cannot get user after creation.");
|
|
216
|
+
},
|
|
217
|
+
async getUser(id) {
|
|
218
|
+
return await getRecord(db, GET_USER_BY_ID_SQL, [id]);
|
|
219
|
+
},
|
|
220
|
+
async getUserByEmail(email) {
|
|
221
|
+
return await getRecord(db, GET_USER_BY_EMAIL_SQL, [email]);
|
|
222
|
+
},
|
|
223
|
+
async getUserByAccount({ providerAccountId, provider }) {
|
|
224
|
+
return await getRecord(db, GET_USER_BY_ACCOUNTL_SQL, [
|
|
225
|
+
providerAccountId,
|
|
226
|
+
provider,
|
|
227
|
+
]);
|
|
228
|
+
},
|
|
229
|
+
async updateUser(user) {
|
|
230
|
+
const params = await getRecord(db, GET_USER_BY_ID_SQL, [
|
|
231
|
+
user.id,
|
|
232
|
+
]);
|
|
233
|
+
if (params) {
|
|
234
|
+
// copy any properties not in the update into the existing one and use that for bind params
|
|
235
|
+
// covers the scenario where the user arg doesnt have all of the current users properties
|
|
236
|
+
Object.assign(params, user);
|
|
237
|
+
const res = await updateRecord(db, UPDATE_USER_BY_ID_SQL, [
|
|
238
|
+
params.name,
|
|
239
|
+
params.email,
|
|
240
|
+
params.emailVerified?.toISOString(),
|
|
241
|
+
params.image,
|
|
242
|
+
params.id,
|
|
243
|
+
]);
|
|
244
|
+
if (res.success) {
|
|
245
|
+
// we could probably just return
|
|
246
|
+
const user = await getRecord(db, GET_USER_BY_ID_SQL, [
|
|
247
|
+
params.id,
|
|
248
|
+
]);
|
|
249
|
+
if (user)
|
|
250
|
+
return user;
|
|
251
|
+
throw new Error("Error updating user: Cannot get user after updating.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw new Error("Error updating user: Failed to run the update SQL.");
|
|
255
|
+
},
|
|
256
|
+
async deleteUser(userId) {
|
|
257
|
+
// this should probably be in a db.batch but batch has problems right now in miniflare
|
|
258
|
+
// no multi line sql statements
|
|
259
|
+
await deleteRecord(db, DELETE_ACCOUNT_BY_USER_ID_SQL, [userId]);
|
|
260
|
+
await deleteRecord(db, DELETE_SESSION_BY_USER_ID_SQL, [userId]);
|
|
261
|
+
await deleteRecord(db, DELETE_USER_SQL, [userId]);
|
|
262
|
+
return null;
|
|
263
|
+
},
|
|
264
|
+
async linkAccount(a) {
|
|
265
|
+
// convert user_id to userId and provider_account_id to providerAccountId
|
|
266
|
+
const id = crypto.randomUUID();
|
|
267
|
+
const createBindings = [
|
|
268
|
+
id,
|
|
269
|
+
a.userId,
|
|
270
|
+
a.type,
|
|
271
|
+
a.provider,
|
|
272
|
+
a.providerAccountId,
|
|
273
|
+
a.refresh_token,
|
|
274
|
+
a.access_token,
|
|
275
|
+
a.expires_at,
|
|
276
|
+
a.token_type,
|
|
277
|
+
a.scope,
|
|
278
|
+
a.id_token,
|
|
279
|
+
a.session_state,
|
|
280
|
+
a.oauth_token ?? null,
|
|
281
|
+
a.oauth_token_secret ?? null,
|
|
282
|
+
];
|
|
283
|
+
const getBindings = [id];
|
|
284
|
+
return await createRecord(db, CREATE_ACCOUNT_SQL, createBindings, GET_ACCOUNT_BY_ID_SQL, getBindings);
|
|
285
|
+
},
|
|
286
|
+
async unlinkAccount({ providerAccountId, provider }) {
|
|
287
|
+
await deleteRecord(db, DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL, [provider, providerAccountId]);
|
|
288
|
+
},
|
|
289
|
+
async createSession({ sessionToken, userId, expires }) {
|
|
290
|
+
const id = crypto.randomUUID();
|
|
291
|
+
const createBindings = [id, sessionToken, userId, expires.toISOString()];
|
|
292
|
+
const getBindings = [sessionToken];
|
|
293
|
+
const session = await createRecord(db, CREATE_SESSION_SQL, createBindings, GET_SESSION_BY_TOKEN_SQL, getBindings);
|
|
294
|
+
if (session)
|
|
295
|
+
return session;
|
|
296
|
+
throw new Error(`Couldn't create session`);
|
|
297
|
+
},
|
|
298
|
+
async getSessionAndUser(sessionToken) {
|
|
299
|
+
const session = await getRecord(db, GET_SESSION_BY_TOKEN_SQL, [sessionToken]);
|
|
300
|
+
// no session? no user!
|
|
301
|
+
if (session === null)
|
|
302
|
+
return null;
|
|
303
|
+
// this shouldnt happen, but just in case
|
|
304
|
+
const user = await getRecord(db, GET_USER_BY_ID_SQL, [
|
|
305
|
+
session.userId,
|
|
306
|
+
]);
|
|
307
|
+
if (user === null)
|
|
308
|
+
return null;
|
|
309
|
+
return { session, user };
|
|
310
|
+
},
|
|
311
|
+
async updateSession({ sessionToken, expires }) {
|
|
312
|
+
// kinda strange that we have to deal with an undefined expires,
|
|
313
|
+
// we dont have any policy to enforce, lets just expire it now.
|
|
314
|
+
if (expires === undefined) {
|
|
315
|
+
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken]);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const session = await getRecord(db, GET_SESSION_BY_TOKEN_SQL, [sessionToken]);
|
|
319
|
+
if (!session)
|
|
320
|
+
return null;
|
|
321
|
+
session.expires = expires;
|
|
322
|
+
await updateRecord(db, UPDATE_SESSION_BY_SESSION_TOKEN_SQL, [
|
|
323
|
+
expires?.toISOString(),
|
|
324
|
+
sessionToken,
|
|
325
|
+
]);
|
|
326
|
+
return await db
|
|
327
|
+
.prepare(UPDATE_SESSION_BY_SESSION_TOKEN_SQL)
|
|
328
|
+
.bind(expires?.toISOString(), sessionToken)
|
|
329
|
+
.first();
|
|
330
|
+
},
|
|
331
|
+
async deleteSession(sessionToken) {
|
|
332
|
+
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken]);
|
|
333
|
+
return null;
|
|
334
|
+
},
|
|
335
|
+
async createVerificationToken({ identifier, expires, token }) {
|
|
336
|
+
return await createRecord(db, CREATE_VERIFICATION_TOKEN_SQL, [identifier, expires.toISOString(), token], GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL, [identifier, token]);
|
|
337
|
+
},
|
|
338
|
+
async useVerificationToken({ identifier, token }) {
|
|
339
|
+
const verificationToken = await getRecord(db, GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL, [identifier, token]);
|
|
340
|
+
if (!verificationToken)
|
|
341
|
+
return null;
|
|
342
|
+
await deleteRecord(db, DELETE_VERIFICATION_TOKEN_SQL, [identifier, token]);
|
|
343
|
+
return verificationToken;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
package/migrations.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { D1Database } from ".";
|
|
2
|
+
export declare const upSQLStatements: string[];
|
|
3
|
+
export declare const down: string[];
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @param db
|
|
7
|
+
*/
|
|
8
|
+
declare function up(db: D1Database): Promise<void>;
|
|
9
|
+
export { up };
|
|
10
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,GAAG,CAAA;AAEnC,eAAO,MAAM,eAAe,UAuC3B,CAAA;AAED,eAAO,MAAM,IAAI,UAKhB,CAAA;AAED;;;GAGG;AACH,iBAAe,EAAE,CAAC,EAAE,EAAE,UAAU,iBAS/B;AAED,OAAO,EAAE,EAAE,EAAE,CAAA"}
|
package/migrations.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const upSQLStatements = [
|
|
2
|
+
`CREATE TABLE IF NOT EXISTS "accounts" (
|
|
3
|
+
"id" text NOT NULL,
|
|
4
|
+
"userId" text NOT NULL DEFAULT NULL,
|
|
5
|
+
"type" text NOT NULL DEFAULT NULL,
|
|
6
|
+
"provider" text NOT NULL DEFAULT NULL,
|
|
7
|
+
"providerAccountId" text NOT NULL DEFAULT NULL,
|
|
8
|
+
"refresh_token" text DEFAULT NULL,
|
|
9
|
+
"access_token" text DEFAULT NULL,
|
|
10
|
+
"expires_at" number DEFAULT NULL,
|
|
11
|
+
"token_type" text DEFAULT NULL,
|
|
12
|
+
"scope" text DEFAULT NULL,
|
|
13
|
+
"id_token" text DEFAULT NULL,
|
|
14
|
+
"session_state" text DEFAULT NULL,
|
|
15
|
+
"oauth_token_secret" text DEFAULT NULL,
|
|
16
|
+
"oauth_token" text DEFAULT NULL,
|
|
17
|
+
PRIMARY KEY (id)
|
|
18
|
+
);`,
|
|
19
|
+
`CREATE TABLE IF NOT EXISTS "sessions" (
|
|
20
|
+
"id" text NOT NULL,
|
|
21
|
+
"sessionToken" text NOT NULL,
|
|
22
|
+
"userId" text NOT NULL DEFAULT NULL,
|
|
23
|
+
"expires" datetime NOT NULL DEFAULT NULL,
|
|
24
|
+
PRIMARY KEY (sessionToken)
|
|
25
|
+
);`,
|
|
26
|
+
`CREATE TABLE IF NOT EXISTS "users" (
|
|
27
|
+
"id" text NOT NULL DEFAULT '',
|
|
28
|
+
"name" text DEFAULT NULL,
|
|
29
|
+
"email" text DEFAULT NULL,
|
|
30
|
+
"emailVerified" datetime DEFAULT NULL,
|
|
31
|
+
"image" text DEFAULT NULL,
|
|
32
|
+
PRIMARY KEY (id)
|
|
33
|
+
);`,
|
|
34
|
+
`CREATE TABLE IF NOT EXISTS "verification_tokens" (
|
|
35
|
+
"identifier" text NOT NULL,
|
|
36
|
+
"token" text NOT NULL DEFAULT NULL,
|
|
37
|
+
"expires" datetime NOT NULL DEFAULT NULL,
|
|
38
|
+
PRIMARY KEY (token)
|
|
39
|
+
);`,
|
|
40
|
+
];
|
|
41
|
+
export const down = [
|
|
42
|
+
`DROP TABLE IF EXISTS "accounts";`,
|
|
43
|
+
`DROP TABLE IF EXISTS "sessions";`,
|
|
44
|
+
`DROP TABLE IF EXISTS "users";`,
|
|
45
|
+
`DROP TABLE IF EXISTS "verification_token";`,
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
*
|
|
49
|
+
* @param db
|
|
50
|
+
*/
|
|
51
|
+
async function up(db) {
|
|
52
|
+
// run the migration
|
|
53
|
+
upSQLStatements.forEach(async (sql) => {
|
|
54
|
+
try {
|
|
55
|
+
const res = await db.prepare(sql).run();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error(e.cause?.message, e.message);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export { up };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auth/d1-adapter",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A Cloudflare D1 adapter for Auth.js",
|
|
5
|
+
"homepage": "https://authjs.dev",
|
|
6
|
+
"repository": "https://github.com/nextauthjs/next-auth",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/nextauthjs/next-auth/issues"
|
|
9
|
+
},
|
|
10
|
+
"author": "Josh Schlesser <josh@schlesser.dev>",
|
|
11
|
+
"contributors": [
|
|
12
|
+
"Thang Huu Vu <hi@thvu.dev>"
|
|
13
|
+
],
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"next-auth",
|
|
17
|
+
"@auth",
|
|
18
|
+
"Auth.js",
|
|
19
|
+
"next.js",
|
|
20
|
+
"oauth",
|
|
21
|
+
"d1"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./index.d.ts",
|
|
28
|
+
"import": "./index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"*.d.ts*",
|
|
33
|
+
"*.js",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"private": false,
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@auth/core": "0.15.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@cloudflare/workers-types": "^4.20230321.0",
|
|
45
|
+
"@miniflare/d1": "^2.12.2",
|
|
46
|
+
"better-sqlite3": "^7.0.0",
|
|
47
|
+
"jest": "^29.3.0",
|
|
48
|
+
"@auth/adapter-test": "0.0.0",
|
|
49
|
+
"@auth/tsconfig": "0.0.0"
|
|
50
|
+
},
|
|
51
|
+
"jest": {
|
|
52
|
+
"preset": "@auth/adapter-test/jest"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc",
|
|
56
|
+
"clean": "rm -rf dist",
|
|
57
|
+
"test": "jest"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>An unofficial <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://developers.cloudflare.com/d1/">
|
|
5
|
+
* <img style={{display: "block"}} src="/img/adapters/d1.svg" width="48" />
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Warning
|
|
10
|
+
* 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.
|
|
11
|
+
* The adapter is compatible with the D1 api as of March 22, 2023.
|
|
12
|
+
*
|
|
13
|
+
* ## Installation
|
|
14
|
+
*
|
|
15
|
+
* ```bash npm2yarn2pnpm
|
|
16
|
+
* npm install next-auth @auth/d1-adapter
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module @auth/d1-adapter
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { D1Database as WorkerDatabase } from "@cloudflare/workers-types"
|
|
23
|
+
import type { D1Database as MiniflareD1Database } from "@miniflare/d1"
|
|
24
|
+
import type {
|
|
25
|
+
Adapter,
|
|
26
|
+
AdapterSession,
|
|
27
|
+
AdapterUser,
|
|
28
|
+
AdapterAccount,
|
|
29
|
+
VerificationToken as AdapterVerificationToken,
|
|
30
|
+
} from "@auth/core/adapters"
|
|
31
|
+
|
|
32
|
+
export { up } from "./migrations"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @type @cloudflare/workers-types.D1Database | @miniflare/d1.D1Database
|
|
36
|
+
*/
|
|
37
|
+
export type D1Database = WorkerDatabase | MiniflareD1Database
|
|
38
|
+
|
|
39
|
+
// all the sqls
|
|
40
|
+
// USER
|
|
41
|
+
export const CREATE_USER_SQL = `INSERT INTO users (id, name, email, emailVerified, image) VALUES (?, ?, ?, ?, ?)`
|
|
42
|
+
export const GET_USER_BY_ID_SQL = `SELECT * FROM users WHERE id = ?`
|
|
43
|
+
export const GET_USER_BY_EMAIL_SQL = `SELECT * FROM users WHERE email = ?`
|
|
44
|
+
export const GET_USER_BY_ACCOUNTL_SQL = `
|
|
45
|
+
SELECT u.*
|
|
46
|
+
FROM users u JOIN accounts a ON a.userId = u.id
|
|
47
|
+
WHERE a.providerAccountId = ? AND a.provider = ?`
|
|
48
|
+
export const UPDATE_USER_BY_ID_SQL = `
|
|
49
|
+
UPDATE users
|
|
50
|
+
SET name = ?, email = ?, emailVerified = ?, image = ?
|
|
51
|
+
WHERE id = ? `
|
|
52
|
+
export const DELETE_USER_SQL = `DELETE FROM users WHERE id = ?`
|
|
53
|
+
|
|
54
|
+
// SESSION
|
|
55
|
+
export const CREATE_SESSION_SQL =
|
|
56
|
+
"INSERT INTO sessions (id, sessionToken, userId, expires) VALUES (?,?,?,?)"
|
|
57
|
+
export const GET_SESSION_BY_TOKEN_SQL = `
|
|
58
|
+
SELECT id, sessionToken, userId, expires
|
|
59
|
+
FROM sessions
|
|
60
|
+
WHERE sessionToken = ?`
|
|
61
|
+
export const UPDATE_SESSION_BY_SESSION_TOKEN_SQL = `UPDATE sessions SET expires = ? WHERE sessionToken = ?`
|
|
62
|
+
export const DELETE_SESSION_SQL = `DELETE FROM sessions WHERE sessionToken = ?`
|
|
63
|
+
export const DELETE_SESSION_BY_USER_ID_SQL = `DELETE FROM sessions WHERE userId = ?`
|
|
64
|
+
|
|
65
|
+
// ACCOUNT
|
|
66
|
+
export const CREATE_ACCOUNT_SQL = `
|
|
67
|
+
INSERT INTO accounts (
|
|
68
|
+
id, userId, type, provider,
|
|
69
|
+
providerAccountId, refresh_token, access_token,
|
|
70
|
+
expires_at, token_type, scope, id_token, session_state,
|
|
71
|
+
oauth_token, oauth_token_secret
|
|
72
|
+
)
|
|
73
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
74
|
+
export const GET_ACCOUNT_BY_ID_SQL = `SELECT * FROM accounts WHERE id = ? `
|
|
75
|
+
export const GET_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = `SELECT * FROM accounts WHERE provider = ? AND providerAccountId = ?`
|
|
76
|
+
export const DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL = `DELETE FROM accounts WHERE provider = ? AND providerAccountId = ?`
|
|
77
|
+
export const DELETE_ACCOUNT_BY_USER_ID_SQL = `DELETE FROM accounts WHERE userId = ?`
|
|
78
|
+
|
|
79
|
+
// VERIFICATION_TOKEN
|
|
80
|
+
export const GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL = `SELECT * FROM verification_tokens WHERE identifier = ? AND token = ?`
|
|
81
|
+
export const CREATE_VERIFICATION_TOKEN_SQL = `INSERT INTO verification_tokens (identifier, expires, token) VALUES (?,?,?)`
|
|
82
|
+
export const DELETE_VERIFICATION_TOKEN_SQL = `DELETE FROM verification_tokens WHERE identifier = ? and token = ?`
|
|
83
|
+
|
|
84
|
+
// helper functions
|
|
85
|
+
|
|
86
|
+
// isDate is borrowed from the supabase adapter, graciously
|
|
87
|
+
// depending on error messages ("Invalid Date") is always precarious, but probably fine for a built in native like Date
|
|
88
|
+
function isDate(date: any) {
|
|
89
|
+
return (
|
|
90
|
+
new Date(date).toString() !== "Invalid Date" && !isNaN(Date.parse(date))
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// format is borrowed from the supabase adapter, graciously
|
|
95
|
+
function format<T>(obj: Record<string, any>): T {
|
|
96
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
97
|
+
if (value === null) {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
99
|
+
delete obj[key]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isDate(value)) {
|
|
103
|
+
obj[key] = new Date(value)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return obj as T
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// D1 doesnt like undefined, it wants null when calling bind
|
|
111
|
+
function cleanBindings(bindings: any[]) {
|
|
112
|
+
return bindings.map((e) => (e === undefined ? null : e))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function createRecord<RecordType>(
|
|
116
|
+
db: D1Database,
|
|
117
|
+
CREATE_SQL: string,
|
|
118
|
+
bindings: any[],
|
|
119
|
+
GET_SQL: string,
|
|
120
|
+
getBindings: any[]
|
|
121
|
+
) {
|
|
122
|
+
try {
|
|
123
|
+
bindings = cleanBindings(bindings)
|
|
124
|
+
await db
|
|
125
|
+
.prepare(CREATE_SQL)
|
|
126
|
+
.bind(...bindings)
|
|
127
|
+
.run()
|
|
128
|
+
return await getRecord<RecordType>(db, GET_SQL, getBindings)
|
|
129
|
+
} catch (e: any) {
|
|
130
|
+
console.error(e.message, e.cause?.message)
|
|
131
|
+
throw e
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function getRecord<RecordType>(
|
|
136
|
+
db: D1Database,
|
|
137
|
+
SQL: string,
|
|
138
|
+
bindings: any[]
|
|
139
|
+
): Promise<RecordType | null> {
|
|
140
|
+
try {
|
|
141
|
+
bindings = cleanBindings(bindings)
|
|
142
|
+
const res: any = await db
|
|
143
|
+
.prepare(SQL)
|
|
144
|
+
.bind(...bindings)
|
|
145
|
+
.first()
|
|
146
|
+
if (res) {
|
|
147
|
+
return format<RecordType>(res)
|
|
148
|
+
} else {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
console.error(e.message, e.cause?.message)
|
|
153
|
+
throw e
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function updateRecord(
|
|
158
|
+
db: D1Database,
|
|
159
|
+
SQL: string,
|
|
160
|
+
bindings: any[]
|
|
161
|
+
) {
|
|
162
|
+
try {
|
|
163
|
+
bindings = cleanBindings(bindings)
|
|
164
|
+
return await db
|
|
165
|
+
.prepare(SQL)
|
|
166
|
+
.bind(...bindings)
|
|
167
|
+
.run()
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
console.error(e.message, e.cause?.message)
|
|
170
|
+
throw e
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function deleteRecord(
|
|
175
|
+
db: D1Database,
|
|
176
|
+
SQL: string,
|
|
177
|
+
bindings: any[]
|
|
178
|
+
) {
|
|
179
|
+
// eslint-disable-next-line no-useless-catch
|
|
180
|
+
try {
|
|
181
|
+
bindings = cleanBindings(bindings)
|
|
182
|
+
await db
|
|
183
|
+
.prepare(SQL)
|
|
184
|
+
.bind(...bindings)
|
|
185
|
+
.run()
|
|
186
|
+
} catch (e: any) {
|
|
187
|
+
console.error(e.message, e.cause?.message)
|
|
188
|
+
throw e
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
*
|
|
194
|
+
* ## Setup
|
|
195
|
+
*
|
|
196
|
+
* 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.
|
|
197
|
+
*
|
|
198
|
+
* ### Configure Auth.js
|
|
199
|
+
*
|
|
200
|
+
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
201
|
+
* import NextAuth from "next-auth"
|
|
202
|
+
* import { D1Adapter, up } from "@auth/d1-adapter"
|
|
203
|
+
*
|
|
204
|
+
*
|
|
205
|
+
* // For more information on each option (and a full list of options) go to
|
|
206
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
207
|
+
* export default NextAuth({
|
|
208
|
+
* // https://authjs.dev/reference/providers/
|
|
209
|
+
* providers: [],
|
|
210
|
+
* adapter: D1Adapter(env.db)
|
|
211
|
+
* ...
|
|
212
|
+
* })
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* ### Migrations
|
|
216
|
+
*
|
|
217
|
+
* Somewhere in the initialization of your application you need to run the `up(env.db)` function to create the tables in D1.
|
|
218
|
+
* It will create 4 tables if they don't already exist:
|
|
219
|
+
* `accounts`, `sessions`, `users`, `verification_tokens`.
|
|
220
|
+
*
|
|
221
|
+
* The table prefix "" is not configurable at this time.
|
|
222
|
+
*
|
|
223
|
+
* 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.
|
|
224
|
+
* ```javascript
|
|
225
|
+
* import { up } from "@auth/d1-adapter"
|
|
226
|
+
*
|
|
227
|
+
* let migrated = false;
|
|
228
|
+
* async function migrationHandle({event, resolve}) {
|
|
229
|
+
* if(!migrated) {
|
|
230
|
+
* try {
|
|
231
|
+
* await up(event.platform.env.db)
|
|
232
|
+
* migrated = true
|
|
233
|
+
* } catch(e) {
|
|
234
|
+
* console.log(e.cause.message, e.message)
|
|
235
|
+
* }
|
|
236
|
+
* }
|
|
237
|
+
* return resolve(event)
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
*
|
|
242
|
+
* 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.
|
|
243
|
+
* Paste and run the SQL into your D1 dashboard query tool.
|
|
244
|
+
*
|
|
245
|
+
**/
|
|
246
|
+
export function D1Adapter(db: D1Database): Adapter {
|
|
247
|
+
// we need to run migrations if we dont have the right tables
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
async createUser(user) {
|
|
251
|
+
const id: string = crypto.randomUUID()
|
|
252
|
+
const createBindings = [
|
|
253
|
+
id,
|
|
254
|
+
user.name,
|
|
255
|
+
user.email,
|
|
256
|
+
user.emailVerified?.toISOString(),
|
|
257
|
+
user.image,
|
|
258
|
+
]
|
|
259
|
+
const getBindings = [id]
|
|
260
|
+
|
|
261
|
+
const newUser = await createRecord<AdapterUser>(
|
|
262
|
+
db,
|
|
263
|
+
CREATE_USER_SQL,
|
|
264
|
+
createBindings,
|
|
265
|
+
GET_USER_BY_ID_SQL,
|
|
266
|
+
getBindings
|
|
267
|
+
)
|
|
268
|
+
if (newUser) return newUser
|
|
269
|
+
throw new Error("Error creating user: Cannot get user after creation.")
|
|
270
|
+
},
|
|
271
|
+
async getUser(id) {
|
|
272
|
+
return await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [id])
|
|
273
|
+
},
|
|
274
|
+
async getUserByEmail(email) {
|
|
275
|
+
return await getRecord<AdapterUser>(db, GET_USER_BY_EMAIL_SQL, [email])
|
|
276
|
+
},
|
|
277
|
+
async getUserByAccount({ providerAccountId, provider }) {
|
|
278
|
+
return await getRecord<AdapterUser>(db, GET_USER_BY_ACCOUNTL_SQL, [
|
|
279
|
+
providerAccountId,
|
|
280
|
+
provider,
|
|
281
|
+
])
|
|
282
|
+
},
|
|
283
|
+
async updateUser(user) {
|
|
284
|
+
const params = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
|
|
285
|
+
user.id,
|
|
286
|
+
])
|
|
287
|
+
if (params) {
|
|
288
|
+
// copy any properties not in the update into the existing one and use that for bind params
|
|
289
|
+
// covers the scenario where the user arg doesnt have all of the current users properties
|
|
290
|
+
Object.assign(params, user)
|
|
291
|
+
const res = await updateRecord(db, UPDATE_USER_BY_ID_SQL, [
|
|
292
|
+
params.name,
|
|
293
|
+
params.email,
|
|
294
|
+
params.emailVerified?.toISOString(),
|
|
295
|
+
params.image,
|
|
296
|
+
params.id,
|
|
297
|
+
])
|
|
298
|
+
if (res.success) {
|
|
299
|
+
// we could probably just return
|
|
300
|
+
const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
|
|
301
|
+
params.id,
|
|
302
|
+
])
|
|
303
|
+
if (user) return user
|
|
304
|
+
throw new Error(
|
|
305
|
+
"Error updating user: Cannot get user after updating."
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
throw new Error("Error updating user: Failed to run the update SQL.")
|
|
310
|
+
},
|
|
311
|
+
async deleteUser(userId) {
|
|
312
|
+
// this should probably be in a db.batch but batch has problems right now in miniflare
|
|
313
|
+
// no multi line sql statements
|
|
314
|
+
await deleteRecord(db, DELETE_ACCOUNT_BY_USER_ID_SQL, [userId])
|
|
315
|
+
await deleteRecord(db, DELETE_SESSION_BY_USER_ID_SQL, [userId])
|
|
316
|
+
await deleteRecord(db, DELETE_USER_SQL, [userId])
|
|
317
|
+
return null
|
|
318
|
+
},
|
|
319
|
+
async linkAccount(a) {
|
|
320
|
+
// convert user_id to userId and provider_account_id to providerAccountId
|
|
321
|
+
const id = crypto.randomUUID()
|
|
322
|
+
const createBindings = [
|
|
323
|
+
id,
|
|
324
|
+
a.userId,
|
|
325
|
+
a.type,
|
|
326
|
+
a.provider,
|
|
327
|
+
a.providerAccountId,
|
|
328
|
+
a.refresh_token,
|
|
329
|
+
a.access_token,
|
|
330
|
+
a.expires_at,
|
|
331
|
+
a.token_type,
|
|
332
|
+
a.scope,
|
|
333
|
+
a.id_token,
|
|
334
|
+
a.session_state,
|
|
335
|
+
a.oauth_token ?? null,
|
|
336
|
+
a.oauth_token_secret ?? null,
|
|
337
|
+
]
|
|
338
|
+
const getBindings = [id]
|
|
339
|
+
return await createRecord<AdapterAccount>(
|
|
340
|
+
db,
|
|
341
|
+
CREATE_ACCOUNT_SQL,
|
|
342
|
+
createBindings,
|
|
343
|
+
GET_ACCOUNT_BY_ID_SQL,
|
|
344
|
+
getBindings
|
|
345
|
+
)
|
|
346
|
+
},
|
|
347
|
+
async unlinkAccount({ providerAccountId, provider }) {
|
|
348
|
+
await deleteRecord(
|
|
349
|
+
db,
|
|
350
|
+
DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL,
|
|
351
|
+
[provider, providerAccountId]
|
|
352
|
+
)
|
|
353
|
+
},
|
|
354
|
+
async createSession({ sessionToken, userId, expires }) {
|
|
355
|
+
const id = crypto.randomUUID()
|
|
356
|
+
const createBindings = [id, sessionToken, userId, expires.toISOString()]
|
|
357
|
+
const getBindings = [sessionToken]
|
|
358
|
+
const session = await createRecord<AdapterSession>(
|
|
359
|
+
db,
|
|
360
|
+
CREATE_SESSION_SQL,
|
|
361
|
+
createBindings,
|
|
362
|
+
GET_SESSION_BY_TOKEN_SQL,
|
|
363
|
+
getBindings
|
|
364
|
+
)
|
|
365
|
+
if (session) return session
|
|
366
|
+
throw new Error(`Couldn't create session`)
|
|
367
|
+
},
|
|
368
|
+
async getSessionAndUser(sessionToken) {
|
|
369
|
+
const session: any = await getRecord<AdapterSession>(
|
|
370
|
+
db,
|
|
371
|
+
GET_SESSION_BY_TOKEN_SQL,
|
|
372
|
+
[sessionToken]
|
|
373
|
+
)
|
|
374
|
+
// no session? no user!
|
|
375
|
+
if (session === null) return null
|
|
376
|
+
|
|
377
|
+
// this shouldnt happen, but just in case
|
|
378
|
+
const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [
|
|
379
|
+
session.userId,
|
|
380
|
+
])
|
|
381
|
+
if (user === null) return null
|
|
382
|
+
|
|
383
|
+
return { session, user }
|
|
384
|
+
},
|
|
385
|
+
async updateSession({ sessionToken, expires }) {
|
|
386
|
+
// kinda strange that we have to deal with an undefined expires,
|
|
387
|
+
// we dont have any policy to enforce, lets just expire it now.
|
|
388
|
+
if (expires === undefined) {
|
|
389
|
+
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken])
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
const session = await getRecord<AdapterSession>(
|
|
393
|
+
db,
|
|
394
|
+
GET_SESSION_BY_TOKEN_SQL,
|
|
395
|
+
[sessionToken]
|
|
396
|
+
)
|
|
397
|
+
if (!session) return null
|
|
398
|
+
session.expires = expires
|
|
399
|
+
await updateRecord(db, UPDATE_SESSION_BY_SESSION_TOKEN_SQL, [
|
|
400
|
+
expires?.toISOString(),
|
|
401
|
+
sessionToken,
|
|
402
|
+
])
|
|
403
|
+
return await db
|
|
404
|
+
.prepare(UPDATE_SESSION_BY_SESSION_TOKEN_SQL)
|
|
405
|
+
.bind(expires?.toISOString(), sessionToken)
|
|
406
|
+
.first()
|
|
407
|
+
},
|
|
408
|
+
async deleteSession(sessionToken) {
|
|
409
|
+
await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken])
|
|
410
|
+
return null
|
|
411
|
+
},
|
|
412
|
+
async createVerificationToken({ identifier, expires, token }) {
|
|
413
|
+
return await createRecord(
|
|
414
|
+
db,
|
|
415
|
+
CREATE_VERIFICATION_TOKEN_SQL,
|
|
416
|
+
[identifier, expires.toISOString(), token],
|
|
417
|
+
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
|
|
418
|
+
[identifier, token]
|
|
419
|
+
)
|
|
420
|
+
},
|
|
421
|
+
async useVerificationToken({ identifier, token }) {
|
|
422
|
+
const verificationToken = await getRecord<AdapterVerificationToken>(
|
|
423
|
+
db,
|
|
424
|
+
GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL,
|
|
425
|
+
[identifier, token]
|
|
426
|
+
)
|
|
427
|
+
if (!verificationToken) return null
|
|
428
|
+
await deleteRecord(db, DELETE_VERIFICATION_TOKEN_SQL, [identifier, token])
|
|
429
|
+
return verificationToken
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { D1Database } from "."
|
|
2
|
+
|
|
3
|
+
export const upSQLStatements = [
|
|
4
|
+
`CREATE TABLE IF NOT EXISTS "accounts" (
|
|
5
|
+
"id" text NOT NULL,
|
|
6
|
+
"userId" text NOT NULL DEFAULT NULL,
|
|
7
|
+
"type" text NOT NULL DEFAULT NULL,
|
|
8
|
+
"provider" text NOT NULL DEFAULT NULL,
|
|
9
|
+
"providerAccountId" text NOT NULL DEFAULT NULL,
|
|
10
|
+
"refresh_token" text DEFAULT NULL,
|
|
11
|
+
"access_token" text DEFAULT NULL,
|
|
12
|
+
"expires_at" number DEFAULT NULL,
|
|
13
|
+
"token_type" text DEFAULT NULL,
|
|
14
|
+
"scope" text DEFAULT NULL,
|
|
15
|
+
"id_token" text DEFAULT NULL,
|
|
16
|
+
"session_state" text DEFAULT NULL,
|
|
17
|
+
"oauth_token_secret" text DEFAULT NULL,
|
|
18
|
+
"oauth_token" text DEFAULT NULL,
|
|
19
|
+
PRIMARY KEY (id)
|
|
20
|
+
);`,
|
|
21
|
+
`CREATE TABLE IF NOT EXISTS "sessions" (
|
|
22
|
+
"id" text NOT NULL,
|
|
23
|
+
"sessionToken" text NOT NULL,
|
|
24
|
+
"userId" text NOT NULL DEFAULT NULL,
|
|
25
|
+
"expires" datetime NOT NULL DEFAULT NULL,
|
|
26
|
+
PRIMARY KEY (sessionToken)
|
|
27
|
+
);`,
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS "users" (
|
|
29
|
+
"id" text NOT NULL DEFAULT '',
|
|
30
|
+
"name" text DEFAULT NULL,
|
|
31
|
+
"email" text DEFAULT NULL,
|
|
32
|
+
"emailVerified" datetime DEFAULT NULL,
|
|
33
|
+
"image" text DEFAULT NULL,
|
|
34
|
+
PRIMARY KEY (id)
|
|
35
|
+
);`,
|
|
36
|
+
`CREATE TABLE IF NOT EXISTS "verification_tokens" (
|
|
37
|
+
"identifier" text NOT NULL,
|
|
38
|
+
"token" text NOT NULL DEFAULT NULL,
|
|
39
|
+
"expires" datetime NOT NULL DEFAULT NULL,
|
|
40
|
+
PRIMARY KEY (token)
|
|
41
|
+
);`,
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export const down = [
|
|
45
|
+
`DROP TABLE IF EXISTS "accounts";`,
|
|
46
|
+
`DROP TABLE IF EXISTS "sessions";`,
|
|
47
|
+
`DROP TABLE IF EXISTS "users";`,
|
|
48
|
+
`DROP TABLE IF EXISTS "verification_token";`,
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param db
|
|
54
|
+
*/
|
|
55
|
+
async function up(db: D1Database) {
|
|
56
|
+
// run the migration
|
|
57
|
+
upSQLStatements.forEach(async (sql) => {
|
|
58
|
+
try {
|
|
59
|
+
const res = await db.prepare(sql).run()
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
console.error(e.cause?.message, e.message)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { up }
|