mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
3 Commits
@auth/dgra
...
fix/comply
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cb75a5b0a | ||
|
|
c1d8b446aa | ||
|
|
d10d6e20ca |
@@ -22,20 +22,22 @@
|
||||
* @module @next-auth/firebase-adapter
|
||||
*/
|
||||
|
||||
import { type AppOptions } from "firebase-admin"
|
||||
import { Firestore } from "firebase-admin/firestore"
|
||||
import { type AppOptions, getApps, initializeApp } from "firebase-admin/app"
|
||||
|
||||
import type { Adapter, AdapterUser } from "next-auth/adapters"
|
||||
import {
|
||||
collestionsFactory,
|
||||
deleteDocs,
|
||||
initFirestore,
|
||||
getDoc,
|
||||
getOneDoc,
|
||||
mapFieldsFactory,
|
||||
} from "./utils"
|
||||
Firestore,
|
||||
getFirestore,
|
||||
initializeFirestore,
|
||||
Timestamp,
|
||||
} from "firebase-admin/firestore"
|
||||
|
||||
export { initFirestore } from "./utils"
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
|
||||
/** Configure the Firebase Adapter. */
|
||||
export interface FirebaseAdapterConfig extends AppOptions {
|
||||
@@ -300,3 +302,156 @@ export function FirestoreAdapter(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// for consistency, store all fields as snake_case in the database
|
||||
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
|
||||
userId: "user_id",
|
||||
sessionToken: "session_token",
|
||||
providerAccountId: "provider_account_id",
|
||||
emailVerified: "email_verified",
|
||||
}
|
||||
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
|
||||
|
||||
for (const key in MAP_TO_FIRESTORE) {
|
||||
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
|
||||
}
|
||||
|
||||
const identity = <T>(x: T) => x
|
||||
|
||||
/** @internal */
|
||||
export function mapFieldsFactory(preferSnakeCase?: boolean) {
|
||||
if (preferSnakeCase) {
|
||||
return {
|
||||
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
|
||||
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
|
||||
}
|
||||
}
|
||||
return { toDb: identity, fromDb: identity }
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function getConverter<Document extends Record<string, any>>(options: {
|
||||
excludeId?: boolean
|
||||
preferSnakeCase?: boolean
|
||||
}): FirebaseFirestore.FirestoreDataConverter<Document> {
|
||||
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
|
||||
|
||||
return {
|
||||
toFirestore(object) {
|
||||
const document: Record<string, unknown> = {}
|
||||
|
||||
for (const key in object) {
|
||||
if (key === "id") continue
|
||||
const value = object[key]
|
||||
if (value !== undefined) {
|
||||
document[mapper.toDb(key)] = value
|
||||
} else {
|
||||
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
|
||||
}
|
||||
}
|
||||
|
||||
return document
|
||||
},
|
||||
|
||||
fromFirestore(
|
||||
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
|
||||
): Document {
|
||||
const document = snapshot.data()! // we can guarantee it exists
|
||||
|
||||
const object: Record<string, unknown> = {}
|
||||
|
||||
if (!options?.excludeId) {
|
||||
object.id = snapshot.id
|
||||
}
|
||||
|
||||
for (const key in document) {
|
||||
let value: any = document[key]
|
||||
if (value instanceof Timestamp) value = value.toDate()
|
||||
|
||||
object[mapper.fromDb(key)] = value
|
||||
}
|
||||
|
||||
return object as Document
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getOneDoc<T>(
|
||||
querySnapshot: FirebaseFirestore.Query<T>
|
||||
): Promise<T | null> {
|
||||
const querySnap = await querySnapshot.limit(1).get()
|
||||
return querySnap.docs[0]?.data() ?? null
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async function deleteDocs<T>(
|
||||
querySnapshot: FirebaseFirestore.Query<T>
|
||||
): Promise<void> {
|
||||
const querySnap = await querySnapshot.get()
|
||||
for (const doc of querySnap.docs) {
|
||||
await doc.ref.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getDoc<T>(
|
||||
docRef: FirebaseFirestore.DocumentReference<T>
|
||||
): Promise<T | null> {
|
||||
const docSnap = await docRef.get()
|
||||
return docSnap.data() ?? null
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function collestionsFactory(
|
||||
db: FirebaseFirestore.Firestore,
|
||||
preferSnakeCase = false
|
||||
) {
|
||||
return {
|
||||
users: db
|
||||
.collection("users")
|
||||
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
|
||||
sessions: db
|
||||
.collection("sessions")
|
||||
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
|
||||
accounts: db
|
||||
.collection("accounts")
|
||||
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
|
||||
verification_tokens: db
|
||||
.collection(
|
||||
preferSnakeCase ? "verification_tokens" : "verificationTokens"
|
||||
)
|
||||
.withConverter(
|
||||
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
|
||||
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts title="lib/firestore.ts"
|
||||
* import { initFirestore } from "@next-auth/firebase-adapter"
|
||||
* import { cert } from "firebase-admin/app"
|
||||
*
|
||||
* export const firestore = initFirestore({
|
||||
* credential: cert({
|
||||
* projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function initFirestore(
|
||||
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
|
||||
) {
|
||||
const apps = getApps()
|
||||
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
|
||||
|
||||
if (app) return getFirestore(app)
|
||||
|
||||
return initializeFirestore(initializeApp(options, options.name))
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { AppOptions, getApps, initializeApp } from "firebase-admin/app"
|
||||
|
||||
import {
|
||||
getFirestore,
|
||||
initializeFirestore,
|
||||
Timestamp,
|
||||
} from "firebase-admin/firestore"
|
||||
|
||||
import type {
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
import { FirebaseAdapterConfig } from "."
|
||||
|
||||
// for consistency, store all fields as snake_case in the database
|
||||
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
|
||||
userId: "user_id",
|
||||
sessionToken: "session_token",
|
||||
providerAccountId: "provider_account_id",
|
||||
emailVerified: "email_verified",
|
||||
}
|
||||
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
|
||||
|
||||
for (const key in MAP_TO_FIRESTORE) {
|
||||
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
|
||||
}
|
||||
|
||||
const identity = <T>(x: T) => x
|
||||
|
||||
/** @internal */
|
||||
export function mapFieldsFactory(preferSnakeCase?: boolean) {
|
||||
if (preferSnakeCase) {
|
||||
return {
|
||||
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
|
||||
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
|
||||
}
|
||||
}
|
||||
return { toDb: identity, fromDb: identity }
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getConverter<Document extends Record<string, any>>(options: {
|
||||
excludeId?: boolean
|
||||
preferSnakeCase?: boolean
|
||||
}): FirebaseFirestore.FirestoreDataConverter<Document> {
|
||||
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
|
||||
|
||||
return {
|
||||
toFirestore(object) {
|
||||
const document: Record<string, unknown> = {}
|
||||
|
||||
for (const key in object) {
|
||||
if (key === "id") continue
|
||||
const value = object[key]
|
||||
if (value !== undefined) {
|
||||
document[mapper.toDb(key)] = value
|
||||
} else {
|
||||
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
|
||||
}
|
||||
}
|
||||
|
||||
return document
|
||||
},
|
||||
|
||||
fromFirestore(
|
||||
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
|
||||
): Document {
|
||||
const document = snapshot.data()! // we can guarantee it exists
|
||||
|
||||
const object: Record<string, unknown> = {}
|
||||
|
||||
if (!options?.excludeId) {
|
||||
object.id = snapshot.id
|
||||
}
|
||||
|
||||
for (const key in document) {
|
||||
let value: any = document[key]
|
||||
if (value instanceof Timestamp) value = value.toDate()
|
||||
|
||||
object[mapper.fromDb(key)] = value
|
||||
}
|
||||
|
||||
return object as Document
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getOneDoc<T>(
|
||||
querySnapshot: FirebaseFirestore.Query<T>
|
||||
): Promise<T | null> {
|
||||
const querySnap = await querySnapshot.limit(1).get()
|
||||
return querySnap.docs[0]?.data() ?? null
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function deleteDocs<T>(
|
||||
querySnapshot: FirebaseFirestore.Query<T>
|
||||
): Promise<void> {
|
||||
const querySnap = await querySnapshot.get()
|
||||
for (const doc of querySnap.docs) {
|
||||
await doc.ref.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getDoc<T>(
|
||||
docRef: FirebaseFirestore.DocumentReference<T>
|
||||
): Promise<T | null> {
|
||||
const docSnap = await docRef.get()
|
||||
return docSnap.data() ?? null
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function collestionsFactory(
|
||||
db: FirebaseFirestore.Firestore,
|
||||
preferSnakeCase = false
|
||||
) {
|
||||
return {
|
||||
users: db
|
||||
.collection("users")
|
||||
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
|
||||
sessions: db
|
||||
.collection("sessions")
|
||||
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
|
||||
accounts: db
|
||||
.collection("accounts")
|
||||
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
|
||||
verification_tokens: db
|
||||
.collection(
|
||||
preferSnakeCase ? "verification_tokens" : "verificationTokens"
|
||||
)
|
||||
.withConverter(
|
||||
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
|
||||
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts title="lib/firestore.ts"
|
||||
* import { initFirestore } from "@next-auth/firebase-adapter"
|
||||
* import { cert } from "firebase-admin/app"
|
||||
*
|
||||
* export const firestore = initFirestore({
|
||||
* credential: cert({
|
||||
* projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function initFirestore(
|
||||
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
|
||||
) {
|
||||
const apps = getApps()
|
||||
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
|
||||
|
||||
if (app) return getFirestore(app)
|
||||
|
||||
return initializeFirestore(initializeApp(options, options.name))
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getDoc,
|
||||
getOneDoc,
|
||||
mapFieldsFactory,
|
||||
} from "../src/utils"
|
||||
} from "../src"
|
||||
|
||||
describe.each([
|
||||
{ namingStrategy: "snake_case" },
|
||||
|
||||
Reference in New Issue
Block a user