UNPKG

@heymarco/next-auth

Version:

A complete authentication solution for web applications.

1,169 lines (907 loc) 50.2 kB
// ORMs: import type { PrismaClient, } from '@prisma/client' // cryptos: import { default as bcrypt, } from 'bcrypt' import { customAlphabet, } from 'nanoid/async' // internals: import type { // types: Awaitable, // models: Adapter, AdapterUser, AdapterSession, AdapterAccount, AdapterCredentials, AdapterRole, } from './types.js' // types: export type CredentialsSignIn = Record<'username'|'password', string> export interface CreatePasswordResetTokenData { passwordResetToken : string user : AdapterUser } export interface ValidatePasswordResetTokenData { email : string username : string|null } export interface RegisterUserData { userId : string emailConfirmationToken : string|null } // options: export interface CredentialsSignInOptions { now ?: Date requireEmailVerified ?: boolean failureMaxAttempts ?: number|null failureLockDuration ?: number } export interface CreatePasswordResetTokenOptions { now ?: Date resetThrottle ?: number resetMaxAge ?: number } export interface ValidatePasswordResetTokenOptions { now ?: Date } export interface UsePasswordResetTokenOptions { now ?: Date } export interface RegisterUserOptions { requireEmailVerified ?: boolean } export interface MarkEmailAsVerifiedOptions { now ?: Date } export interface UseEmailConfirmationTokenOptions { now ?: Date } export interface ModelOptions<TPrisma extends PrismaClient> { modelUser ?: Extract<keyof TPrisma, string> modelRole ?: Extract<keyof TPrisma, string> | null modelAccount ?: Extract<keyof TPrisma, string> modelSession ?: Extract<keyof TPrisma, string> modelCredentials ?: Extract<keyof TPrisma, string> modelPasswordResetToken ?: Extract<keyof TPrisma, string> | null modelEmailConfirmationToken ?: Extract<keyof TPrisma, string> | null modelUserRefRoleId ?: string | null modelAccountRefUserId ?: string modelSessionRefUserId ?: string modelCredentialsRefUserId ?: string modelPasswordResetTokenRefUserId ?: string | null modelEmailConfirmationTokenRefUserId ?: string | null } export interface AdapterWithCredentials extends Adapter { // sign in: credentialsSignIn : (credentials : CredentialsSignIn , options?: CredentialsSignInOptions ) => Awaitable<AdapterUser|false|Date|null> // password resets: createPasswordResetToken : (usernameOrEmail : string , options?: CreatePasswordResetTokenOptions ) => Awaitable<CreatePasswordResetTokenData|Date|null> validatePasswordResetToken : (passwordResetToken : string , options?: ValidatePasswordResetTokenOptions) => Awaitable<ValidatePasswordResetTokenData|null> usePasswordResetToken : (passwordResetToken : string, password: string , options?: UsePasswordResetTokenOptions ) => Awaitable<boolean> // registrations: checkUsernameAvailability : (username : string ) => Awaitable<boolean> checkEmailAvailability : (email : string ) => Awaitable<boolean> registerUser : (name: string , email : string, username: string, password: string, options?: RegisterUserOptions ) => Awaitable<RegisterUserData> // email verifications: markEmailAsVerified : (userId : string , options?: MarkEmailAsVerifiedOptions ) => Awaitable<void> useEmailConfirmationToken : (emailConfirmationToken : string , options?: UseEmailConfirmationTokenOptions ) => Awaitable<boolean> // user credentials: getCredentialsByUserId : (userId : string ) => Awaitable<AdapterCredentials|null> getCredentialsByUserEmail : (userEmail : string ) => Awaitable<AdapterCredentials|null> // user roles: getRoleByUserId : (userId : string ) => Awaitable<AdapterRole|null> getRoleByUserEmail : (userEmail : string ) => Awaitable<AdapterRole|null> } export const PrismaAdapterWithCredentials = <TPrisma extends PrismaClient>(prisma: TPrisma, options?: ModelOptions<TPrisma>): AdapterWithCredentials => { // options: options ??= {}; const { modelUser = 'user', modelRole = 'role', modelAccount = 'account', modelSession = 'session', modelCredentials = 'credentials', modelPasswordResetToken = 'passwordResetToken', modelEmailConfirmationToken = 'emailConfirmationToken', } = options; const { modelUserRefRoleId = `${modelRole}Id`, modelAccountRefUserId = `${modelUser}Id`, modelSessionRefUserId = `${modelUser}Id`, modelCredentialsRefUserId = `${modelUser}Id`, modelPasswordResetTokenRefUserId = `${modelUser}Id`, modelEmailConfirmationTokenRefUserId = `${modelUser}Id`, } = options; return { // CRUD users: createUser : async (userData ) => { const { name, ...restUserData} = userData; if (!name) throw Error('`name` is required.'); return (prisma[modelUser] as any).create({ data : { ...restUserData, name, }, }); }, getUser : async (userId ) => { return (prisma[modelUser] as any).findUnique({ where : { id : userId, }, }); }, getUserByEmail : async (userEmail ) => { return (prisma[modelUser] as any).findUnique({ where : { email : userEmail, }, }); }, getUserByAccount : async (userAccount ) => { const { provider, providerAccountId, } = userAccount; return prisma.$transaction(async (prismaTransaction): Promise<AdapterUser|null> => { const account = await ((prismaTransaction as TPrisma)[modelAccount] as any).findFirst({ where : { provider, providerAccountId, }, select : { [modelAccountRefUserId] : true, }, }); if (!account) return null; return ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { id : account[modelAccountRefUserId], }, }); }); }, updateUser : async (userData ) => { const { id, name, ...restUserData} = userData; if ((name !== undefined) && !name) throw Error('`name` is required.'); return (prisma[modelUser] as any).update({ where : { id, }, data : { ...restUserData, name, }, }); }, deleteUser : async (userId ) => { return (prisma[modelUser] as any).delete({ where : { id : userId, }, }); }, // CRUD sessions: createSession : async (sessionData ) => { const { userId : userId, ...restSessionData} = sessionData; return (prisma[modelSession] as any).create({ data : { ...restSessionData, [modelSessionRefUserId] : userId, }, }); }, getSessionAndUser : async (sessionToken ) => { return prisma.$transaction(async (prismaTransaction): Promise<{session: AdapterSession, user: AdapterUser}|null> => { const session = await ((prismaTransaction as TPrisma)[modelSession] as any).findUnique({ where : { sessionToken, }, }); if (!session) return null; const user = await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { id : session[modelSessionRefUserId], }, }); if (!user) return null; return { session, user, }; }); }, updateSession : async (sessionData ) => { const { userId : userId, ...restSessionData} = sessionData; return (prisma[modelSession] as any).update({ where : { sessionToken : restSessionData.sessionToken, }, data : { ...restSessionData, [modelSessionRefUserId] : userId, }, }); }, deleteSession : async (sessionToken ) => { return (prisma[modelSession] as any).delete({ where : { sessionToken, }, }); }, // CRUD accounts: linkAccount : async (accountData ) => { const { userId : userId, ...restAccountData} = accountData; const account = await (prisma[modelAccount] as any).create({ data : { ...restAccountData, [modelAccountRefUserId] : userId, }, }); return account as AdapterAccount; }, unlinkAccount : async (userAccount ) => { const { provider, providerAccountId, } = userAccount; const deletedAccount = await prisma.$transaction(async (prismaTransaction) => { const account = await ((prismaTransaction as TPrisma)[modelAccount] as any).findFirst({ where : { provider, providerAccountId, }, select : { id : true, }, }); if (!account) return undefined; return await ((prismaTransaction as TPrisma)[modelAccount] as any).delete({ where : { id : account?.id, }, }); }); return deletedAccount as AdapterAccount; }, // token verifications: createVerificationToken : undefined, useVerificationToken : undefined, // -------------------------------------------------------------------------------------- // sign in: credentialsSignIn : async (credentials , options) => { // options: const { now = new Date(), requireEmailVerified = true, failureMaxAttempts = null, failureLockDuration = 0.25, } = options ?? {}; // credentials: const { username : usernameOrEmailRaw, password, } = credentials; // normalizations: const usernameOrEmail = usernameOrEmailRaw.toLowerCase(); // a database transaction for preventing multiple bulk login for bypassing failureMaxAttempts (forced to be a sequential operation): // an atomic transaction of [`find user's credentials by username (or email)`, `update the failureAttempts & lockedAt`]: return prisma.$transaction(async (prismaTransaction): Promise<AdapterUser|false|Date|null> => { // AdapterUser: succeeded; false: email is not verified; Date: account locked up; null: invalid username and/or password // find user data + credentials by given username (or email): const userWithCredentials = ( usernameOrEmail.includes('@') // if username contains '@' => treat as email, otherwise regular username ? await (async () => { // first: find the user: const user = await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { email : usernameOrEmail, }, }); if (!user) return null; // then: find the related credentials: const credentials = await ((prismaTransaction as TPrisma)[modelCredentials] as any).findUnique({ where : { [modelCredentialsRefUserId] : user.id, }, select : { id : true, // required: for further updating failure_counter and/or lockedAt failureAttempts : true, // required: for inspecting the failureMaxAttempts constraint lockedAt : true, // required: for inspecting the failureLockDuration constraint password : true, // required: for password hash comparison }, }); // then: combine them: return { user, credentials, }; })() : await (async () => { // first: find the credentials: const credentials = await ((prismaTransaction as TPrisma)[modelCredentials] as any).findUnique({ where : { username : usernameOrEmail, }, select : { id : true, // required: for further updating failure_counter and/or lockedAt failureAttempts : true, // required: for inspecting the failureMaxAttempts constraint lockedAt : true, // required: for inspecting the failureLockDuration constraint password : true, // required: for password hash comparison [modelCredentialsRefUserId] : true, // required: for finding the related user }, }); if (!credentials) return null; // then: find the user: const user = await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { id : credentials[modelCredentialsRefUserId], }, }); // then: combine them: return { user, credentials, }; })() ); if (!userWithCredentials) return null; // no user found with given username (or email) => return null (not found) // exclude credentials property to increase security strength: const { user : user, credentials : expectedCredentials, } = userWithCredentials; // check if user's email was verified: if (requireEmailVerified) { if (user.emailVerified === null) return false; } // if // verify whether the credentials does exist: if (!expectedCredentials) return null; // no credential was configured on the user's account => unable to compare => return null (assumes as password do not match) // verify whether the credentials is not locked out: { const lockedAt = expectedCredentials.lockedAt ?? null; if (lockedAt !== null) { const lockedUntil = new Date(/* since: */ lockedAt.valueOf() + /* duration: */ (failureLockDuration * 60 * 60 * 1000 /* convert hours to milliseconds */)); if (lockedUntil > now) { // still in locked period => return the released_out date: return lockedUntil; } else { // the locked period expired => unlock & reset the failure_counter: await ((prismaTransaction as TPrisma)[modelCredentials] as any).update({ where : { id : expectedCredentials.id, }, data : { failureAttempts : null, // reset the failure_counter lockedAt : null, // reset the lock_date constraint }, select : { id : true, }, }); expectedCredentials.failureAttempts = null; // reset this variable too expectedCredentials.lockedAt = null; // reset this variable too } // if } // if } // perform password hash comparison: { const isSuccess = !!password && !!expectedCredentials.password && await bcrypt.compare(password, expectedCredentials.password); if (isSuccess) { // signIn attemp succeeded: if (expectedCredentials.failureAttempts !== null) { // there are some failure attempts => reset // reset the failure_counter: await ((prismaTransaction as TPrisma)[modelCredentials] as any).update({ where : { id : expectedCredentials.id, }, data : { failureAttempts : null, // reset the failure_counter }, select : { id : true, }, }); } // if } else { // signIn attemp failed: if (failureMaxAttempts !== null) { // there are a limit of failure signIn attempts // increase the failure_counter and/or lockedAt: const currentFailureAttempts : number = (expectedCredentials.failureAttempts ?? 0) + 1; const isLocked : boolean = (currentFailureAttempts >= failureMaxAttempts); await ((prismaTransaction as TPrisma)[modelCredentials] as any).update({ where : { id : expectedCredentials.id, }, data : { failureAttempts : currentFailureAttempts, lockedAt : ( !isLocked // if under limit ? undefined // do not lock now, the user still have a/some chance(s) to retry : now // lock now, too many retries ), }, select : { id : true, }, }); if (isLocked) return new Date(/* since: */ now.valueOf() + /* duration: */ (failureLockDuration * 60 * 60 * 1000 /* convert hours to milliseconds */)); // the credentials has been locked } // if } // if if (!isSuccess) return null; // password hash comparison do not match => return null (password do not match) } // the verification passed => authorized => return An `AdapterUser` object: return user; }); }, // password resets: createPasswordResetToken : async (usernameOrEmail , options) => { // conditions: const hasPasswordResetToken = !!modelPasswordResetToken && (modelPasswordResetToken in prisma); if (!hasPasswordResetToken) return null; // options: const { now = new Date(), resetThrottle, resetMaxAge = 24, } = options ?? {}; // normalizations: usernameOrEmail = usernameOrEmail.toLowerCase(); // generate the passwordResetToken data: const passwordResetToken = await customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', 16)(); const passwordResetMaxAge = resetMaxAge * 60 * 60 * 1000 /* convert hours to milliseconds */; const passwordResetExpiry = new Date(now.valueOf() + passwordResetMaxAge); // an atomic transaction of [`find user by username (or email)`, `find passwordResetToken by user id`, `create/update the new passwordResetToken`]: const user = await prisma.$transaction(async (prismaTransaction): Promise<AdapterUser|Date|null> => { // AdapterUser: succeeded; Date: reset request is too frequent; null: invalid username or email // find user id by given username (or email): const userId = ( usernameOrEmail.includes('@') // if username contains '@' => treat as email, otherwise regular username ? (await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { email : usernameOrEmail, }, select : { id : true, // required: for id key }, }))?.id : (await ((prismaTransaction as TPrisma)[modelCredentials] as any).findUnique({ where : { username : usernameOrEmail, }, select : { [modelCredentialsRefUserId] : true, // required: for id key }, }))?.[modelCredentialsRefUserId] ); if (userId === undefined) return null; // limits the rate of passwordResetToken request: if (resetThrottle !== undefined) { // there are a limit of passwordResetToken request // find the last request date (if found) of passwordResetToken by user id: const {updatedAt: lastRequestDate} = await ((prismaTransaction as TPrisma)[modelPasswordResetToken] as any).findUnique({ where : { [modelPasswordResetTokenRefUserId as any] : userId, }, select : { updatedAt : true, }, }) ?? {}; // calculate how often the last request of passwordResetToken: if (!!lastRequestDate) { const minInterval = resetThrottle * 60 * 60 * 1000 /* convert hours to milliseconds */; if ((now.valueOf() - lastRequestDate.valueOf()) < minInterval) { // the request interval is shorter than minInterval => reject the request // the reset request is too frequent => reject: return new Date(lastRequestDate.valueOf() + minInterval); } // if } // if } // if // create/update the passwordResetToken record and get the related user name & email: const relatedPasswordResetToken = await ((prismaTransaction as TPrisma)[modelPasswordResetToken] as any).upsert({ where : { [modelPasswordResetTokenRefUserId as any] : userId, }, create : { [modelPasswordResetTokenRefUserId as any] : userId, expiresAt : passwordResetExpiry, token : passwordResetToken, }, update : { expiresAt : passwordResetExpiry, token : passwordResetToken, }, select : { [modelPasswordResetTokenRefUserId as any] : true, }, }); if (!relatedPasswordResetToken) return null; return ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { id : relatedPasswordResetToken[modelPasswordResetTokenRefUserId as any], }, }); }); if (!user || (user instanceof Date)) return user; return { passwordResetToken, user, }; }, validatePasswordResetToken : async (passwordResetToken , options) => { // conditions: const hasPasswordResetToken = !!modelPasswordResetToken && (modelPasswordResetToken in prisma); if (!hasPasswordResetToken) return null; // options: const { now = new Date(), } = options ?? {}; return prisma.$transaction(async (prismaTransaction): Promise<ValidatePasswordResetTokenData|null> => { const relatedPasswordResetToken = await ((prismaTransaction as TPrisma)[modelPasswordResetToken] as any).findUnique({ where : { token : passwordResetToken, expiresAt : { gt : now, // not expired yet (expires in the future) }, }, select : { [modelPasswordResetTokenRefUserId as any] : true, }, }); if (!relatedPasswordResetToken) return null; const [user, credentials] = await Promise.all([ ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { id : relatedPasswordResetToken[modelPasswordResetTokenRefUserId as any], }, select : { email : true, }, }), ((prismaTransaction as TPrisma)[modelCredentials] as any).findUnique({ where : { [modelCredentialsRefUserId] : relatedPasswordResetToken[modelPasswordResetTokenRefUserId as any], }, select : { username : true, }, }), ]); if (!user) return null; if (!credentials) return null; return { email : user.email, username : credentials.username || null, }; }); }, usePasswordResetToken : async (passwordResetToken, password: string, options) => { // conditions: const hasPasswordResetToken = !!modelPasswordResetToken && (modelPasswordResetToken in prisma); if (!hasPasswordResetToken) return false; // options: const { now = new Date(), } = options ?? {}; // generate the hashed password: const hashedPassword = await bcrypt.hash(password, 10); // an atomic transaction of [`find user id by passwordResetToken`, `delete current passwordResetToken record`, `create/update user's credentials`]: return prisma.$transaction(async (prismaTransaction): Promise<boolean> => { // find the existance of passwordResetToken record by given passwordResetToken: const relatedPasswordResetToken = await ((prismaTransaction as TPrisma)[modelPasswordResetToken] as any).findUnique({ where : { token : passwordResetToken, expiresAt : { gt : now, // not expired yet (expires in the future) }, }, select : { id : true, [modelPasswordResetTokenRefUserId as any] : true, }, }); if (!relatedPasswordResetToken) { // there is no passwordResetToken record with related passwordResetToken // report the error: return false; } // if await Promise.all([ // delete the current passwordResetToken record so it cannot be re-use again: await ((prismaTransaction as TPrisma)[modelPasswordResetToken] as any).deleteMany({ where : { id : relatedPasswordResetToken.id, }, }), // create/update user's credentials: ((prismaTransaction as TPrisma)[modelCredentials] as any).upsert({ where : { [modelCredentialsRefUserId] : relatedPasswordResetToken[modelPasswordResetTokenRefUserId as any], }, create : { [modelCredentialsRefUserId] : relatedPasswordResetToken[modelPasswordResetTokenRefUserId as any], password : hashedPassword, }, update : { password : hashedPassword, }, select : { id : true, }, }), // resetting password is also intrinsically verifies the email: await ((prismaTransaction as TPrisma)[modelUser] as any).updateMany({ where : { id : relatedPasswordResetToken[modelPasswordResetTokenRefUserId as any], // unique, guarantees only update one or zero }, data : { emailVerified : now, }, }), ]); // report the success: return true; }); }, // registrations: checkUsernameAvailability : async (username ) => { // normalizations: username = username.toLowerCase(); // database query: return !(await (prisma[modelCredentials] as any).findUnique({ where : { username : username, }, select : { id : true, }, })); }, checkEmailAvailability : async (email ) => { // normalizations: email = email.toLowerCase(); // database query: return !(await (prisma[modelUser] as any).findUnique({ where : { email : email, }, select : { id : true, }, })); }, registerUser : async (name, email, username, password , options) => { // options: const { requireEmailVerified = false, } = options ?? {}; // normalizations: email = email.toLowerCase(); username = username.toLowerCase(); // generate the hashed password: const hashedPassword = await bcrypt.hash(password, 10); // generate the emailConfirmationToken data: const hasEmailConfirmationToken = !!modelEmailConfirmationToken && (modelEmailConfirmationToken in prisma); const emailConfirmationToken : string|null = (requireEmailVerified && hasEmailConfirmationToken) ? await customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', 16)() : null; // an atomic transaction of [`create/update User`, `create/update Credentials`, `create/update EmailConfirmationToken`]: return prisma.$transaction(async (prismaTransaction): Promise<RegisterUserData> => { // create/update User: const userData = { name : name, email : email, emailVerified : null, // reset }; const { id: userId } = await ((prismaTransaction as TPrisma)[modelUser] as any).upsert({ where : { email : email, emailVerified : null, }, create : userData, update : userData, select : { id : true, }, }); // create/update Credentials: const credentialsData = { failureAttempts : null, // reset lockedAt : null, // reset username : username, password : hashedPassword, }; await ((prismaTransaction as TPrisma)[modelCredentials] as any).upsert({ where : { [modelCredentialsRefUserId] : userId, }, create : { [modelCredentialsRefUserId] : userId, ...credentialsData, }, update : { ...credentialsData, }, select : { id : true, }, }); // create/update EmailConfirmationToken: if (hasEmailConfirmationToken && emailConfirmationToken) { await ((prismaTransaction as TPrisma)[modelEmailConfirmationToken] as any).upsert({ where : { [modelEmailConfirmationTokenRefUserId as any] : userId, }, create : { [modelEmailConfirmationTokenRefUserId as any] : userId, token : emailConfirmationToken, }, update : { token : emailConfirmationToken, }, select : { id : true, }, }); } // if return { userId, emailConfirmationToken, }; }); }, // email verifications: markEmailAsVerified : async (userId , options) => { // options: const { now = new Date(), } = options ?? {}; await (prisma[modelUser] as any).update({ where : { id : userId, }, data : { emailVerified : now, }, select : { id : true, }, }); }, useEmailConfirmationToken : async (emailConfirmationToken , options) => { // conditions: const hasEmailConfirmationToken = !!modelEmailConfirmationToken && (modelEmailConfirmationToken in prisma); if (!hasEmailConfirmationToken) return false; // options: const { now = new Date(), } = options ?? {}; // an atomic transaction of [`find user id by emailConfirmationToken`, `delete current emailConfirmationToken record`, `update user's emailVerified field`]: return prisma.$transaction(async (prismaTransaction): Promise<boolean> => { // find the existance of emailConfirmationToken record by given emailConfirmationToken: const relatedEmailConfirmationToken = await ((prismaTransaction as TPrisma)[modelEmailConfirmationToken] as any).findUnique({ where : { token : emailConfirmationToken, }, select : { id : true, [modelEmailConfirmationTokenRefUserId as any] : true, }, }); if (!relatedEmailConfirmationToken) { // there is no emailConfirmationToken record with related emailConfirmationToken // report the error: return false; } // if await Promise.all([ // delete the current emailConfirmationToken record so it cannot be re-use again: ((prismaTransaction as TPrisma)[modelEmailConfirmationToken] as any).deleteMany({ where : { [modelEmailConfirmationTokenRefUserId as any] : relatedEmailConfirmationToken.id, }, }), // update user's emailVerified field (if not already verified): ((prismaTransaction as TPrisma)[modelUser] as any).updateMany({ where : { id : relatedEmailConfirmationToken[modelEmailConfirmationTokenRefUserId as any], // unique, guarantees only update one or zero }, data : { emailVerified : now, }, }), ]); // report the success: return true; }); }, // user credentials: getCredentialsByUserId : async (userId ) => { // database query: return (prisma[modelCredentials] as any).findUnique({ where : { [modelCredentialsRefUserId] : userId, }, select : { username : true, // only username is shown for security purpose }, }); }, getCredentialsByUserEmail : async (userEmail ) => { // normalizations: userEmail = userEmail.toLowerCase(); // database query: return prisma.$transaction(async (prismaTransaction): Promise<AdapterCredentials|null> => { const relatedUser = await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { email : userEmail, }, select : { id : true, }, }); if (!relatedUser) return null; return ((prismaTransaction as TPrisma)[modelCredentials] as any).findUnique({ where : { [modelCredentialsRefUserId] : relatedUser.id, }, select : { username : true, // only username is shown for security purpose }, }); }); }, // user roles: getRoleByUserId : async (userId ) => { // conditions: const hasRole = !!modelRole && (modelRole in prisma); if (!hasRole) return null; // database query: return prisma.$transaction(async (prismaTransaction): Promise<AdapterRole|null> => { const relatedUser = await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { id : userId, }, select : { [modelUserRefRoleId as any] : true, }, }); if (!relatedUser) return null; if (relatedUser[modelUserRefRoleId as any] === null) return null; return ((prismaTransaction as TPrisma)[modelRole as any] as any).findUnique({ where : { id : relatedUser[modelUserRefRoleId as any], }, }); }); }, getRoleByUserEmail : async (userEmail ) => { // conditions: const hasRole = !!modelRole && (modelRole in prisma); if (!hasRole) return null; // normalizations: userEmail = userEmail.toLowerCase(); // database query: return prisma.$transaction(async (prismaTransaction): Promise<AdapterRole|null> => { const relatedUser = await ((prismaTransaction as TPrisma)[modelUser] as any).findUnique({ where : { email : userEmail, }, select : { [modelUserRefRoleId as any] : true, }, }); if (!relatedUser) return null; if (relatedUser[modelUserRefRoleId as any] === null) return null; return ((prismaTransaction as TPrisma)[modelRole as a