@auth/firebase-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 +143 -0
- package/index.d.ts.map +1 -0
- package/index.js +327 -0
- package/package.json +57 -0
- package/src/index.ts +452 -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://firebase.google.com" target="_blank">
|
|
7
|
+
<img height="64px" src="https://authjs.dev/img/adapters/firebase.svg"/>
|
|
8
|
+
</a>
|
|
9
|
+
<h3 align="center"><b>Firebase Adapter</b> - NextAuth.js / Auth.js</a></h3>
|
|
10
|
+
<p align="center" style="align: center;">
|
|
11
|
+
<a href="https://npm.im/@auth/firebase-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/firebase-adapter">
|
|
15
|
+
<img alt="npm" src="https://img.shields.io/npm/v/@auth/firebase-adapter?color=green&label=@auth/firebase-adapter&style=flat-square">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://www.npmtrends.com/@auth/firebase-adapter">
|
|
18
|
+
<img src="https://img.shields.io/npm/dm/@auth/firebase-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/firebase).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <span>
|
|
4
|
+
* Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
|
|
5
|
+
* using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
|
|
6
|
+
* and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
|
|
7
|
+
* <a href="https://firebase.google.com/">
|
|
8
|
+
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/firebase.svg" height="48" width="48"/>
|
|
9
|
+
* </a>
|
|
10
|
+
* </div>
|
|
11
|
+
*
|
|
12
|
+
* ## Installation
|
|
13
|
+
*
|
|
14
|
+
* ```bash npm2yarn2pnpm
|
|
15
|
+
* npm install @auth/firebase-adapter firebase-admin
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @module @auth/firebase-adapter
|
|
19
|
+
*/
|
|
20
|
+
import { type AppOptions } from "firebase-admin/app";
|
|
21
|
+
import { Firestore } from "firebase-admin/firestore";
|
|
22
|
+
import type { Adapter } from "@auth/core/adapters";
|
|
23
|
+
/** Configure the Firebase Adapter. */
|
|
24
|
+
export interface FirebaseAdapterConfig extends AppOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
|
|
27
|
+
*/
|
|
28
|
+
name?: string;
|
|
29
|
+
firestore?: Firestore;
|
|
30
|
+
/**
|
|
31
|
+
* Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
|
|
32
|
+
* Passing `snake_case` will convert all field and collection names to `snake_case`.
|
|
33
|
+
* E.g. the collection `verificationTokens` will be `verification_tokens`,
|
|
34
|
+
* and fields like `emailVerified` will be `email_verified` instead.
|
|
35
|
+
*
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
39
|
+
* import NextAuth from "next-auth"
|
|
40
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
41
|
+
*
|
|
42
|
+
* export default NextAuth({
|
|
43
|
+
* adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
|
|
44
|
+
* // ...
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
namingStrategy?: "snake_case";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* ## Setup
|
|
52
|
+
*
|
|
53
|
+
* First, create a Firebase project and generate a service account key. Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
|
|
54
|
+
*
|
|
55
|
+
* Now you have a few options to authenticate with the Firebase Admin SDK in your app:
|
|
56
|
+
*
|
|
57
|
+
* ### Environment variables
|
|
58
|
+
* - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
|
|
59
|
+
* - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
|
|
60
|
+
* - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
64
|
+
* import NextAuth from "next-auth"
|
|
65
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
66
|
+
*
|
|
67
|
+
* export default NextAuth({
|
|
68
|
+
* adapter: FirestoreAdapter(),
|
|
69
|
+
* // ...
|
|
70
|
+
* })
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* ### Service account values
|
|
74
|
+
*
|
|
75
|
+
* - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
|
|
76
|
+
* - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
|
|
77
|
+
* - Pass the config to the adapter, using the environment variables as shown in the example below.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
81
|
+
* import NextAuth from "next-auth"
|
|
82
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
83
|
+
* import { cert } from "firebase-admin/app"
|
|
84
|
+
*
|
|
85
|
+
* export default NextAuth({
|
|
86
|
+
* adapter: FirestoreAdapter({
|
|
87
|
+
* credential: cert({
|
|
88
|
+
* projectId: process.env.FIREBASE_PROJECT_ID,
|
|
89
|
+
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
90
|
+
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
|
91
|
+
* })
|
|
92
|
+
* })
|
|
93
|
+
* // ...
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* ### Using an existing Firestore instance
|
|
98
|
+
*
|
|
99
|
+
* If you already have a Firestore instance, you can pass that to the adapter directly instead.
|
|
100
|
+
*
|
|
101
|
+
* :::note
|
|
102
|
+
* When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
|
|
103
|
+
* :::
|
|
104
|
+
*
|
|
105
|
+
* :::tip
|
|
106
|
+
* You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
|
|
107
|
+
* :::
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
111
|
+
* import NextAuth from "next-auth"
|
|
112
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
113
|
+
* import { firestore } from "lib/firestore"
|
|
114
|
+
*
|
|
115
|
+
* export default NextAuth({
|
|
116
|
+
* adapter: FirestoreAdapter(firestore),
|
|
117
|
+
* // ...
|
|
118
|
+
* })
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function FirestoreAdapter(config?: FirebaseAdapterConfig | Firestore): Adapter;
|
|
122
|
+
/**
|
|
123
|
+
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
|
|
124
|
+
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts title="lib/firestore.ts"
|
|
128
|
+
* import { initFirestore } from "@auth/firebase-adapter"
|
|
129
|
+
* import { cert } from "firebase-admin/app"
|
|
130
|
+
*
|
|
131
|
+
* export const firestore = initFirestore({
|
|
132
|
+
* credential: cert({
|
|
133
|
+
* projectId: process.env.FIREBASE_PROJECT_ID,
|
|
134
|
+
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
135
|
+
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
|
136
|
+
* })
|
|
137
|
+
* })
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare function initFirestore(options?: AppOptions & {
|
|
141
|
+
name?: FirebaseAdapterConfig["name"];
|
|
142
|
+
}): Firestore;
|
|
143
|
+
//# 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;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,KAAK,UAAU,EAA0B,MAAM,oBAAoB,CAAA;AAE5E,OAAO,EACL,SAAS,EAIV,MAAM,0BAA0B,CAAA;AAEjC,OAAO,KAAK,EACV,OAAO,EAKR,MAAM,qBAAqB,CAAA;AAE5B,sCAAsC;AACtC,MAAM,WAAW,qBAAsB,SAAQ,UAAU;IACvD;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,YAAY,CAAA;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,CAAC,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAgKT;AA8HD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,GAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAA;CAAO,aAQpE"}
|
package/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <span>
|
|
4
|
+
* Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
|
|
5
|
+
* using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
|
|
6
|
+
* and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
|
|
7
|
+
* <a href="https://firebase.google.com/">
|
|
8
|
+
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/firebase.svg" height="48" width="48"/>
|
|
9
|
+
* </a>
|
|
10
|
+
* </div>
|
|
11
|
+
*
|
|
12
|
+
* ## Installation
|
|
13
|
+
*
|
|
14
|
+
* ```bash npm2yarn2pnpm
|
|
15
|
+
* npm install @auth/firebase-adapter firebase-admin
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @module @auth/firebase-adapter
|
|
19
|
+
*/
|
|
20
|
+
import { getApps, initializeApp } from "firebase-admin/app";
|
|
21
|
+
import { Firestore, getFirestore, initializeFirestore, Timestamp, } from "firebase-admin/firestore";
|
|
22
|
+
/**
|
|
23
|
+
* ## Setup
|
|
24
|
+
*
|
|
25
|
+
* First, create a Firebase project and generate a service account key. Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
|
|
26
|
+
*
|
|
27
|
+
* Now you have a few options to authenticate with the Firebase Admin SDK in your app:
|
|
28
|
+
*
|
|
29
|
+
* ### Environment variables
|
|
30
|
+
* - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
|
|
31
|
+
* - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
|
|
32
|
+
* - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
36
|
+
* import NextAuth from "next-auth"
|
|
37
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
38
|
+
*
|
|
39
|
+
* export default NextAuth({
|
|
40
|
+
* adapter: FirestoreAdapter(),
|
|
41
|
+
* // ...
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* ### Service account values
|
|
46
|
+
*
|
|
47
|
+
* - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
|
|
48
|
+
* - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
|
|
49
|
+
* - Pass the config to the adapter, using the environment variables as shown in the example below.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
53
|
+
* import NextAuth from "next-auth"
|
|
54
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
55
|
+
* import { cert } from "firebase-admin/app"
|
|
56
|
+
*
|
|
57
|
+
* export default NextAuth({
|
|
58
|
+
* adapter: FirestoreAdapter({
|
|
59
|
+
* credential: cert({
|
|
60
|
+
* projectId: process.env.FIREBASE_PROJECT_ID,
|
|
61
|
+
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
62
|
+
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
|
63
|
+
* })
|
|
64
|
+
* })
|
|
65
|
+
* // ...
|
|
66
|
+
* })
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* ### Using an existing Firestore instance
|
|
70
|
+
*
|
|
71
|
+
* If you already have a Firestore instance, you can pass that to the adapter directly instead.
|
|
72
|
+
*
|
|
73
|
+
* :::note
|
|
74
|
+
* When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
|
|
75
|
+
* :::
|
|
76
|
+
*
|
|
77
|
+
* :::tip
|
|
78
|
+
* You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
|
|
79
|
+
* :::
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
83
|
+
* import NextAuth from "next-auth"
|
|
84
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
85
|
+
* import { firestore } from "lib/firestore"
|
|
86
|
+
*
|
|
87
|
+
* export default NextAuth({
|
|
88
|
+
* adapter: FirestoreAdapter(firestore),
|
|
89
|
+
* // ...
|
|
90
|
+
* })
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function FirestoreAdapter(config) {
|
|
94
|
+
const { db, namingStrategy = "default" } = config instanceof Firestore
|
|
95
|
+
? { db: config }
|
|
96
|
+
: { ...config, db: config?.firestore ?? initFirestore(config) };
|
|
97
|
+
const preferSnakeCase = namingStrategy === "snake_case";
|
|
98
|
+
const C = collectionsFactory(db, preferSnakeCase);
|
|
99
|
+
const mapper = mapFieldsFactory(preferSnakeCase);
|
|
100
|
+
return {
|
|
101
|
+
async createUser(userInit) {
|
|
102
|
+
const { id: userId } = await C.users.add(userInit);
|
|
103
|
+
const user = await getDoc(C.users.doc(userId));
|
|
104
|
+
if (!user)
|
|
105
|
+
throw new Error("[createUser] Failed to fetch created user");
|
|
106
|
+
return user;
|
|
107
|
+
},
|
|
108
|
+
async getUser(id) {
|
|
109
|
+
return await getDoc(C.users.doc(id));
|
|
110
|
+
},
|
|
111
|
+
async getUserByEmail(email) {
|
|
112
|
+
return await getOneDoc(C.users.where("email", "==", email));
|
|
113
|
+
},
|
|
114
|
+
async getUserByAccount({ provider, providerAccountId }) {
|
|
115
|
+
const account = await getOneDoc(C.accounts
|
|
116
|
+
.where("provider", "==", provider)
|
|
117
|
+
.where(mapper.toDb("providerAccountId"), "==", providerAccountId));
|
|
118
|
+
if (!account)
|
|
119
|
+
return null;
|
|
120
|
+
return await getDoc(C.users.doc(account.userId));
|
|
121
|
+
},
|
|
122
|
+
async updateUser(partialUser) {
|
|
123
|
+
if (!partialUser.id)
|
|
124
|
+
throw new Error("[updateUser] Missing id");
|
|
125
|
+
const userRef = C.users.doc(partialUser.id);
|
|
126
|
+
await userRef.set(partialUser, { merge: true });
|
|
127
|
+
const user = await getDoc(userRef);
|
|
128
|
+
if (!user)
|
|
129
|
+
throw new Error("[updateUser] Failed to fetch updated user");
|
|
130
|
+
return user;
|
|
131
|
+
},
|
|
132
|
+
async deleteUser(userId) {
|
|
133
|
+
await db.runTransaction(async (transaction) => {
|
|
134
|
+
const accounts = await C.accounts
|
|
135
|
+
.where(mapper.toDb("userId"), "==", userId)
|
|
136
|
+
.get();
|
|
137
|
+
const sessions = await C.sessions
|
|
138
|
+
.where(mapper.toDb("userId"), "==", userId)
|
|
139
|
+
.get();
|
|
140
|
+
transaction.delete(C.users.doc(userId));
|
|
141
|
+
accounts.forEach((account) => transaction.delete(account.ref));
|
|
142
|
+
sessions.forEach((session) => transaction.delete(session.ref));
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
async linkAccount(accountInit) {
|
|
146
|
+
const ref = await C.accounts.add(accountInit);
|
|
147
|
+
const account = await ref.get().then((doc) => doc.data());
|
|
148
|
+
return account ?? null;
|
|
149
|
+
},
|
|
150
|
+
async unlinkAccount({ provider, providerAccountId }) {
|
|
151
|
+
await deleteDocs(C.accounts
|
|
152
|
+
.where("provider", "==", provider)
|
|
153
|
+
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
|
|
154
|
+
.limit(1));
|
|
155
|
+
},
|
|
156
|
+
async createSession(sessionInit) {
|
|
157
|
+
const ref = await C.sessions.add(sessionInit);
|
|
158
|
+
const session = await ref.get().then((doc) => doc.data());
|
|
159
|
+
if (session)
|
|
160
|
+
return session ?? null;
|
|
161
|
+
throw new Error("[createSession] Failed to fetch created session");
|
|
162
|
+
},
|
|
163
|
+
async getSessionAndUser(sessionToken) {
|
|
164
|
+
const session = await getOneDoc(C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken));
|
|
165
|
+
if (!session)
|
|
166
|
+
return null;
|
|
167
|
+
const user = await getDoc(C.users.doc(session.userId));
|
|
168
|
+
if (!user)
|
|
169
|
+
return null;
|
|
170
|
+
return { session, user };
|
|
171
|
+
},
|
|
172
|
+
async updateSession(partialSession) {
|
|
173
|
+
const sessionId = await db.runTransaction(async (transaction) => {
|
|
174
|
+
const sessionSnapshot = (await transaction.get(C.sessions
|
|
175
|
+
.where(mapper.toDb("sessionToken"), "==", partialSession.sessionToken)
|
|
176
|
+
.limit(1))).docs[0];
|
|
177
|
+
if (!sessionSnapshot?.exists)
|
|
178
|
+
return null;
|
|
179
|
+
transaction.set(sessionSnapshot.ref, partialSession, { merge: true });
|
|
180
|
+
return sessionSnapshot.id;
|
|
181
|
+
});
|
|
182
|
+
if (!sessionId)
|
|
183
|
+
return null;
|
|
184
|
+
const session = await getDoc(C.sessions.doc(sessionId));
|
|
185
|
+
if (session)
|
|
186
|
+
return session;
|
|
187
|
+
throw new Error("[updateSession] Failed to fetch updated session");
|
|
188
|
+
},
|
|
189
|
+
async deleteSession(sessionToken) {
|
|
190
|
+
await deleteDocs(C.sessions
|
|
191
|
+
.where(mapper.toDb("sessionToken"), "==", sessionToken)
|
|
192
|
+
.limit(1));
|
|
193
|
+
},
|
|
194
|
+
async createVerificationToken(verificationToken) {
|
|
195
|
+
await C.verification_tokens.add(verificationToken);
|
|
196
|
+
return verificationToken;
|
|
197
|
+
},
|
|
198
|
+
async useVerificationToken({ identifier, token }) {
|
|
199
|
+
const verificationTokenSnapshot = (await C.verification_tokens
|
|
200
|
+
.where("identifier", "==", identifier)
|
|
201
|
+
.where("token", "==", token)
|
|
202
|
+
.limit(1)
|
|
203
|
+
.get()).docs[0];
|
|
204
|
+
if (!verificationTokenSnapshot)
|
|
205
|
+
return null;
|
|
206
|
+
const data = verificationTokenSnapshot.data();
|
|
207
|
+
await verificationTokenSnapshot.ref.delete();
|
|
208
|
+
return data;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// for consistency, store all fields as snake_case in the database
|
|
213
|
+
const MAP_TO_FIRESTORE = {
|
|
214
|
+
userId: "user_id",
|
|
215
|
+
sessionToken: "session_token",
|
|
216
|
+
providerAccountId: "provider_account_id",
|
|
217
|
+
emailVerified: "email_verified",
|
|
218
|
+
};
|
|
219
|
+
const MAP_FROM_FIRESTORE = {};
|
|
220
|
+
for (const key in MAP_TO_FIRESTORE) {
|
|
221
|
+
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]] = key;
|
|
222
|
+
}
|
|
223
|
+
const identity = (x) => x;
|
|
224
|
+
/** @internal */
|
|
225
|
+
export function mapFieldsFactory(preferSnakeCase) {
|
|
226
|
+
if (preferSnakeCase) {
|
|
227
|
+
return {
|
|
228
|
+
toDb: (field) => MAP_TO_FIRESTORE[field] ?? field,
|
|
229
|
+
fromDb: (field) => MAP_FROM_FIRESTORE[field] ?? field,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return { toDb: identity, fromDb: identity };
|
|
233
|
+
}
|
|
234
|
+
/** @internal */
|
|
235
|
+
function getConverter(options) {
|
|
236
|
+
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false);
|
|
237
|
+
return {
|
|
238
|
+
toFirestore(object) {
|
|
239
|
+
const document = {};
|
|
240
|
+
for (const key in object) {
|
|
241
|
+
if (key === "id")
|
|
242
|
+
continue;
|
|
243
|
+
const value = object[key];
|
|
244
|
+
if (value !== undefined) {
|
|
245
|
+
document[mapper.toDb(key)] = value;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return document;
|
|
252
|
+
},
|
|
253
|
+
fromFirestore(snapshot) {
|
|
254
|
+
const document = snapshot.data(); // we can guarantee it exists
|
|
255
|
+
const object = {};
|
|
256
|
+
if (!options?.excludeId) {
|
|
257
|
+
object.id = snapshot.id;
|
|
258
|
+
}
|
|
259
|
+
for (const key in document) {
|
|
260
|
+
let value = document[key];
|
|
261
|
+
if (value instanceof Timestamp)
|
|
262
|
+
value = value.toDate();
|
|
263
|
+
object[mapper.fromDb(key)] = value;
|
|
264
|
+
}
|
|
265
|
+
return object;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** @internal */
|
|
270
|
+
export async function getOneDoc(querySnapshot) {
|
|
271
|
+
const querySnap = await querySnapshot.limit(1).get();
|
|
272
|
+
return querySnap.docs[0]?.data() ?? null;
|
|
273
|
+
}
|
|
274
|
+
/** @internal */
|
|
275
|
+
async function deleteDocs(querySnapshot) {
|
|
276
|
+
const querySnap = await querySnapshot.get();
|
|
277
|
+
for (const doc of querySnap.docs) {
|
|
278
|
+
await doc.ref.delete();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** @internal */
|
|
282
|
+
export async function getDoc(docRef) {
|
|
283
|
+
const docSnap = await docRef.get();
|
|
284
|
+
return docSnap.data() ?? null;
|
|
285
|
+
}
|
|
286
|
+
/** @internal */
|
|
287
|
+
export function collectionsFactory(db, preferSnakeCase = false) {
|
|
288
|
+
return {
|
|
289
|
+
users: db
|
|
290
|
+
.collection("users")
|
|
291
|
+
.withConverter(getConverter({ preferSnakeCase })),
|
|
292
|
+
sessions: db
|
|
293
|
+
.collection("sessions")
|
|
294
|
+
.withConverter(getConverter({ preferSnakeCase })),
|
|
295
|
+
accounts: db
|
|
296
|
+
.collection("accounts")
|
|
297
|
+
.withConverter(getConverter({ preferSnakeCase })),
|
|
298
|
+
verification_tokens: db
|
|
299
|
+
.collection(preferSnakeCase ? "verification_tokens" : "verificationTokens")
|
|
300
|
+
.withConverter(getConverter({ preferSnakeCase, excludeId: true })),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
|
|
305
|
+
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts title="lib/firestore.ts"
|
|
309
|
+
* import { initFirestore } from "@auth/firebase-adapter"
|
|
310
|
+
* import { cert } from "firebase-admin/app"
|
|
311
|
+
*
|
|
312
|
+
* export const firestore = initFirestore({
|
|
313
|
+
* credential: cert({
|
|
314
|
+
* projectId: process.env.FIREBASE_PROJECT_ID,
|
|
315
|
+
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
316
|
+
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
|
317
|
+
* })
|
|
318
|
+
* })
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
export function initFirestore(options = {}) {
|
|
322
|
+
const apps = getApps();
|
|
323
|
+
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0];
|
|
324
|
+
if (app)
|
|
325
|
+
return getFirestore(app);
|
|
326
|
+
return initializeFirestore(initializeApp(options, options.name));
|
|
327
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@auth/firebase-adapter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Firebase 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": "Ron Houben <ron.houben85@gmail.com>",
|
|
11
|
+
"contributors": [
|
|
12
|
+
"Nico Domino <yo@ndo.dev>",
|
|
13
|
+
"Alex Meuer <github@alexmeuer.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
|
+
"firebase",
|
|
33
|
+
"firebase-admin"
|
|
34
|
+
],
|
|
35
|
+
"private": false,
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@auth/core": "0.8.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"firebase-admin": "^11.4.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"firebase-admin": "^11.4.1",
|
|
47
|
+
"firebase-tools": "^11.16.1",
|
|
48
|
+
"jest": "^29.3.1",
|
|
49
|
+
"@next-auth/adapter-test": "0.0.0",
|
|
50
|
+
"@next-auth/tsconfig": "0.0.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "tsc -w",
|
|
54
|
+
"build": "tsc",
|
|
55
|
+
"test": "firebase emulators:exec --only firestore --project next-auth-test 'jest -c tests/jest.config.js'"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
|
3
|
+
* <span>
|
|
4
|
+
* Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
|
|
5
|
+
* using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
|
|
6
|
+
* and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
|
|
7
|
+
* <a href="https://firebase.google.com/">
|
|
8
|
+
* <img style={{display: "block"}} src="https://authjs.dev/img/adapters/firebase.svg" height="48" width="48"/>
|
|
9
|
+
* </a>
|
|
10
|
+
* </div>
|
|
11
|
+
*
|
|
12
|
+
* ## Installation
|
|
13
|
+
*
|
|
14
|
+
* ```bash npm2yarn2pnpm
|
|
15
|
+
* npm install @auth/firebase-adapter firebase-admin
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @module @auth/firebase-adapter
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type AppOptions, getApps, initializeApp } from "firebase-admin/app"
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
Firestore,
|
|
25
|
+
getFirestore,
|
|
26
|
+
initializeFirestore,
|
|
27
|
+
Timestamp,
|
|
28
|
+
} from "firebase-admin/firestore"
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
Adapter,
|
|
32
|
+
AdapterUser,
|
|
33
|
+
AdapterAccount,
|
|
34
|
+
AdapterSession,
|
|
35
|
+
VerificationToken,
|
|
36
|
+
} from "@auth/core/adapters"
|
|
37
|
+
|
|
38
|
+
/** Configure the Firebase Adapter. */
|
|
39
|
+
export interface FirebaseAdapterConfig extends AppOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
|
|
42
|
+
*/
|
|
43
|
+
name?: string
|
|
44
|
+
firestore?: Firestore
|
|
45
|
+
/**
|
|
46
|
+
* Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
|
|
47
|
+
* Passing `snake_case` will convert all field and collection names to `snake_case`.
|
|
48
|
+
* E.g. the collection `verificationTokens` will be `verification_tokens`,
|
|
49
|
+
* and fields like `emailVerified` will be `email_verified` instead.
|
|
50
|
+
*
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
54
|
+
* import NextAuth from "next-auth"
|
|
55
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
56
|
+
*
|
|
57
|
+
* export default NextAuth({
|
|
58
|
+
* adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
|
|
59
|
+
* // ...
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
namingStrategy?: "snake_case"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ## Setup
|
|
68
|
+
*
|
|
69
|
+
* First, create a Firebase project and generate a service account key. Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
|
|
70
|
+
*
|
|
71
|
+
* Now you have a few options to authenticate with the Firebase Admin SDK in your app:
|
|
72
|
+
*
|
|
73
|
+
* ### Environment variables
|
|
74
|
+
* - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
|
|
75
|
+
* - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
|
|
76
|
+
* - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
80
|
+
* import NextAuth from "next-auth"
|
|
81
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
82
|
+
*
|
|
83
|
+
* export default NextAuth({
|
|
84
|
+
* adapter: FirestoreAdapter(),
|
|
85
|
+
* // ...
|
|
86
|
+
* })
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ### Service account values
|
|
90
|
+
*
|
|
91
|
+
* - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
|
|
92
|
+
* - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
|
|
93
|
+
* - Pass the config to the adapter, using the environment variables as shown in the example below.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
97
|
+
* import NextAuth from "next-auth"
|
|
98
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
99
|
+
* import { cert } from "firebase-admin/app"
|
|
100
|
+
*
|
|
101
|
+
* export default NextAuth({
|
|
102
|
+
* adapter: FirestoreAdapter({
|
|
103
|
+
* credential: cert({
|
|
104
|
+
* projectId: process.env.FIREBASE_PROJECT_ID,
|
|
105
|
+
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
106
|
+
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
|
107
|
+
* })
|
|
108
|
+
* })
|
|
109
|
+
* // ...
|
|
110
|
+
* })
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* ### Using an existing Firestore instance
|
|
114
|
+
*
|
|
115
|
+
* If you already have a Firestore instance, you can pass that to the adapter directly instead.
|
|
116
|
+
*
|
|
117
|
+
* :::note
|
|
118
|
+
* When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
|
|
119
|
+
* :::
|
|
120
|
+
*
|
|
121
|
+
* :::tip
|
|
122
|
+
* You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
|
|
123
|
+
* :::
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts title="pages/api/auth/[...nextauth].ts"
|
|
127
|
+
* import NextAuth from "next-auth"
|
|
128
|
+
* import { FirestoreAdapter } from "@auth/firebase-adapter"
|
|
129
|
+
* import { firestore } from "lib/firestore"
|
|
130
|
+
*
|
|
131
|
+
* export default NextAuth({
|
|
132
|
+
* adapter: FirestoreAdapter(firestore),
|
|
133
|
+
* // ...
|
|
134
|
+
* })
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function FirestoreAdapter(
|
|
138
|
+
config?: FirebaseAdapterConfig | Firestore
|
|
139
|
+
): Adapter {
|
|
140
|
+
const { db, namingStrategy = "default" } =
|
|
141
|
+
config instanceof Firestore
|
|
142
|
+
? { db: config }
|
|
143
|
+
: { ...config, db: config?.firestore ?? initFirestore(config) }
|
|
144
|
+
|
|
145
|
+
const preferSnakeCase = namingStrategy === "snake_case"
|
|
146
|
+
const C = collectionsFactory(db, preferSnakeCase)
|
|
147
|
+
const mapper = mapFieldsFactory(preferSnakeCase)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
async createUser(userInit) {
|
|
151
|
+
const { id: userId } = await C.users.add(userInit as AdapterUser)
|
|
152
|
+
|
|
153
|
+
const user = await getDoc(C.users.doc(userId))
|
|
154
|
+
if (!user) throw new Error("[createUser] Failed to fetch created user")
|
|
155
|
+
|
|
156
|
+
return user
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async getUser(id) {
|
|
160
|
+
return await getDoc(C.users.doc(id))
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async getUserByEmail(email) {
|
|
164
|
+
return await getOneDoc(C.users.where("email", "==", email))
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async getUserByAccount({ provider, providerAccountId }) {
|
|
168
|
+
const account = await getOneDoc(
|
|
169
|
+
C.accounts
|
|
170
|
+
.where("provider", "==", provider)
|
|
171
|
+
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
|
|
172
|
+
)
|
|
173
|
+
if (!account) return null
|
|
174
|
+
|
|
175
|
+
return await getDoc(C.users.doc(account.userId))
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async updateUser(partialUser) {
|
|
179
|
+
if (!partialUser.id) throw new Error("[updateUser] Missing id")
|
|
180
|
+
|
|
181
|
+
const userRef = C.users.doc(partialUser.id)
|
|
182
|
+
|
|
183
|
+
await userRef.set(partialUser, { merge: true })
|
|
184
|
+
|
|
185
|
+
const user = await getDoc(userRef)
|
|
186
|
+
if (!user) throw new Error("[updateUser] Failed to fetch updated user")
|
|
187
|
+
|
|
188
|
+
return user
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async deleteUser(userId) {
|
|
192
|
+
await db.runTransaction(async (transaction) => {
|
|
193
|
+
const accounts = await C.accounts
|
|
194
|
+
.where(mapper.toDb("userId"), "==", userId)
|
|
195
|
+
.get()
|
|
196
|
+
const sessions = await C.sessions
|
|
197
|
+
.where(mapper.toDb("userId"), "==", userId)
|
|
198
|
+
.get()
|
|
199
|
+
|
|
200
|
+
transaction.delete(C.users.doc(userId))
|
|
201
|
+
|
|
202
|
+
accounts.forEach((account) => transaction.delete(account.ref))
|
|
203
|
+
sessions.forEach((session) => transaction.delete(session.ref))
|
|
204
|
+
})
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async linkAccount(accountInit) {
|
|
208
|
+
const ref = await C.accounts.add(accountInit)
|
|
209
|
+
const account = await ref.get().then((doc) => doc.data())
|
|
210
|
+
return account ?? null
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async unlinkAccount({ provider, providerAccountId }) {
|
|
214
|
+
await deleteDocs(
|
|
215
|
+
C.accounts
|
|
216
|
+
.where("provider", "==", provider)
|
|
217
|
+
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
|
|
218
|
+
.limit(1)
|
|
219
|
+
)
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async createSession(sessionInit) {
|
|
223
|
+
const ref = await C.sessions.add(sessionInit)
|
|
224
|
+
const session = await ref.get().then((doc) => doc.data())
|
|
225
|
+
|
|
226
|
+
if (session) return session ?? null
|
|
227
|
+
|
|
228
|
+
throw new Error("[createSession] Failed to fetch created session")
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async getSessionAndUser(sessionToken) {
|
|
232
|
+
const session = await getOneDoc(
|
|
233
|
+
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
|
|
234
|
+
)
|
|
235
|
+
if (!session) return null
|
|
236
|
+
|
|
237
|
+
const user = await getDoc(C.users.doc(session.userId))
|
|
238
|
+
if (!user) return null
|
|
239
|
+
|
|
240
|
+
return { session, user }
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async updateSession(partialSession) {
|
|
244
|
+
const sessionId = await db.runTransaction(async (transaction) => {
|
|
245
|
+
const sessionSnapshot = (
|
|
246
|
+
await transaction.get(
|
|
247
|
+
C.sessions
|
|
248
|
+
.where(
|
|
249
|
+
mapper.toDb("sessionToken"),
|
|
250
|
+
"==",
|
|
251
|
+
partialSession.sessionToken
|
|
252
|
+
)
|
|
253
|
+
.limit(1)
|
|
254
|
+
)
|
|
255
|
+
).docs[0]
|
|
256
|
+
if (!sessionSnapshot?.exists) return null
|
|
257
|
+
|
|
258
|
+
transaction.set(sessionSnapshot.ref, partialSession, { merge: true })
|
|
259
|
+
|
|
260
|
+
return sessionSnapshot.id
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (!sessionId) return null
|
|
264
|
+
|
|
265
|
+
const session = await getDoc(C.sessions.doc(sessionId))
|
|
266
|
+
if (session) return session
|
|
267
|
+
throw new Error("[updateSession] Failed to fetch updated session")
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async deleteSession(sessionToken) {
|
|
271
|
+
await deleteDocs(
|
|
272
|
+
C.sessions
|
|
273
|
+
.where(mapper.toDb("sessionToken"), "==", sessionToken)
|
|
274
|
+
.limit(1)
|
|
275
|
+
)
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
async createVerificationToken(verificationToken) {
|
|
279
|
+
await C.verification_tokens.add(verificationToken)
|
|
280
|
+
return verificationToken
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async useVerificationToken({ identifier, token }) {
|
|
284
|
+
const verificationTokenSnapshot = (
|
|
285
|
+
await C.verification_tokens
|
|
286
|
+
.where("identifier", "==", identifier)
|
|
287
|
+
.where("token", "==", token)
|
|
288
|
+
.limit(1)
|
|
289
|
+
.get()
|
|
290
|
+
).docs[0]
|
|
291
|
+
|
|
292
|
+
if (!verificationTokenSnapshot) return null
|
|
293
|
+
|
|
294
|
+
const data = verificationTokenSnapshot.data()
|
|
295
|
+
await verificationTokenSnapshot.ref.delete()
|
|
296
|
+
return data
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// for consistency, store all fields as snake_case in the database
|
|
302
|
+
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
|
|
303
|
+
userId: "user_id",
|
|
304
|
+
sessionToken: "session_token",
|
|
305
|
+
providerAccountId: "provider_account_id",
|
|
306
|
+
emailVerified: "email_verified",
|
|
307
|
+
}
|
|
308
|
+
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
|
|
309
|
+
|
|
310
|
+
for (const key in MAP_TO_FIRESTORE) {
|
|
311
|
+
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const identity = <T>(x: T) => x
|
|
315
|
+
|
|
316
|
+
/** @internal */
|
|
317
|
+
export function mapFieldsFactory(preferSnakeCase?: boolean) {
|
|
318
|
+
if (preferSnakeCase) {
|
|
319
|
+
return {
|
|
320
|
+
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
|
|
321
|
+
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { toDb: identity, fromDb: identity }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** @internal */
|
|
328
|
+
function getConverter<Document extends Record<string, any>>(options: {
|
|
329
|
+
excludeId?: boolean
|
|
330
|
+
preferSnakeCase?: boolean
|
|
331
|
+
}): FirebaseFirestore.FirestoreDataConverter<Document> {
|
|
332
|
+
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
toFirestore(object) {
|
|
336
|
+
const document: Record<string, unknown> = {}
|
|
337
|
+
|
|
338
|
+
for (const key in object) {
|
|
339
|
+
if (key === "id") continue
|
|
340
|
+
const value = object[key]
|
|
341
|
+
if (value !== undefined) {
|
|
342
|
+
document[mapper.toDb(key)] = value
|
|
343
|
+
} else {
|
|
344
|
+
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return document
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
fromFirestore(
|
|
352
|
+
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
|
|
353
|
+
): Document {
|
|
354
|
+
const document = snapshot.data()! // we can guarantee it exists
|
|
355
|
+
|
|
356
|
+
const object: Record<string, unknown> = {}
|
|
357
|
+
|
|
358
|
+
if (!options?.excludeId) {
|
|
359
|
+
object.id = snapshot.id
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const key in document) {
|
|
363
|
+
let value: any = document[key]
|
|
364
|
+
if (value instanceof Timestamp) value = value.toDate()
|
|
365
|
+
|
|
366
|
+
object[mapper.fromDb(key)] = value
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return object as Document
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** @internal */
|
|
375
|
+
export async function getOneDoc<T>(
|
|
376
|
+
querySnapshot: FirebaseFirestore.Query<T>
|
|
377
|
+
): Promise<T | null> {
|
|
378
|
+
const querySnap = await querySnapshot.limit(1).get()
|
|
379
|
+
return querySnap.docs[0]?.data() ?? null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @internal */
|
|
383
|
+
async function deleteDocs<T>(
|
|
384
|
+
querySnapshot: FirebaseFirestore.Query<T>
|
|
385
|
+
): Promise<void> {
|
|
386
|
+
const querySnap = await querySnapshot.get()
|
|
387
|
+
for (const doc of querySnap.docs) {
|
|
388
|
+
await doc.ref.delete()
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** @internal */
|
|
393
|
+
export async function getDoc<T>(
|
|
394
|
+
docRef: FirebaseFirestore.DocumentReference<T>
|
|
395
|
+
): Promise<T | null> {
|
|
396
|
+
const docSnap = await docRef.get()
|
|
397
|
+
return docSnap.data() ?? null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** @internal */
|
|
401
|
+
export function collectionsFactory(
|
|
402
|
+
db: FirebaseFirestore.Firestore,
|
|
403
|
+
preferSnakeCase = false
|
|
404
|
+
) {
|
|
405
|
+
return {
|
|
406
|
+
users: db
|
|
407
|
+
.collection("users")
|
|
408
|
+
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
|
|
409
|
+
sessions: db
|
|
410
|
+
.collection("sessions")
|
|
411
|
+
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
|
|
412
|
+
accounts: db
|
|
413
|
+
.collection("accounts")
|
|
414
|
+
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
|
|
415
|
+
verification_tokens: db
|
|
416
|
+
.collection(
|
|
417
|
+
preferSnakeCase ? "verification_tokens" : "verificationTokens"
|
|
418
|
+
)
|
|
419
|
+
.withConverter(
|
|
420
|
+
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
|
|
421
|
+
),
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
|
|
427
|
+
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```ts title="lib/firestore.ts"
|
|
431
|
+
* import { initFirestore } from "@auth/firebase-adapter"
|
|
432
|
+
* import { cert } from "firebase-admin/app"
|
|
433
|
+
*
|
|
434
|
+
* export const firestore = initFirestore({
|
|
435
|
+
* credential: cert({
|
|
436
|
+
* projectId: process.env.FIREBASE_PROJECT_ID,
|
|
437
|
+
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
|
438
|
+
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
|
439
|
+
* })
|
|
440
|
+
* })
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
export function initFirestore(
|
|
444
|
+
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
|
|
445
|
+
) {
|
|
446
|
+
const apps = getApps()
|
|
447
|
+
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
|
|
448
|
+
|
|
449
|
+
if (app) return getFirestore(app)
|
|
450
|
+
|
|
451
|
+
return initializeFirestore(initializeApp(options, options.name))
|
|
452
|
+
}
|