Skip to content

Instantly share code, notes, and snippets.

@kripod
Created April 28, 2021 10:41
Show Gist options
  • Save kripod/35ccf5319c8477de6f71c1877c7de320 to your computer and use it in GitHub Desktop.
Save kripod/35ccf5319c8477de6f71c1877c7de320 to your computer and use it in GitHub Desktop.
Works well with next-auth@3.18.0
import type * as Prisma from "@prisma/client";
import { createHash, randomBytes } from "crypto";
import type { Adapter } from "next-auth/adapters";
import {
CreateSessionError,
CreateUserError,
CreateVerificationRequestError,
DeleteSessionError,
DeleteUserError,
DeleteVerificationRequestError,
GetSessionError,
GetUserByEmailError,
GetUserByIdError,
GetUserByProviderAccountIdError,
GetVerificationRequestError,
LinkAccountError,
UnlinkAccountError,
UpdateSessionError,
UpdateUserError,
} from "next-auth/errors";
const defaultSessionMaxAgeMs = 30 * 24 * 60 * 60 * 1000;
const defaultSessionUpdateAgeMs = 24 * 60 * 60 * 1000;
function verificationRequestToken({
token,
secret,
}: {
token: string;
secret: string;
}) {
// TODO: Use bcrypt or a more secure method
return createHash("sha256").update(`${token}${secret}`).digest("hex");
}
type PrismaAdapterConfig<P extends Prisma.PrismaClient> = {
prisma: P;
};
export function PrismaAdapter<P extends Prisma.PrismaClient>({
prisma,
}: PrismaAdapterConfig<P>): Adapter {
return {
getAdapter: async ({ logger, ...appOptions }) => {
function debug(debugCode: string, ...args: unknown[]) {
logger.debug(`PRISMA_${debugCode}`, ...args);
}
if (!appOptions.session.maxAge) {
debug(
"GET_ADAPTER",
"Session expiry not configured (defaulting to 30 days)",
);
}
if (!appOptions.session.updateAge) {
debug(
"GET_ADAPTER",
"Session update age not configured (defaulting to 1 day)",
);
}
const sessionMaxAgeMs = appOptions.session.maxAge
? appOptions.session.maxAge * 1000
: defaultSessionMaxAgeMs;
const sessionUpdateAgeMs = appOptions.session.updateAge
? appOptions.session.updateAge * 1000
: defaultSessionUpdateAgeMs;
return {
createUser: async (profile) => {
debug("CREATE_USER", profile);
try {
return await prisma.user.create({
data: {
name: profile.name,
email: profile.email,
image: profile.image,
emailVerified: profile.emailVerified?.toISOString() || null,
},
});
} catch (error) {
logger.error("CREATE_USER_ERROR", error);
throw new CreateUserError(error);
}
},
getUser: async (id) => {
debug("GET_USER_BY_ID", id);
try {
return await prisma.user.findUnique({ where: { id } });
} catch (error) {
logger.error("GET_USER_BY_ID_ERROR", error);
throw new GetUserByIdError(error);
}
},
getUserByEmail: async (email) => {
debug("GET_USER_BY_EMAIL", email);
try {
if (!email) return null;
return await prisma.user.findUnique({ where: { email } });
} catch (error) {
logger.error("GET_USER_BY_EMAIL_ERROR", error);
throw new GetUserByEmailError(error);
}
},
getUserByProviderAccountId: async (providerId, providerAccountId) => {
debug(
"GET_USER_BY_PROVIDER_ACCOUNT_ID",
providerId,
providerAccountId,
);
try {
const account = await prisma.account.findUnique({
where: {
providerId_providerAccountId: { providerId, providerAccountId },
},
select: { user: true },
});
return account ? account.user : null;
} catch (error) {
logger.error("GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR", error);
throw new GetUserByProviderAccountIdError(error);
}
},
updateUser: async (user) => {
debug("UPDATE_USER", user);
try {
return await prisma.user.update({
where: { id: user.id },
data: {
name: user.name,
email: user.email,
image: user.image,
emailVerified: user.emailVerified?.toISOString() || null,
},
});
} catch (error) {
logger.error("UPDATE_USER_ERROR", error);
throw new UpdateUserError(error);
}
},
/* TODO: Remove type annotations */
deleteUser: async (userId: string) => {
debug("DELETE_USER", userId);
try {
return await prisma.user.delete({ where: { id: userId } });
} catch (error) {
logger.error("DELETE_USER_ERROR", error);
throw new DeleteUserError(error);
}
},
linkAccount: async (
userId,
providerId,
providerType,
providerAccountId,
refreshToken,
accessToken,
accessTokenExpires,
) => {
debug(
"LINK_ACCOUNT",
userId,
providerId,
providerType,
providerAccountId,
refreshToken,
accessToken,
accessTokenExpires,
);
try {
await prisma.account.create({
data: {
userId,
providerId,
providerType,
providerAccountId,
refreshToken,
accessToken,
accessTokenExpires:
accessTokenExpires != null
? new Date(accessTokenExpires)
: null,
},
});
} catch (error) {
logger.error("LINK_ACCOUNT_ERROR", error);
throw new LinkAccountError(error);
}
},
/* TODO: Remove type annotations */
unlinkAccount: async (
userId: string,
providerId: string,
providerAccountId: string,
) => {
debug("UNLINK_ACCOUNT", userId, providerId, providerAccountId);
try {
return await prisma.account.delete({
where: {
providerId_providerAccountId: { providerId, providerAccountId },
},
});
} catch (error) {
logger.error("UNLINK_ACCOUNT_ERROR", error);
throw new UnlinkAccountError(error);
}
},
createSession: async (user) => {
debug("CREATE_SESSION", user);
try {
return await prisma.session.create({
data: {
userId: user.id,
expires: new Date(Date.now() + sessionMaxAgeMs),
sessionToken: randomBytes(32).toString("hex"),
accessToken: randomBytes(32).toString("hex"),
},
});
} catch (error) {
logger.error("CREATE_SESSION_ERROR", error);
throw new CreateSessionError(error);
}
},
getSession: async (sessionToken) => {
debug("GET_SESSION", sessionToken);
try {
const session = await prisma.session.findUnique({
where: { sessionToken },
});
if (session && session.expires < new Date()) {
await prisma.session.delete({ where: { sessionToken } });
return null;
}
return session;
} catch (error) {
logger.error("GET_SESSION_ERROR", error);
throw new GetSessionError(error);
}
},
updateSession: async (session, force) => {
debug("UPDATE_SESSION", session);
try {
if (
!force &&
Number(session.expires) - sessionMaxAgeMs + sessionUpdateAgeMs >
Date.now()
) {
return null;
}
return await prisma.session.update({
where: { id: session.id },
data: {
expires: new Date(Date.now() + sessionMaxAgeMs),
},
});
} catch (error) {
logger.error("UPDATE_SESSION_ERROR", error);
throw new UpdateSessionError(error);
}
},
deleteSession: async (sessionToken) => {
debug("DELETE_SESSION", sessionToken);
try {
await prisma.session.delete({ where: { sessionToken } });
} catch (error) {
logger.error("DELETE_SESSION_ERROR", error);
throw new DeleteSessionError(error);
}
},
createVerificationRequest: async (
identifier,
url,
token,
secret,
provider,
) => {
debug("CREATE_VERIFICATION_REQUEST", identifier);
try {
const hashedToken = verificationRequestToken({ token, secret });
const verificationRequest = await prisma.verificationRequest.create(
{
data: {
identifier,
token: hashedToken,
expires: new Date(
Date.now() +
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
provider.maxAge! * 1000,
),
},
},
);
await provider.sendVerificationRequest({
identifier,
url,
token,
baseUrl: appOptions.baseUrl,
provider,
});
return verificationRequest;
} catch (error) {
logger.error("CREATE_VERIFICATION_REQUEST_ERROR", error);
throw new CreateVerificationRequestError(error);
}
},
getVerificationRequest: async (identifier, token, secret) => {
debug("GET_VERIFICATION_REQUEST", identifier, token);
try {
const hashedToken = verificationRequestToken({ token, secret });
const verificationRequest = await prisma.verificationRequest.findUnique(
{
where: { identifier_token: { identifier, token: hashedToken } },
},
);
if (
verificationRequest &&
verificationRequest.expires < new Date()
) {
await prisma.verificationRequest.delete({
where: { identifier_token: { identifier, token: hashedToken } },
});
return null;
}
return verificationRequest;
} catch (error) {
logger.error("GET_VERIFICATION_REQUEST_ERROR", error);
throw new GetVerificationRequestError(error);
}
},
deleteVerificationRequest: async (identifier, token, secret) => {
debug("DELETE_VERIFICATION_REQUEST", identifier, token);
try {
const hashedToken = verificationRequestToken({ token, secret });
await prisma.verificationRequest.delete({
where: { identifier_token: { identifier, token: hashedToken } },
});
} catch (error) {
logger.error("DELETE_VERIFICATION_REQUEST_ERROR", error);
throw new DeleteVerificationRequestError(error);
}
},
};
},
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment