Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions api/cloud_api/email_templates/membershipConfirmationCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function membershipConfirmationCode(user, recipient, confirmCode) {
return new Promise((resolve, reject) => {
return resolve({
from: user,
to: recipient,
subject: 'SCE Membership Confirmation',
generateTextFromHTML: true,
html: `
<p>
Hi, <br />
Thank you for signing up for membership! <br />
Please use the below confirmation code when you
visit your profile page on the
SCE website to verify your membership: <br /> <b>${confirmCode}</b>
</p>
`
});
});
}
module.exports = { membershipConfirmationCode };
52 changes: 51 additions & 1 deletion api/cloud_api/routes/Mailer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ const { verification } = require('../email_templates/verification');
const { passwordReset } = require('../email_templates/passwordReset');
const { blastEmail } = require('../email_templates/blastEmail');
const { unsubscribeEmail } = require('../email_templates/unsubscribeEmail');
const { membershipConfirmationCode } = require('../email_templates/membershipConfirmationCode');
const {
OK,
BAD_REQUEST
BAD_REQUEST,
SERVER_ERROR
} = require('../../util/constants').STATUS_CODES;
const logger = require('../../util/logger');
const { googleApiKeys } = require('../../config/config.json');
Expand Down Expand Up @@ -134,4 +136,52 @@ router.post('/sendUnsubscribeEmail', async (req, res) => {
return res.sendStatus(OK);
});

router.post('/sendMembershipConfirmationCode', async (req, res) => {
if (!ENABLED && process.env.NODE_ENV !== 'test') {
return res.sendStatus(OK);
}
const scopes = ['https://mail.google.com/'];
const pathToToken = __dirname + '/../../config/token.json';
const apiHandler = new SceGoogleApiHandler(scopes, pathToToken);
const tokenJson = await apiHandler.checkIfTokenFileExists();

if (tokenJson) {
if (apiHandler.checkIfTokenIsExpired(tokenJson)) {
logger.warn('refreshing token');
MetricsHandler.gcpRefreshTokenLastUpdated.set(Math.floor(Date.now() / 1000));
apiHandler.refreshToken();
}
} else {
logger.warn('getting new token! ', { tokenJson });
apiHandler.getNewToken();
}

const { recipientEmail, confirmationCode } = req.body;

if (!recipientEmail || !confirmationCode) {
logger.warn('Missing recipientEmail or confirmationCode', { body: req.body });
return res.status(BAD_REQUEST).json({
error: 'recipientEmail and confirmationCode are required',
});
}

await membershipConfirmationCode(USER, recipientEmail, confirmationCode)
.then((template) => {
apiHandler
.sendEmail(template)
.then((_) => {
res.sendStatus(OK);
MetricsHandler.emailSent.inc({ type: 'membershipConfirmationCode' });
})
.catch((err) => {
logger.error('unable to send confirmation code: ', err);
res.sendStatus(SERVER_ERROR);
});
})
.catch((err) => {
logger.error('unable to generate member confirmation email template: ', err);
res.sendStatus(SERVER_ERROR);
});
});

module.exports = router;
41 changes: 41 additions & 0 deletions api/main_endpoints/models/MembershipPayment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;


const MembershipPaymentSchema = new Schema(
{
createdAt: {
type: Date,
default: Date.now
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
default: null,
},
status: {
type: String,
enum: ['pending', 'completed', 'rejected'],
default: 'pending',
required: true,
},
confirmationCode: {
type: String,
unique: true,
required: true,
},
amount: {
type: Number,
required: true,
},
venmoDetails:{
transactionId: { type: String },
payerName: { type: String },
note: { type: String },
}
},
{ collection: 'MembershipPayments' }
);


module.exports = mongoose.model('MembershipPayment', MembershipPaymentSchema);
71 changes: 71 additions & 0 deletions api/main_endpoints/routes/MembershipPayment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const express = require('express');
const router = express.Router();
const bodyParser = require('body-parser');
router.use(bodyParser.json());
const {
BAD_REQUEST,
SERVER_ERROR,
NOT_FOUND,
OK,
} = require('../../util/constants').STATUS_CODES;
const membershipState = require('../../util/constants').MEMBERSHIP_STATE;
const User = require('../models/User');
const { getMemberExpirationDate, updateMembershipExpiration } = require('../util/userHelpers');
const { findVerifyPayment, rejectPayment } = require('../util/membershipPaymentQueries');
const { decodeToken } = require('../util/token-functions.js');

router.post('/verifyMembership', async (req, res) => {
const decoded = await decodeToken(req, membershipState.PENDING);
if (decoded.status !== OK) {
return res.sendStatus(decoded.status);
}

const { confirmationCode } = req.body;
const userId = decoded.token._id;

if (!confirmationCode) {
return res.sendStatus(BAD_REQUEST);
}

const paymentDocument = await findVerifyPayment(confirmationCode, userId);
if (paymentDocument === null){
return res.sendStatus(SERVER_ERROR);
}
if (paymentDocument === false){
return res.sendStatus(NOT_FOUND);
}

const paymentId = paymentDocument._id;
const { amount } = paymentDocument;

if (amount < 20){
const rejected = await rejectPayment(paymentId);
if (rejected === null){
return res.sendStatus(SERVER_ERROR);
}
if (rejected === false){
return res.sendStatus(NOT_FOUND);
}
return res.sendStatus(BAD_REQUEST);
}

let semestersToAdd = 0;
if (amount >= 30) {
semestersToAdd = 2;
} else {
semestersToAdd = 1;
}

const membershipUpdateResult = await updateMembershipExpiration(
decoded.token._id,
semestersToAdd
);

if (membershipUpdateResult === null) {
return res.sendStatus(SERVER_ERROR);
}
if (membershipUpdateResult === false) {
return res.status(NOT_FOUND).send('User not found.');
}
return res.sendStatus(OK);
});
14 changes: 13 additions & 1 deletion api/main_endpoints/util/emailHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,16 @@ async function sendPasswordReset(resetToken, email) {
});
}

module.exports = { sendUnsubscribeEmail, sendVerificationEmail, sendPasswordReset };
async function membershipConfirmationCode(confirmCode, email) {
return new Promise((resolve) => {
axios
.post(`${MAILER_API_URL}/Mailer/sendMembershipConfirmationCode`, {
recipientEmail: email,
confirmationCode: confirmCode
})
.then(() => resolve(true))
.catch(() => resolve(false));
});
}

module.exports = { sendUnsubscribeEmail, sendVerificationEmail, sendPasswordReset, membershipConfirmationCode };
62 changes: 62 additions & 0 deletions api/main_endpoints/util/membershipPaymentQueries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import MembershipPayment from '../main_endpoints/models/MembershipPayment.js';

const status = {
PENDING: 'pending',
COMPLETED: 'completed',
REJECTED: 'rejected',
};

function findVerifyPayment(confirmationCode, userId) {
return new Promise((resolve) => {
try {
MembershipPayment.findOneAndUpdate(
{
confirmationCode,
status: status.PENDING,
},
{
$set: { userId, status: status.COMPLETED },
},
{
new: true,
runValidators: true,
},
(error, result) => {
if (error) {
return resolve(null);
}
if (!result) {
return resolve(false);
}
return resolve(result);
}
);
} catch (error) {
return resolve(null);
}
});
}

function rejectPayment(paymentId) {
return new Promise((resolve) => {
try {
MembershipPayment.findByIdAndUpdate(
paymentId,
{ $set: { status: status.REJECTED } },
(error, result) => {
if (error) {
return resolve(null);
}
if (!result) {
return resolve(false);
}
return resolve(true);
}
);
} catch (error) {
return resolve(null);
}
});
}

module.exports = { findVerifyPayment, rejectPayment };
25 changes: 25 additions & 0 deletions api/main_endpoints/util/userHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,30 @@ function checkIfPageCountResets(lastLogin) {
return lastLoginWasOverOneWeekAgo || aSundayHasPassedSinceLastLogin;
}

/**
* Update a user's membershipValidUntil date
* @param {String} userId - The user's ID
* @param {Number} numberOfSemestersToSignUpFor - Number of semesters to extend
* @returns {Object} result - Contains success status and message
*/
async function updateMembershipExpiration(userId, numberOfSemestersToSignUpFor) {
try {
const newExpiration = getMemberExpirationDate(numberOfSemestersToSignUpFor);
const user = await User.findByIdAndUpdate(
userId,
{ membershipValidUntil: newExpiration },
{ new: true },
);
if (!user) {
return false;
}
return true;
} catch (error) {
logger.error('Error updating membership:', error);
return null;
}
}

module.exports = {
registerUser,
getMemberExpirationDate,
Expand All @@ -211,4 +235,5 @@ module.exports = {
userWithEmailExists,
checkIfPageCountResets,
findPasswordReset,
updateMembershipExpiration
};