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
JavaScript
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}.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