UNPKG

@daliboru/payload-auth-plugin-fork

Version:

Forked payload-auth-plugin with custom access token feature.

431 lines (382 loc) 9.56 kB
import { parseCookies, type PayloadRequest } from "payload" import { EmailAlreadyExistError, InvalidCredentials, InvalidRequestBodyError, MissingOrInvalidVerification, UnauthorizedAPIRequest, UserNotFoundAPIError, } from "../errors/apiErrors.js" import { hashPassword, verifyPassword } from "../utils/password.js" import { SuccessKind } from "../../types.js" import { ephemeralCode, verifyEphemeralCode } from "../utils/hash.js" import { APP_COOKIE_SUFFIX } from "../../constants.js" import { createSessionCookies, invalidateOAuthCookies, verifySessionCookie, } from "../utils/cookies.js" const redirectWithSession = async ( cookieName: string, path: string, secret: string, fields: Record<string, string | number>, request: PayloadRequest, ) => { let cookies = [] cookies = [...(await createSessionCookies(cookieName, secret, fields))] cookies = invalidateOAuthCookies(cookies) const successRedirectionURL = new URL(`${request.origin}${path}`) const res = new Response(null, { status: 302, headers: { Location: successRedirectionURL.href, }, }) for (const c of cookies) { res.headers.append("Set-Cookie", c) } return res } export const PasswordSignin = async ( pluginType: string, request: PayloadRequest, internal: { usersCollectionSlug: string }, useAdmin: boolean, secret: string, successRedirectPath: string, errorRedirectPath: string, ) => { const body = request.json && ((await request.json()) as { email: string; password: string }) if (!body?.email || !body.password) { return new InvalidRequestBodyError() } const email = body.email.toLowerCase() const { payload } = request const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email }, }, limit: 1, }) if (docs.length !== 1) { return new UserNotFoundAPIError() } const userRecord = docs[0] if (!userRecord.hashedPassword) { return new InvalidCredentials() } const isVerifed = await verifyPassword( body.password, userRecord.hashedPassword, userRecord.hashSalt, userRecord.hashIterations, ) if (!isVerifed) { return new InvalidCredentials() } const cookieName = useAdmin ? `${payload.config.cookiePrefix}-token` : `__${pluginType}-${APP_COOKIE_SUFFIX}` const signinFields = { id: userRecord.id, email, collection: internal.usersCollectionSlug, } return await redirectWithSession( cookieName, successRedirectPath, secret, signinFields, request, ) } export const PasswordSignup = async ( pluginType: string, request: PayloadRequest, internal: { usersCollectionSlug: string }, useAdmin: boolean, secret: string, successRedirectPath: string, errorRedirectPath: string, ) => { const body = request.json && ((await request.json()) as { email: string password: string allowAutoSignin?: boolean userInfo?: Record<string, unknown> }) if (!body?.email || !body.password) { return new InvalidRequestBodyError() } const email = body.email.toLowerCase() const { payload } = request const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email }, }, limit: 1, }) if (docs.length > 0) { return new EmailAlreadyExistError() } const { hash: hashedPassword, salt: hashSalt, iterations, } = await hashPassword(body.password) const userRecord = await payload.create({ collection: internal.usersCollectionSlug, data: { email, hashedPassword: hashedPassword, hashIterations: iterations, hashSalt, ...body.userInfo, }, }) if (body.allowAutoSignin) { const cookieName = useAdmin ? `${payload.config.cookiePrefix}-token` : `__${pluginType}-${APP_COOKIE_SUFFIX}` const signinFields = { id: userRecord.id, email, collection: internal.usersCollectionSlug, } return await redirectWithSession( cookieName, successRedirectPath, secret, signinFields, request, ) } return Response.json( { message: "Signed up successfully", kind: SuccessKind.Created, isSuccess: true, isError: false, }, { status: 201 }, ) } export const ForgotPasswordInit = async ( request: PayloadRequest, internal: { usersCollectionSlug: string }, emailTemplate: any, ) => { const { payload } = request const body = request.json && ((await request.json()) as { email: string }) if (!body?.email) { return new InvalidRequestBodyError() } const email = body.email.toLowerCase() const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email }, }, limit: 1, }) if (docs.length !== 1) { return new UserNotFoundAPIError() } const { code, hash } = await ephemeralCode(6, payload.secret) await payload.sendEmail({ to: email, subject: "Password recovery", html: await emailTemplate({ verificationCode: code, }), }) const res = new Response( JSON.stringify({ message: "Verification email sent", kind: SuccessKind.Created, isSuccess: true, isError: false, }), { status: 201 }, ) const verification_token_expires = new Date() verification_token_expires.setDate(verification_token_expires.getDate() + 7) await payload.update({ collection: internal.usersCollectionSlug, id: docs[0].id, data: { verificationHash: hash, verificationCode: code, verificationTokenExpire: Math.floor( verification_token_expires.getTime() / 1000, ), verificationKind: "PASSWORD_RESTORE", }, }) return res } export const ForgotPasswordVerify = async ( request: PayloadRequest, internal: { usersCollectionSlug: string }, ) => { const { payload } = request const body = request.json && ((await request.json()) as { password: string code: string }) if (!body?.password || !body.code) { return new InvalidRequestBodyError() } const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { verificationCode: { equals: body.code }, }, }) const currentDate = Date.now() if ( docs.length === 0 || docs[0].verificationCode !== body.code || !docs[0].verificationHash || Math.floor(currentDate / 1000) > docs[0].verificationTokenExpire || docs[0].verificationKind !== "PASSWORD_RESTORE" ) { return new MissingOrInvalidVerification() } const { verificationHash: hash, id: userId } = docs[0] const isVerified = await verifyEphemeralCode(body.code, hash, payload.secret) if (!isVerified) { return new MissingOrInvalidVerification() } const { hash: hashedPassword, salt: hashSalt, iterations, } = await hashPassword(body.password) await payload.update({ collection: internal.usersCollectionSlug, id: userId, data: { hashedPassword, hashSalt, hashIterations: iterations, verificationHash: null, verificationCode: null, verificationTokenExpire: null, verificationKind: null, }, }) const res = new Response( JSON.stringify({ message: "Password recovered successfully", kind: SuccessKind.Updated, isSuccess: true, isError: false, }), { status: 201 }, ) return res } export const ResetPassword = async ( cookieName: string, secret: string, internal: { usersCollectionSlug: string }, request: PayloadRequest, ) => { const { payload } = request const cookies = parseCookies(request.headers) const token = cookies.get(cookieName) if (!token) { return new UnauthorizedAPIRequest() } const jwtResponse = await verifySessionCookie(token, secret) if (!jwtResponse.payload) { return new UnauthorizedAPIRequest() } const body = request.json && ((await request.json()) as { email: string currentPassword: string newPassword: string signoutOnUpdate?: boolean | undefined }) if (!body?.email || !body?.currentPassword || !body?.newPassword) { return new InvalidRequestBodyError() } const email = body.email.toLowerCase() const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email }, }, limit: 1, }) if (docs.length !== 1) { return new UserNotFoundAPIError() } const user = docs[0] const isVerifed = await verifyPassword( body.currentPassword, user.hashedPassword, user.hashSalt, user.hashIterations, ) if (!isVerifed) { return new InvalidCredentials() } const { hash: hashedPassword, salt: hashSalt, iterations, } = await hashPassword(body.newPassword) await payload.update({ collection: internal.usersCollectionSlug, id: user.id, data: { hashedPassword, hashSalt, hashIterations: iterations, }, }) // if (body.signoutOnUpdate) { // let cookies: string[] = [] // cookies = [...invalidateSessionCookies(cookieName, cookies)] // return // } const res = new Response( JSON.stringify({ message: "Password reset complete", kind: SuccessKind.Updated, isSuccess: true, isError: false, }), { status: 201, }, ) return res }