@daliboru/payload-auth-plugin-fork
Version:
Forked payload-auth-plugin with custom access token feature.
431 lines (382 loc) • 9.56 kB
text/typescript
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
}