Skip to content

Commit

Permalink
Merge branch 'main' into feature/katelynrachel/getinvolved-upcomingev…
Browse files Browse the repository at this point in the history
…ents-pageeditor
  • Loading branch information
katelynpdn committed May 9, 2024
2 parents 01da90e + 4d0acd5 commit a8a044c
Show file tree
Hide file tree
Showing 72 changed files with 2,149 additions and 119 deletions.
Binary file added .swp
Binary file not shown.
21 changes: 16 additions & 5 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@
"http-errors": "^2.0.0",
"module-alias": "^2.2.3",
"mongodb": "^6.3.0",
"mongoose": "^8.0.3",
"nodemailer": "^6.9.11"
"mongoose": "^8.0.3"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/nodemailer": "^6.4.14",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"husky": "^8.0.3",
"nodemailer": "^6.9.13",
"nodemon": "^3.0.2",
"prettier": "^3.1.1",
"ts-node-dev": "^2.0.0",
Expand Down
5 changes: 3 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import testimonialRoutes from "src/routes/testimonial";
import newsletterRoutes from "src/routes/newsletter"; // Import newsletter routes
import emailRoutes from "src/routes/emails";
import pageeditorRoutes from "src/routes/pageeditor";
import paypalRoutes from "src/routes/paypal";

const app = express();

Expand All @@ -36,13 +37,13 @@ app.use(
app.use("/api/subscribers", subscriberRoutes);
app.use("/api/member", memberRoutes);
app.use("/api/BackgroundImage", backgroundImageRoutes);

app.use("/api/eventDetails", eventDetailsRoutes);
app.use("/api/volunteerDetails", volunteerDetailsRoutes);
app.use("/api/testimonial", testimonialRoutes);
app.use("/api/newsletter", newsletterRoutes); // Use newsletter routes
app.use("/api/newsletter", newsletterRoutes);
app.use("/api/emails", emailRoutes);
app.use("/api/pageeditor", pageeditorRoutes);
app.use("/api/orders", paypalRoutes); // Donation Order routes

/**
* Error handler; all errors thrown by server are handled here.
Expand Down
4 changes: 4 additions & 0 deletions backend/src/controllers/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export const createEmail: RequestHandler = async (req, res, next) => {
const specialStr = specialUpdates ? "Yes" : "No";
EMAIL_SUBJECT = `Newsletter Form: Subscription by ${firstName} ${lastName}`;
EMAIL_BODY = `First Name: ${firstName} \nLast Name: ${lastName}\nEmail: ${email}\nReceive Quarterly Updates: ${quarterlyStr}\nReceive Special Events Updates: ${specialStr}`;
} else if (req.body.type === "donation") {
const { firstName, lastName, email, phone, comment } = req.body;
EMAIL_SUBJECT = `Physical Donation Form: Donation from ${firstName} ${lastName}`;
EMAIL_BODY = `First Name: ${firstName}\nLast Name: ${lastName}\nEmail: ${email}\nPhone Number: ${phone}\nDonation Comment:\n${comment}`;
}

try {
Expand Down
22 changes: 22 additions & 0 deletions backend/src/controllers/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RequestHandler } from "express";
import { captureOrder, createOrder } from "src/services/paypal";

export const createOrderHandler: RequestHandler = async (req, res, next) => {
try {
const { cart } = req.body;
const { jsonResponse, httpStatusCode } = await createOrder(cart);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
next(error);
}
};

export const captureOrderHandler: RequestHandler = async (req, res, next) => {
try {
const { orderId } = req.params;
const { jsonResponse, httpStatusCode } = await captureOrder(orderId);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
next(error);
}
};
54 changes: 52 additions & 2 deletions backend/src/controllers/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,75 @@ import { RequestHandler } from "express";
import { validationResult } from "express-validator";
import Subscriber from "src/models/subscriber";
import validationErrorParser from "src/util/validationErrorParser";
import createHttpError from "http-errors";

export const createSubscriber: RequestHandler = async (req, res, next) => {
const errors = validationResult(req);
const { email } = req.body;

let memSince;
if (req.body.memberSince) {
memSince = req.body.memberSince;
} else {
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const day = String(currentDate.getDate()).padStart(2, "0");
memSince = `${year}-${month}-${day}`;
}

try {
// validationErrorParser is a helper that throws 400 if there are errors
validationErrorParser(errors);
const subscriber = await Subscriber.create({
email: email,
firstName: req.body.firstName,
lastName: req.body.lastName,
memberSince: memSince,
quarterlyUpdates: req.body.quarterlyUpdates,
specialUpdates: req.body.specialUpdates,
});

// successfully created subscriber in db
res.status(201).json(subscriber);
} catch (error) {
next(error);
}
};

export const getAllSubscribers: RequestHandler = async (req, res, next) => {
try {
const mailingListEntries = await Subscriber.find({});

if (!mailingListEntries || mailingListEntries.length === 0) {
return res.status(200).json({ message: "No mailing list entries found." });
}

// const formattedMailingList = mailingListEntries.map((entry) => {
// if (!entry.firstName) {
// entry.firstName = "";
// }
// if (!entry.lastName) {
// entry.lastName = "";
// }
// });

res.status(200).json(mailingListEntries);
} catch (error) {
next(error);
}
};

export const deleteSubscriber: RequestHandler = async (req, res, next) => {
const { id } = req.params;

try {
const subscriber = await Subscriber.findByIdAndDelete(id);

if (!subscriber) {
throw createHttpError(404, "Subscriber not found.");
}

res.status(200).json(subscriber);
} catch (error) {
next(error);
}
};
1 change: 0 additions & 1 deletion backend/src/models/eventDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const eventDetailsSchema = new Schema({
date: { type: String, required: true },
location: { type: String, required: true },
imageURI: { type: String, required: true },
volunteers: { type: [String], required: false, default: [] },
});

type EventDetails = InferSchemaType<typeof eventDetailsSchema>;
Expand Down
1 change: 1 addition & 0 deletions backend/src/models/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const subscriberSchema = new Schema({
email: { type: String, required: true },
firstName: { type: String },
lastName: { type: String },
memberSince: { type: String },
quarterlyUpdates: { type: Boolean },
specialUpdates: { type: Boolean },
});
Expand Down
9 changes: 9 additions & 0 deletions backend/src/routes/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from "express";
import { captureOrderHandler, createOrderHandler } from "src/controllers/paypal";

const router = express.Router();

router.post("/", createOrderHandler);
router.post("/:orderId/capture", captureOrderHandler);

export default router;
7 changes: 2 additions & 5 deletions backend/src/routes/subscriber.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
/*
* Newsletter subscription route requests.
*/

import express from "express";
import * as SubscriberController from "src/controllers/subscriber";
import * as SubscriberValidator from "src/validators/subscriber";

const router = express.Router();

router.post("/", SubscriberValidator.createSubscriber, SubscriberController.createSubscriber);

router.get("/", SubscriberController.getAllSubscribers);
router.delete("/:id", SubscriberValidator.deleteSubscriber, SubscriberController.deleteSubscriber);
export default router;
4 changes: 3 additions & 1 deletion backend/src/services/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const sendContactEmail = async (subject: string, message: string) => {
const EMAIL_SUBJECT = `${subject}`;
const EMAIL_BODY = `${message}`;
const transporter = nodemailer.createTransport({
service: "gmail",
service: "Gmail",
host: "smtp.gmail.com",
secure: true,
auth: {
user: env.EMAIL_USER,
pass: env.EMAIL_APP_PASSWORD,
Expand Down
125 changes: 125 additions & 0 deletions backend/src/services/paypal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID;
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET;

const base = "https://api-m.sandbox.paypal.com";

/**
* Types the CartItem and Cart objects which gets used in a createOrder request
*/
type CartItem = {
id: string;
amount: string; // string dollar amout, eg "5.00"
quantity: string;
};

type Cart = CartItem[];

/**
* Generate an OAuth 2.0 token to authenticate with PayPal REST APIs
* @see https://developer.paypal.com/api/rest/authentication/
*/
const generateAccessToken = async () => {
try {
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
throw new Error("MISSING_PAYPAL_API_CREDENTIALS");
}
const auth = Buffer.from(PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET).toString("base64");
const response = await fetch(`${base}/v1/oauth2/token`, {
method: "POST",
body: "grant_type=client_credentials",
headers: {
Authorization: `Basic ${auth}`,
},
});

const data = await response.json();
return data.access_token;
} catch (error) {
console.error("Failed to generate PayPal Access Token: ", error);
}
};

async function handleResponse(response: Response) {
try {
const jsonResponse = await response.json();
return {
jsonResponse,
httpStatusCode: response.status,
};
} catch (error) {
const errorMessage = await response.text();
throw new Error(errorMessage);
}
}

export async function createOrder(cart: Cart) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders`;
console.log("shopping cart info", cart);

const payload = {
intent: "CAPTURE",
purchase_units: [
{
items: [
{
name: "Donation to 4FLOT",
description: "Deserunt aliquip enim Lorem.",
quantity: "1",
unit_amount: {
currency_code: "USD",
value: cart[0].amount,
},
category: "DONATION",
},
],
amount: {
currency_code: "USD",
value: cart[0].amount,
breakdown: {
item_total: {
currency_code: "USD",
value: cart[0].amount,
},
},
},
},
],
};

const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
method: "POST",
body: JSON.stringify(payload),
});

return handleResponse(response);
}

export async function captureOrder(orderID: string) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders/${orderID}/capture`;

const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
// Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation:
// https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/
// "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}'
// "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}'
},
});

return handleResponse(response);
}
Loading

0 comments on commit a8a044c

Please sign in to comment.