@auth/fauna-adapter 1.0.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 +120 -0
- package/index.d.ts.map +1 -0
- package/index.js +218 -0
- package/package.json +62 -0
- package/src/index.ts +301 -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://fauna.com" target="_blank">
|
|
7
|
+
<img height="64px" src="https://authjs.dev/img/adapters/fauna.svg"/>
|
|
8
|
+
</a>
|
|
9
|
+
<h3 align="center"><b>Fauna Adapter</b> - NextAuth.js / Auth.js</a></h3>
|
|
10
|
+
<p align="center" style="align: center;">
|
|
11
|
+
<a href="https://npm.im/@auth/fauna-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/fauna-adapter">
|
|
15
|
+
<img alt="npm" src="https://img.shields.io/npm/v/@auth/fauna-adapter?color=green&label=@auth/fauna-adapter&style=flat-square">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://www.npmtrends.com/@auth/fauna-adapter">
|
|
18
|
+
<img src="https://img.shields.io/npm/dm/@auth/fauna-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/fauna).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
|
|
4
|
+
* <a href="https://fauna.com/features">
|
|
5
|
+
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" height="30"/>
|
|
6
|
+
* </a>
|
|
7
|
+
* </div>
|
|
8
|
+
*
|
|
9
|
+
* ## Installation
|
|
10
|
+
*
|
|
11
|
+
* ```bash npm2yarn2pnpm
|
|
12
|
+
* npm install @auth/fauna-adapter faunadb
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module @auth/fauna-adapter
|
|
16
|
+
*/
|
|
17
|
+
import { Client as FaunaClient, ExprArg } from "faunadb";
|
|
18
|
+
import { Adapter } from "@auth/core/adapters";
|
|
19
|
+
export declare const collections: {
|
|
20
|
+
readonly Users: import("faunadb").Expr;
|
|
21
|
+
readonly Accounts: import("faunadb").Expr;
|
|
22
|
+
readonly Sessions: import("faunadb").Expr;
|
|
23
|
+
readonly VerificationTokens: import("faunadb").Expr;
|
|
24
|
+
};
|
|
25
|
+
export declare const indexes: {
|
|
26
|
+
readonly AccountByProviderAndProviderAccountId: import("faunadb").Expr;
|
|
27
|
+
readonly UserByEmail: import("faunadb").Expr;
|
|
28
|
+
readonly SessionByToken: import("faunadb").Expr;
|
|
29
|
+
readonly VerificationTokenByIdentifierAndToken: import("faunadb").Expr;
|
|
30
|
+
readonly SessionsByUser: import("faunadb").Expr;
|
|
31
|
+
readonly AccountsByUser: import("faunadb").Expr;
|
|
32
|
+
};
|
|
33
|
+
export declare const format: {
|
|
34
|
+
/** Takes a plain old JavaScript object and turns it into a Fauna object */
|
|
35
|
+
to(object: Record<string, any>): Record<string, unknown>;
|
|
36
|
+
/** Takes a Fauna object and returns a plain old JavaScript object */
|
|
37
|
+
from<T = Record<string, unknown>>(object: Record<string, any>): T;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Fauna throws an error when something is not found in the db,
|
|
41
|
+
* `next-auth` expects `null` to be returned
|
|
42
|
+
*/
|
|
43
|
+
export declare function query(f: FaunaClient, format: (...args: any) => any): <T>(expr: ExprArg) => Promise<T | null>;
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* ## Setup
|
|
47
|
+
*
|
|
48
|
+
* This is the Fauna 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.
|
|
49
|
+
*
|
|
50
|
+
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapters/fauna](https://authjs.dev/reference/adapters/fauna).
|
|
51
|
+
*
|
|
52
|
+
* ### Configure Auth.js
|
|
53
|
+
*
|
|
54
|
+
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
55
|
+
* import NextAuth from "next-auth"
|
|
56
|
+
* import { Client as FaunaClient } from "faunadb"
|
|
57
|
+
* import { FaunaAdapter } from "@auth/fauna-adapter"
|
|
58
|
+
*
|
|
59
|
+
* const client = new FaunaClient({
|
|
60
|
+
* secret: "secret",
|
|
61
|
+
* scheme: "http",
|
|
62
|
+
* domain: "localhost",
|
|
63
|
+
* port: 8443,
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // For more information on each option (and a full list of options) go to
|
|
67
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
68
|
+
* export default NextAuth({
|
|
69
|
+
* // https://authjs.dev/reference/providers/
|
|
70
|
+
* providers: [],
|
|
71
|
+
* adapter: FaunaAdapter(client)
|
|
72
|
+
* ...
|
|
73
|
+
* })
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* ### Schema
|
|
77
|
+
*
|
|
78
|
+
* Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
|
|
79
|
+
*
|
|
80
|
+
* ```javascript
|
|
81
|
+
* CreateCollection({ name: "accounts" })
|
|
82
|
+
* CreateCollection({ name: "sessions" })
|
|
83
|
+
* CreateCollection({ name: "users" })
|
|
84
|
+
* CreateCollection({ name: "verification_tokens" })
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* ```javascript
|
|
88
|
+
* CreateIndex({
|
|
89
|
+
* name: "account_by_provider_and_provider_account_id",
|
|
90
|
+
* source: Collection("accounts"),
|
|
91
|
+
* unique: true,
|
|
92
|
+
* terms: [
|
|
93
|
+
* { field: ["data", "provider"] },
|
|
94
|
+
* { field: ["data", "providerAccountId"] },
|
|
95
|
+
* ],
|
|
96
|
+
* })
|
|
97
|
+
* CreateIndex({
|
|
98
|
+
* name: "session_by_session_token",
|
|
99
|
+
* source: Collection("sessions"),
|
|
100
|
+
* unique: true,
|
|
101
|
+
* terms: [{ field: ["data", "sessionToken"] }],
|
|
102
|
+
* })
|
|
103
|
+
* CreateIndex({
|
|
104
|
+
* name: "user_by_email",
|
|
105
|
+
* source: Collection("users"),
|
|
106
|
+
* unique: true,
|
|
107
|
+
* terms: [{ field: ["data", "email"] }],
|
|
108
|
+
* })
|
|
109
|
+
* CreateIndex({
|
|
110
|
+
* name: "verification_token_by_identifier_and_token",
|
|
111
|
+
* source: Collection("verification_tokens"),
|
|
112
|
+
* unique: true,
|
|
113
|
+
* terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
|
|
114
|
+
* })
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/adapters#models)
|
|
118
|
+
**/
|
|
119
|
+
export declare function FaunaAdapter(f: FaunaClient): Adapter;
|
|
120
|
+
//# 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":"AACA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,MAAM,IAAI,WAAW,EACrB,OAAO,EAoBR,MAAM,SAAS,CAAA;AAEhB,OAAO,EACL,OAAO,EAIR,MAAM,qBAAqB,CAAA;AAE5B,eAAO,MAAM,WAAW;;;;;CAKd,CAAA;AAEV,eAAO,MAAM,OAAO;;;;;;;CAWV,CAAA;AAEV,eAAO,MAAM,MAAM;IACjB,2EAA2E;eAChE,OAAO,MAAM,EAAE,GAAG,CAAC;IAY9B,qEAAqE;8CAC3B,OAAO,MAAM,EAAE,GAAG,CAAC;CAY9D,CAAA;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,KAAK,GAAG,aACjC,OAAO,uBAsBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0EI;AACJ,wBAAgB,YAAY,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAmGpD"}
|
package/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
/**
|
|
3
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
4
|
+
* <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
|
|
5
|
+
* <a href="https://fauna.com/features">
|
|
6
|
+
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" height="30"/>
|
|
7
|
+
* </a>
|
|
8
|
+
* </div>
|
|
9
|
+
*
|
|
10
|
+
* ## Installation
|
|
11
|
+
*
|
|
12
|
+
* ```bash npm2yarn2pnpm
|
|
13
|
+
* npm install @auth/fauna-adapter faunadb
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @module @auth/fauna-adapter
|
|
17
|
+
*/
|
|
18
|
+
import { Collection, Create, Delete, Exists, Get, If, Index, Let, Match, Ref, Select, Time, Update, Var, Paginate, Lambda, Do, Foreach, } from "faunadb";
|
|
19
|
+
export const collections = {
|
|
20
|
+
Users: Collection("users"),
|
|
21
|
+
Accounts: Collection("accounts"),
|
|
22
|
+
Sessions: Collection("sessions"),
|
|
23
|
+
VerificationTokens: Collection("verification_tokens"),
|
|
24
|
+
};
|
|
25
|
+
export const indexes = {
|
|
26
|
+
AccountByProviderAndProviderAccountId: Index("account_by_provider_and_provider_account_id"),
|
|
27
|
+
UserByEmail: Index("user_by_email"),
|
|
28
|
+
SessionByToken: Index("session_by_session_token"),
|
|
29
|
+
VerificationTokenByIdentifierAndToken: Index("verification_token_by_identifier_and_token"),
|
|
30
|
+
SessionsByUser: Index("sessions_by_user_id"),
|
|
31
|
+
AccountsByUser: Index("accounts_by_user_id"),
|
|
32
|
+
};
|
|
33
|
+
export const format = {
|
|
34
|
+
/** Takes a plain old JavaScript object and turns it into a Fauna object */
|
|
35
|
+
to(object) {
|
|
36
|
+
const newObject = {};
|
|
37
|
+
for (const key in object) {
|
|
38
|
+
const value = object[key];
|
|
39
|
+
if (value instanceof Date) {
|
|
40
|
+
newObject[key] = Time(value.toISOString());
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
newObject[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return newObject;
|
|
47
|
+
},
|
|
48
|
+
/** Takes a Fauna object and returns a plain old JavaScript object */
|
|
49
|
+
from(object) {
|
|
50
|
+
const newObject = {};
|
|
51
|
+
for (const key in object) {
|
|
52
|
+
const value = object[key];
|
|
53
|
+
if (value?.value && typeof value.value === "string") {
|
|
54
|
+
newObject[key] = new Date(value.value);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
newObject[key] = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return newObject;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Fauna throws an error when something is not found in the db,
|
|
65
|
+
* `next-auth` expects `null` to be returned
|
|
66
|
+
*/
|
|
67
|
+
export function query(f, format) {
|
|
68
|
+
return async function (expr) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await f.query(expr);
|
|
71
|
+
if (!result)
|
|
72
|
+
return null;
|
|
73
|
+
return format({ ...result.data, id: result.ref.id });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.name === "NotFound")
|
|
77
|
+
return null;
|
|
78
|
+
if (error.description?.includes("Number or numeric String expected"))
|
|
79
|
+
return null;
|
|
80
|
+
if (process.env.NODE_ENV === "test")
|
|
81
|
+
console.error(error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
*
|
|
88
|
+
* ## Setup
|
|
89
|
+
*
|
|
90
|
+
* This is the Fauna 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.
|
|
91
|
+
*
|
|
92
|
+
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapters/fauna](https://authjs.dev/reference/adapters/fauna).
|
|
93
|
+
*
|
|
94
|
+
* ### Configure Auth.js
|
|
95
|
+
*
|
|
96
|
+
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
97
|
+
* import NextAuth from "next-auth"
|
|
98
|
+
* import { Client as FaunaClient } from "faunadb"
|
|
99
|
+
* import { FaunaAdapter } from "@auth/fauna-adapter"
|
|
100
|
+
*
|
|
101
|
+
* const client = new FaunaClient({
|
|
102
|
+
* secret: "secret",
|
|
103
|
+
* scheme: "http",
|
|
104
|
+
* domain: "localhost",
|
|
105
|
+
* port: 8443,
|
|
106
|
+
* })
|
|
107
|
+
*
|
|
108
|
+
* // For more information on each option (and a full list of options) go to
|
|
109
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
110
|
+
* export default NextAuth({
|
|
111
|
+
* // https://authjs.dev/reference/providers/
|
|
112
|
+
* providers: [],
|
|
113
|
+
* adapter: FaunaAdapter(client)
|
|
114
|
+
* ...
|
|
115
|
+
* })
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* ### Schema
|
|
119
|
+
*
|
|
120
|
+
* Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
|
|
121
|
+
*
|
|
122
|
+
* ```javascript
|
|
123
|
+
* CreateCollection({ name: "accounts" })
|
|
124
|
+
* CreateCollection({ name: "sessions" })
|
|
125
|
+
* CreateCollection({ name: "users" })
|
|
126
|
+
* CreateCollection({ name: "verification_tokens" })
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* ```javascript
|
|
130
|
+
* CreateIndex({
|
|
131
|
+
* name: "account_by_provider_and_provider_account_id",
|
|
132
|
+
* source: Collection("accounts"),
|
|
133
|
+
* unique: true,
|
|
134
|
+
* terms: [
|
|
135
|
+
* { field: ["data", "provider"] },
|
|
136
|
+
* { field: ["data", "providerAccountId"] },
|
|
137
|
+
* ],
|
|
138
|
+
* })
|
|
139
|
+
* CreateIndex({
|
|
140
|
+
* name: "session_by_session_token",
|
|
141
|
+
* source: Collection("sessions"),
|
|
142
|
+
* unique: true,
|
|
143
|
+
* terms: [{ field: ["data", "sessionToken"] }],
|
|
144
|
+
* })
|
|
145
|
+
* CreateIndex({
|
|
146
|
+
* name: "user_by_email",
|
|
147
|
+
* source: Collection("users"),
|
|
148
|
+
* unique: true,
|
|
149
|
+
* terms: [{ field: ["data", "email"] }],
|
|
150
|
+
* })
|
|
151
|
+
* CreateIndex({
|
|
152
|
+
* name: "verification_token_by_identifier_and_token",
|
|
153
|
+
* source: Collection("verification_tokens"),
|
|
154
|
+
* unique: true,
|
|
155
|
+
* terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/adapters#models)
|
|
160
|
+
**/
|
|
161
|
+
export function FaunaAdapter(f) {
|
|
162
|
+
const { Users, Accounts, Sessions, VerificationTokens } = collections;
|
|
163
|
+
const { AccountByProviderAndProviderAccountId, AccountsByUser, SessionByToken, SessionsByUser, UserByEmail, VerificationTokenByIdentifierAndToken, } = indexes;
|
|
164
|
+
const { to, from } = format;
|
|
165
|
+
const q = query(f, from);
|
|
166
|
+
return {
|
|
167
|
+
createUser: async (data) => (await q(Create(Users, { data: to(data) }))),
|
|
168
|
+
getUser: async (id) => await q(Get(Ref(Users, id))),
|
|
169
|
+
getUserByEmail: async (email) => await q(Get(Match(UserByEmail, email))),
|
|
170
|
+
async getUserByAccount({ provider, providerAccountId }) {
|
|
171
|
+
const key = [provider, providerAccountId];
|
|
172
|
+
const ref = Match(AccountByProviderAndProviderAccountId, key);
|
|
173
|
+
const user = await q(Let({ ref }, If(Exists(Var("ref")), Get(Ref(Users, Select(["data", "userId"], Get(Var("ref"))))), null)));
|
|
174
|
+
return user;
|
|
175
|
+
},
|
|
176
|
+
updateUser: async (data) => (await q(Update(Ref(Users, data.id), { data: to(data) }))),
|
|
177
|
+
async deleteUser(userId) {
|
|
178
|
+
await f.query(Do(Foreach(Paginate(Match(SessionsByUser, userId)), Lambda("ref", Delete(Var("ref")))), Foreach(Paginate(Match(AccountsByUser, userId)), Lambda("ref", Delete(Var("ref")))), Delete(Ref(Users, userId))));
|
|
179
|
+
},
|
|
180
|
+
linkAccount: async (data) => (await q(Create(Accounts, { data: to(data) }))),
|
|
181
|
+
async unlinkAccount({ provider, providerAccountId }) {
|
|
182
|
+
const id = [provider, providerAccountId];
|
|
183
|
+
await q(Delete(Select("ref", Get(Match(AccountByProviderAndProviderAccountId, id)))));
|
|
184
|
+
},
|
|
185
|
+
createSession: async (data) => (await q(Create(Sessions, { data: to(data) }))),
|
|
186
|
+
async getSessionAndUser(sessionToken) {
|
|
187
|
+
const session = await q(Get(Match(SessionByToken, sessionToken)));
|
|
188
|
+
if (!session)
|
|
189
|
+
return null;
|
|
190
|
+
const user = await q(Get(Ref(Users, session.userId)));
|
|
191
|
+
return { session, user: user };
|
|
192
|
+
},
|
|
193
|
+
async updateSession(data) {
|
|
194
|
+
const ref = Select("ref", Get(Match(SessionByToken, data.sessionToken)));
|
|
195
|
+
return await q(Update(ref, { data: to(data) }));
|
|
196
|
+
},
|
|
197
|
+
async deleteSession(sessionToken) {
|
|
198
|
+
await q(Delete(Select("ref", Get(Match(SessionByToken, sessionToken)))));
|
|
199
|
+
},
|
|
200
|
+
async createVerificationToken(data) {
|
|
201
|
+
// @ts-expect-error
|
|
202
|
+
const { id: _id, ...verificationToken } = await q(Create(VerificationTokens, { data: to(data) }));
|
|
203
|
+
return verificationToken;
|
|
204
|
+
},
|
|
205
|
+
async useVerificationToken({ identifier, token }) {
|
|
206
|
+
const key = [identifier, token];
|
|
207
|
+
const object = Get(Match(VerificationTokenByIdentifierAndToken, key));
|
|
208
|
+
const verificationToken = await q(object);
|
|
209
|
+
if (!verificationToken)
|
|
210
|
+
return null;
|
|
211
|
+
// Verification tokens can be used only once
|
|
212
|
+
await q(Delete(Select("ref", object)));
|
|
213
|
+
// @ts-expect-error
|
|
214
|
+
delete verificationToken.id;
|
|
215
|
+
return verificationToken;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auth/fauna-adapter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fauna 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": "Bhanu Teja P",
|
|
11
|
+
"contributors": [
|
|
12
|
+
"Nico Domino <yo@ndo.dev>",
|
|
13
|
+
"Balázs Orbán <info@balazsorban.com>"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"*.js",
|
|
19
|
+
"*.d.ts*",
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./index.d.ts",
|
|
25
|
+
"import": "./index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"next-auth",
|
|
31
|
+
"next.js",
|
|
32
|
+
"fauna",
|
|
33
|
+
"faunadb"
|
|
34
|
+
],
|
|
35
|
+
"private": false,
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@auth/core": "0.8.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"faunadb": "^4.3.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@fauna-labs/fauna-schema-migrate": "^2.1.3",
|
|
47
|
+
"faunadb": "^4.3.0",
|
|
48
|
+
"jest": "^27.4.3",
|
|
49
|
+
"@next-auth/adapter-test": "0.0.0",
|
|
50
|
+
"@next-auth/tsconfig": "0.0.0"
|
|
51
|
+
},
|
|
52
|
+
"jest": {
|
|
53
|
+
"preset": "@next-auth/adapter-test/jest"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc",
|
|
57
|
+
"dev": "tsc -w",
|
|
58
|
+
"clean": "rm -rf dist",
|
|
59
|
+
"migrate": "fauna-schema-migrate generate",
|
|
60
|
+
"test": "./tests/test.sh"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
/**
|
|
3
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
4
|
+
* <p style={{fontWeight: "normal"}}>Official <a href="https://docs.fauna.com/fauna/current/">Fauna</a> adapter for Auth.js / NextAuth.js.</p>
|
|
5
|
+
* <a href="https://fauna.com/features">
|
|
6
|
+
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/fauna.svg" height="30"/>
|
|
7
|
+
* </a>
|
|
8
|
+
* </div>
|
|
9
|
+
*
|
|
10
|
+
* ## Installation
|
|
11
|
+
*
|
|
12
|
+
* ```bash npm2yarn2pnpm
|
|
13
|
+
* npm install @auth/fauna-adapter faunadb
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @module @auth/fauna-adapter
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
Client as FaunaClient,
|
|
20
|
+
ExprArg,
|
|
21
|
+
Collection,
|
|
22
|
+
Create,
|
|
23
|
+
Delete,
|
|
24
|
+
Exists,
|
|
25
|
+
Get,
|
|
26
|
+
If,
|
|
27
|
+
Index,
|
|
28
|
+
Let,
|
|
29
|
+
Match,
|
|
30
|
+
Ref,
|
|
31
|
+
Select,
|
|
32
|
+
Time,
|
|
33
|
+
Update,
|
|
34
|
+
Var,
|
|
35
|
+
Paginate,
|
|
36
|
+
Lambda,
|
|
37
|
+
Do,
|
|
38
|
+
Foreach,
|
|
39
|
+
errors,
|
|
40
|
+
} from "faunadb"
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
Adapter,
|
|
44
|
+
AdapterSession,
|
|
45
|
+
AdapterUser,
|
|
46
|
+
VerificationToken,
|
|
47
|
+
} from "@auth/core/adapters"
|
|
48
|
+
|
|
49
|
+
export const collections = {
|
|
50
|
+
Users: Collection("users"),
|
|
51
|
+
Accounts: Collection("accounts"),
|
|
52
|
+
Sessions: Collection("sessions"),
|
|
53
|
+
VerificationTokens: Collection("verification_tokens"),
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
export const indexes = {
|
|
57
|
+
AccountByProviderAndProviderAccountId: Index(
|
|
58
|
+
"account_by_provider_and_provider_account_id"
|
|
59
|
+
),
|
|
60
|
+
UserByEmail: Index("user_by_email"),
|
|
61
|
+
SessionByToken: Index("session_by_session_token"),
|
|
62
|
+
VerificationTokenByIdentifierAndToken: Index(
|
|
63
|
+
"verification_token_by_identifier_and_token"
|
|
64
|
+
),
|
|
65
|
+
SessionsByUser: Index("sessions_by_user_id"),
|
|
66
|
+
AccountsByUser: Index("accounts_by_user_id"),
|
|
67
|
+
} as const
|
|
68
|
+
|
|
69
|
+
export const format = {
|
|
70
|
+
/** Takes a plain old JavaScript object and turns it into a Fauna object */
|
|
71
|
+
to(object: Record<string, any>) {
|
|
72
|
+
const newObject: Record<string, unknown> = {}
|
|
73
|
+
for (const key in object) {
|
|
74
|
+
const value = object[key]
|
|
75
|
+
if (value instanceof Date) {
|
|
76
|
+
newObject[key] = Time(value.toISOString())
|
|
77
|
+
} else {
|
|
78
|
+
newObject[key] = value
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return newObject
|
|
82
|
+
},
|
|
83
|
+
/** Takes a Fauna object and returns a plain old JavaScript object */
|
|
84
|
+
from<T = Record<string, unknown>>(object: Record<string, any>): T {
|
|
85
|
+
const newObject: Record<string, unknown> = {}
|
|
86
|
+
for (const key in object) {
|
|
87
|
+
const value = object[key]
|
|
88
|
+
if (value?.value && typeof value.value === "string") {
|
|
89
|
+
newObject[key] = new Date(value.value)
|
|
90
|
+
} else {
|
|
91
|
+
newObject[key] = value
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return newObject as T
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fauna throws an error when something is not found in the db,
|
|
100
|
+
* `next-auth` expects `null` to be returned
|
|
101
|
+
*/
|
|
102
|
+
export function query(f: FaunaClient, format: (...args: any) => any) {
|
|
103
|
+
return async function <T>(expr: ExprArg): Promise<T | null> {
|
|
104
|
+
try {
|
|
105
|
+
const result = await f.query<{
|
|
106
|
+
data: T
|
|
107
|
+
ref: { id: string }
|
|
108
|
+
} | null>(expr)
|
|
109
|
+
if (!result) return null
|
|
110
|
+
return format({ ...result.data, id: result.ref.id })
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if ((error as errors.FaunaError).name === "NotFound") return null
|
|
113
|
+
if (
|
|
114
|
+
(error as errors.FaunaError).description?.includes(
|
|
115
|
+
"Number or numeric String expected"
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return null
|
|
119
|
+
|
|
120
|
+
if (process.env.NODE_ENV === "test") console.error(error)
|
|
121
|
+
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
*
|
|
129
|
+
* ## Setup
|
|
130
|
+
*
|
|
131
|
+
* This is the Fauna 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.
|
|
132
|
+
*
|
|
133
|
+
* You can find the Fauna schema and seed information in the docs at [authjs.dev/reference/adapters/fauna](https://authjs.dev/reference/adapters/fauna).
|
|
134
|
+
*
|
|
135
|
+
* ### Configure Auth.js
|
|
136
|
+
*
|
|
137
|
+
* ```javascript title="pages/api/auth/[...nextauth].js"
|
|
138
|
+
* import NextAuth from "next-auth"
|
|
139
|
+
* import { Client as FaunaClient } from "faunadb"
|
|
140
|
+
* import { FaunaAdapter } from "@auth/fauna-adapter"
|
|
141
|
+
*
|
|
142
|
+
* const client = new FaunaClient({
|
|
143
|
+
* secret: "secret",
|
|
144
|
+
* scheme: "http",
|
|
145
|
+
* domain: "localhost",
|
|
146
|
+
* port: 8443,
|
|
147
|
+
* })
|
|
148
|
+
*
|
|
149
|
+
* // For more information on each option (and a full list of options) go to
|
|
150
|
+
* // https://authjs.dev/reference/configuration/auth-options
|
|
151
|
+
* export default NextAuth({
|
|
152
|
+
* // https://authjs.dev/reference/providers/
|
|
153
|
+
* providers: [],
|
|
154
|
+
* adapter: FaunaAdapter(client)
|
|
155
|
+
* ...
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* ### Schema
|
|
160
|
+
*
|
|
161
|
+
* Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
|
|
162
|
+
*
|
|
163
|
+
* ```javascript
|
|
164
|
+
* CreateCollection({ name: "accounts" })
|
|
165
|
+
* CreateCollection({ name: "sessions" })
|
|
166
|
+
* CreateCollection({ name: "users" })
|
|
167
|
+
* CreateCollection({ name: "verification_tokens" })
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* ```javascript
|
|
171
|
+
* CreateIndex({
|
|
172
|
+
* name: "account_by_provider_and_provider_account_id",
|
|
173
|
+
* source: Collection("accounts"),
|
|
174
|
+
* unique: true,
|
|
175
|
+
* terms: [
|
|
176
|
+
* { field: ["data", "provider"] },
|
|
177
|
+
* { field: ["data", "providerAccountId"] },
|
|
178
|
+
* ],
|
|
179
|
+
* })
|
|
180
|
+
* CreateIndex({
|
|
181
|
+
* name: "session_by_session_token",
|
|
182
|
+
* source: Collection("sessions"),
|
|
183
|
+
* unique: true,
|
|
184
|
+
* terms: [{ field: ["data", "sessionToken"] }],
|
|
185
|
+
* })
|
|
186
|
+
* CreateIndex({
|
|
187
|
+
* name: "user_by_email",
|
|
188
|
+
* source: Collection("users"),
|
|
189
|
+
* unique: true,
|
|
190
|
+
* terms: [{ field: ["data", "email"] }],
|
|
191
|
+
* })
|
|
192
|
+
* CreateIndex({
|
|
193
|
+
* name: "verification_token_by_identifier_and_token",
|
|
194
|
+
* source: Collection("verification_tokens"),
|
|
195
|
+
* unique: true,
|
|
196
|
+
* terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
|
|
197
|
+
* })
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* > This schema is adapted for use in Fauna and based upon our main [schema](https://authjs.dev/reference/adapters#models)
|
|
201
|
+
**/
|
|
202
|
+
export function FaunaAdapter(f: FaunaClient): Adapter {
|
|
203
|
+
const { Users, Accounts, Sessions, VerificationTokens } = collections
|
|
204
|
+
const {
|
|
205
|
+
AccountByProviderAndProviderAccountId,
|
|
206
|
+
AccountsByUser,
|
|
207
|
+
SessionByToken,
|
|
208
|
+
SessionsByUser,
|
|
209
|
+
UserByEmail,
|
|
210
|
+
VerificationTokenByIdentifierAndToken,
|
|
211
|
+
} = indexes
|
|
212
|
+
const { to, from } = format
|
|
213
|
+
const q = query(f, from)
|
|
214
|
+
return {
|
|
215
|
+
createUser: async (data) => (await q(Create(Users, { data: to(data) })))!,
|
|
216
|
+
getUser: async (id) => await q(Get(Ref(Users, id))),
|
|
217
|
+
getUserByEmail: async (email) => await q(Get(Match(UserByEmail, email))),
|
|
218
|
+
async getUserByAccount({ provider, providerAccountId }) {
|
|
219
|
+
const key = [provider, providerAccountId]
|
|
220
|
+
const ref = Match(AccountByProviderAndProviderAccountId, key)
|
|
221
|
+
const user = await q<AdapterUser>(
|
|
222
|
+
Let(
|
|
223
|
+
{ ref },
|
|
224
|
+
If(
|
|
225
|
+
Exists(Var("ref")),
|
|
226
|
+
Get(Ref(Users, Select(["data", "userId"], Get(Var("ref"))))),
|
|
227
|
+
null
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
return user
|
|
232
|
+
},
|
|
233
|
+
updateUser: async (data) =>
|
|
234
|
+
(await q(Update(Ref(Users, data.id), { data: to(data) })))!,
|
|
235
|
+
async deleteUser(userId) {
|
|
236
|
+
await f.query(
|
|
237
|
+
Do(
|
|
238
|
+
Foreach(
|
|
239
|
+
Paginate(Match(SessionsByUser, userId)),
|
|
240
|
+
Lambda("ref", Delete(Var("ref")))
|
|
241
|
+
),
|
|
242
|
+
Foreach(
|
|
243
|
+
Paginate(Match(AccountsByUser, userId)),
|
|
244
|
+
Lambda("ref", Delete(Var("ref")))
|
|
245
|
+
),
|
|
246
|
+
Delete(Ref(Users, userId))
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
},
|
|
250
|
+
linkAccount: async (data) =>
|
|
251
|
+
(await q(Create(Accounts, { data: to(data) })))!,
|
|
252
|
+
async unlinkAccount({ provider, providerAccountId }) {
|
|
253
|
+
const id = [provider, providerAccountId]
|
|
254
|
+
await q(
|
|
255
|
+
Delete(
|
|
256
|
+
Select("ref", Get(Match(AccountByProviderAndProviderAccountId, id)))
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
},
|
|
260
|
+
createSession: async (data) =>
|
|
261
|
+
(await q<AdapterSession>(Create(Sessions, { data: to(data) })))!,
|
|
262
|
+
async getSessionAndUser(sessionToken) {
|
|
263
|
+
const session = await q<AdapterSession>(
|
|
264
|
+
Get(Match(SessionByToken, sessionToken))
|
|
265
|
+
)
|
|
266
|
+
if (!session) return null
|
|
267
|
+
|
|
268
|
+
const user = await q<AdapterUser>(Get(Ref(Users, session.userId)))
|
|
269
|
+
|
|
270
|
+
return { session, user: user! }
|
|
271
|
+
},
|
|
272
|
+
async updateSession(data) {
|
|
273
|
+
const ref = Select("ref", Get(Match(SessionByToken, data.sessionToken)))
|
|
274
|
+
return await q(Update(ref, { data: to(data) }))
|
|
275
|
+
},
|
|
276
|
+
async deleteSession(sessionToken) {
|
|
277
|
+
await q(Delete(Select("ref", Get(Match(SessionByToken, sessionToken)))))
|
|
278
|
+
},
|
|
279
|
+
async createVerificationToken(data) {
|
|
280
|
+
// @ts-expect-error
|
|
281
|
+
const { id: _id, ...verificationToken } = await q<VerificationToken>(
|
|
282
|
+
Create(VerificationTokens, { data: to(data) })
|
|
283
|
+
)
|
|
284
|
+
return verificationToken
|
|
285
|
+
},
|
|
286
|
+
async useVerificationToken({ identifier, token }) {
|
|
287
|
+
const key = [identifier, token]
|
|
288
|
+
const object = Get(Match(VerificationTokenByIdentifierAndToken, key))
|
|
289
|
+
|
|
290
|
+
const verificationToken = await q<VerificationToken>(object)
|
|
291
|
+
if (!verificationToken) return null
|
|
292
|
+
|
|
293
|
+
// Verification tokens can be used only once
|
|
294
|
+
await q(Delete(Select("ref", object)))
|
|
295
|
+
|
|
296
|
+
// @ts-expect-error
|
|
297
|
+
delete verificationToken.id
|
|
298
|
+
return verificationToken
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
}
|