UNPKG

payloadcms_otp_plugin

Version:

A comprehensive One-Time Password (OTP) authentication plugin for Payload CMS that enables secure passwordless authentication via SMS and email

253 lines (252 loc) 8.24 kB
import { jwtSign } from "payload"; import { addUserSession } from "../utilities/session.js"; import { createTranslationHelper } from "../utilities/translation.js"; export class OTPService { payload; authCollection; request; afterSetOtpHook; constructor(request, collection, afterSetOtpHook){ this.payload = request.payload; this.authCollection = collection; this.request = request; this.afterSetOtpHook = afterSetOtpHook; } generateOTP(length = 6) { const min = Math.pow(10, length - 1); const max = Math.pow(10, length) - 1; return `${Math.floor(Math.random() * (max - min + 1)) + min}`; } async cleanupExpiredOTPs(credentials) { const where = credentials.mobile ? { mobile: { equals: credentials.mobile } } : { email: { equals: credentials.email } }; await this.payload.delete({ collection: 'otpCode', where: { ...where, expiresAt: { less_than: new Date() } }, overrideAccess: true }); } async storeOTP(credentials, code) { // Get expiredTime from plugin configuration (in milliseconds) const expiredTime = this.payload.otpPluginConfig?.expiredTime || 300000; // Default 5 minutes const expiresAt = new Date(Date.now() + expiredTime); const otpRecord = await this.payload.create({ collection: 'otpCode', data: { ...credentials, code, expiresAt, verified: false }, overrideAccess: true }); // Execute afterSetOtp hook if provided if (this.afterSetOtpHook) { await this.afterSetOtpHook({ otp: code, credentials, otpRecord, payload: this.payload, req: this.request }); } return otpRecord; } async sendOTP(credentials, headers) { try { const { t } = createTranslationHelper(headers || new Headers()); if (!credentials.mobile && !credentials.email) { return { success: false, message: t('api.mobile_or_email_required') }; } await this.cleanupExpiredOTPs(credentials); // Get OTP length from plugin configuration const otpLength = this.payload.otpPluginConfig?.otpLength || 6; const otpCode = this.generateOTP(otpLength); await this.storeOTP(credentials, otpCode); // TODO: Integrate with SMS/Email service // console.log(`OTP for ${credentials.mobile || credentials.email}: ${otpCode}`); return { success: true, message: t('api.otp_sent_successfully') }; } catch (error) { // console.error('Error sending OTP:', error); const { t } = createTranslationHelper(headers || new Headers()); return { success: false, message: t('api.failed_to_send_otp') }; } } async verifyOTP(credentials) { const where = credentials.mobile ? { mobile: { equals: credentials.mobile } } : { email: { equals: credentials.email } }; const otpRecord = await this.payload.find({ collection: 'otpCode', where: { ...where, code: { equals: credentials.otp }, verified: { equals: false }, expiresAt: { greater_than: new Date() } }, overrideAccess: true }); const isValid = otpRecord.docs.length > 0; if (isValid) { await this.payload.update({ collection: 'otpCode', id: otpRecord.docs[0].id, data: { verified: true }, overrideAccess: true }); } return { isValid, otpRecord: otpRecord.docs[0] }; } async findOrCreateUser(credentials) { const where = credentials.mobile ? { mobile: { equals: credentials.mobile } } : { email: { equals: credentials.email } }; const users = await this.payload.find({ collection: 'users', where, overrideAccess: true }); if (users.docs.length > 0) { const user = users.docs[0]; // Update verification status for existing users if (credentials.mobile) { await this.payload.update({ collection: 'users', id: user.id, data: { mobileVerified: true }, overrideAccess: true }); } return user; } // Create new user const userData = credentials.mobile ? { mobile: credentials.mobile, email: `${credentials.mobile}@mobile.user`, mobileVerified: true, password: `${credentials.mobile}temp` } : { email: credentials.email, mobile: '', mobileVerified: true, password: `${credentials.email}temp` }; return await this.payload.create({ collection: 'users', data: userData, overrideAccess: true }); } async generateAuthToken(user) { const collectionConfig = this.payload.collections[this.authCollection].config; const { sid } = await addUserSession({ collectionConfig, payload: this.payload, req: this.request, user }); const token = await jwtSign({ fieldsToSign: { id: user.id, collection: 'users', email: user.email, sid: sid }, secret: this.payload.secret, tokenExpiration: 3600 * 2 // 2 hours }); return token.token; } async loginWithOTP(credentials, headers) { try { const { t } = createTranslationHelper(headers || new Headers()); if (!credentials.mobile && !credentials.email || !credentials.otp) { return { success: false, message: t('api.mobile_email_and_otp_required') }; } const { isValid } = await this.verifyOTP(credentials); if (!isValid) { return { success: false, message: t('api.invalid_or_expired_otp') }; } const user = await this.findOrCreateUser(credentials); const token = await this.generateAuthToken(user); return { success: true, message: t('api.login_successful'), data: { token, user } }; } catch (error) { // console.error('Error during OTP login:', error); const { t } = createTranslationHelper(headers || new Headers()); return { success: false, message: t('api.login_failed') }; } } // Backward compatibility async loginWithMobile(credentials) { const result = await this.loginWithOTP(credentials); return { success: result.success, token: result.data?.token, user: result.data?.user, message: result.message }; } } //# sourceMappingURL=index.js.map