diff --git a/backend/dist/src/app.js b/backend/dist/src/app.js index 60fbd48..3c5e960 100644 --- a/backend/dist/src/app.js +++ b/backend/dist/src/app.js @@ -13,6 +13,7 @@ const http_errors_1 = require("http-errors"); const vsr_1 = __importDefault(require("./routes/vsr")); const furnitureItem_1 = __importDefault(require("./routes/furnitureItem")); const user_1 = __importDefault(require("./routes/user")); +const confirmationEmail_1 = __importDefault(require("./routes/confirmationEmail")); const validateEnv_1 = __importDefault(require("./util/validateEnv")); const app = (0, express_1.default)(); // initializes Express to accept JSON in the request/response body @@ -29,6 +30,7 @@ app.use((0, cors_1.default)({ app.use("/api/user", user_1.default); app.use("/api/vsr", vsr_1.default); app.use("/api/furnitureItems", furnitureItem_1.default); +app.use("/api/confirmationEmail", confirmationEmail_1.default); /** * Error handler; all errors thrown by server are handled here. * Explicit typings required here because TypeScript cannot infer the argument types. diff --git a/backend/dist/src/controllers/confirmationEmail.js b/backend/dist/src/controllers/confirmationEmail.js new file mode 100644 index 0000000..f9e7da8 --- /dev/null +++ b/backend/dist/src/controllers/confirmationEmail.js @@ -0,0 +1,51 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateConfirmationEmail = exports.getConfirmationEmail = void 0; +const express_validator_1 = require("express-validator"); +const sanitize_html_1 = __importDefault(require("sanitize-html")); +const confirmationEmails_1 = require("../services/confirmationEmails"); +const validationErrorParser_1 = __importDefault(require("../util/validationErrorParser")); +/** + * Retrieves the VSR confirmation email from the database. + */ +const getConfirmationEmail = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + try { + const confirmationEmail = yield (0, confirmationEmails_1.retrieveConfirmaionEmail)(); + res.status(200).json({ html: confirmationEmail.html, papLogoHTML: confirmationEmails_1.PAP_LOGO_HTML }); + } + catch (error) { + next(error); + } +}); +exports.getConfirmationEmail = getConfirmationEmail; +const updateConfirmationEmail = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + const errors = (0, express_validator_1.validationResult)(req); + try { + (0, validationErrorParser_1.default)(errors); + const { html } = req.body; + const sanitizedHTML = (0, sanitize_html_1.default)(html, { + allowedTags: sanitize_html_1.default.defaults.allowedTags.concat([ + // Frontend editor uses tags for underlining. + "ins", + ]), + }); + const updatedConfirmationEmail = yield (0, confirmationEmails_1.updateConfirmationEmail)(sanitizedHTML); + res.status(200).json(updatedConfirmationEmail); + } + catch (error) { + next(error); + } +}); +exports.updateConfirmationEmail = updateConfirmationEmail; diff --git a/backend/dist/src/controllers/furnitureItem.js b/backend/dist/src/controllers/furnitureItem.js index a987b0d..051945b 100644 --- a/backend/dist/src/controllers/furnitureItem.js +++ b/backend/dist/src/controllers/furnitureItem.js @@ -12,8 +12,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getFurnitureItems = void 0; +exports.updateFurnitureItem = exports.deleteFurnitureItem = exports.createFurnitureItem = exports.getFurnitureItems = void 0; +const express_validator_1 = require("express-validator"); +const http_errors_1 = __importDefault(require("http-errors")); const furnitureItem_1 = __importDefault(require("../models/furnitureItem")); +const validationErrorParser_1 = __importDefault(require("../util/validationErrorParser")); /** * Gets all available furniture items in the database. Does not require authentication. */ @@ -32,3 +35,47 @@ const getFurnitureItems = (req, res, next) => __awaiter(void 0, void 0, void 0, } }); exports.getFurnitureItems = getFurnitureItems; +const createFurnitureItem = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + const errors = (0, express_validator_1.validationResult)(req); + try { + (0, validationErrorParser_1.default)(errors); + const furnitureItem = yield furnitureItem_1.default.create(req.body); + res.status(201).json(furnitureItem); + } + catch (error) { + next(error); + } +}); +exports.createFurnitureItem = createFurnitureItem; +const deleteFurnitureItem = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { id } = req.params; + const deletedFurnitureItem = yield furnitureItem_1.default.findByIdAndDelete(id); + if (deletedFurnitureItem === null) { + throw (0, http_errors_1.default)(404, "FurnitureItem not found at id " + id); + } + return res.status(204).send(); + } + catch (error) { + next(error); + } +}); +exports.deleteFurnitureItem = deleteFurnitureItem; +const updateFurnitureItem = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + const errors = (0, express_validator_1.validationResult)(req); + try { + const { id } = req.params; + (0, validationErrorParser_1.default)(errors); + const updatedFurnitureItem = yield furnitureItem_1.default.findByIdAndUpdate(id, req.body, { + new: true, + }); + if (updatedFurnitureItem == null) { + throw (0, http_errors_1.default)(404, "Furniture Item not found at id " + id); + } + res.status(200).json(updatedFurnitureItem); + } + catch (error) { + next(error); + } +}); +exports.updateFurnitureItem = updateFurnitureItem; diff --git a/backend/dist/src/controllers/user.js b/backend/dist/src/controllers/user.js index 8138ae5..8ba99f7 100644 --- a/backend/dist/src/controllers/user.js +++ b/backend/dist/src/controllers/user.js @@ -1,4 +1,27 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -12,8 +35,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getWhoAmI = void 0; -const user_1 = __importDefault(require("../models/user")); +exports.deleteUser = exports.notifyResetPassword = exports.changeUserPassword = exports.createUser = exports.getUsers = exports.getWhoAmI = void 0; +const firebase_1 = require("../services/firebase"); +const user_1 = __importStar(require("../models/user")); +const express_validator_1 = require("express-validator"); +const validationErrorParser_1 = __importDefault(require("../util/validationErrorParser")); +const http_errors_1 = __importDefault(require("http-errors")); +const emails_1 = require("../services/emails"); /** * Retrieves data about the current user (their MongoDB ID, Firebase UID, and role). * Requires the user to be signed in. @@ -34,3 +62,110 @@ const getWhoAmI = (req, res, next) => __awaiter(void 0, void 0, void 0, function } }); exports.getWhoAmI = getWhoAmI; +/** + * Retrieves a list of all users in our database + */ +const getUsers = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + try { + const papUsers = yield user_1.default.find(); + const displayUsers = []; + for (const papUser of papUsers) { + const { uid, _id } = papUser; + try { + const firebaseUser = yield firebase_1.firebaseAuth.getUser(uid); + const { displayName, email } = firebaseUser; + const displayUser = { _id, uid, displayName, email }; + displayUsers.push(displayUser); + } + catch (error) { + next(error); + } + } + res.status(200).json(displayUsers); + } + catch (error) { + next(error); + } +}); +exports.getUsers = getUsers; +/** + * Creates a new user, in both the Firebase and MongoDB databases + */ +const createUser = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + const errors = (0, express_validator_1.validationResult)(req); + try { + (0, validationErrorParser_1.default)(errors); + const { name, email, password } = req.body; + // First, call the Firebase API to create a new user + const firebaseUser = yield firebase_1.firebaseAuth.createUser({ + displayName: name, + email, + password, + }); + // Now, using the UID of the new Firebase user, create a user in our MongoDB database + const user = yield user_1.default.create({ + uid: firebaseUser.uid, + // We can only create new staff accounts, not admin accounts. + role: user_1.UserRole.STAFF, + }); + res.status(201).json(user); + } + catch (error) { + next(error); + } +}); +exports.createUser = createUser; +/** + * Changes a user's password, finding the user by their UID + */ +const changeUserPassword = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + try { + const errors = (0, express_validator_1.validationResult)(req); + (0, validationErrorParser_1.default)(errors); + const { password } = req.body; + const { uid } = req.params; + const updatedUser = yield firebase_1.firebaseAuth.updateUser(uid, { + password, + }); + yield (0, emails_1.sendPasswordChangedEmailToAdmin)(updatedUser.email); + res.status(200).json(updatedUser); + } + catch (error) { + next(error); + } +}); +exports.changeUserPassword = changeUserPassword; +/** + * Sends an email to notify the user that their password has been reset. + */ +const notifyResetPassword = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { userUid } = req; + const firebaseUser = yield firebase_1.firebaseAuth.getUser(userUid); + yield (0, emails_1.sendOwnPasswordChangedNotificationEmail)(firebaseUser.email); + yield (0, emails_1.sendPasswordChangedEmailToAdmin)(firebaseUser.email); + res.status(204).send(); + } + catch (error) { + next(error); + } +}); +exports.notifyResetPassword = notifyResetPassword; +/** + * Deletes a user from the Firebase and MongoDB databases + */ +const deleteUser = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + try { + const { uid } = req.params; + yield firebase_1.firebaseAuth.deleteUser(uid); + const deletedUser = yield user_1.default.deleteOne({ uid }); + if (deletedUser === null) { + throw (0, http_errors_1.default)(404, "User not found at uid " + uid); + } + return res.status(204).send(); + } + catch (error) { + next(error); + } +}); +exports.deleteUser = deleteUser; diff --git a/backend/dist/src/controllers/vsr.js b/backend/dist/src/controllers/vsr.js index 2a1ba94..83825f7 100644 --- a/backend/dist/src/controllers/vsr.js +++ b/backend/dist/src/controllers/vsr.js @@ -18,16 +18,16 @@ const http_errors_1 = __importDefault(require("http-errors")); const furnitureItem_1 = __importDefault(require("../models/furnitureItem")); const vsr_1 = __importDefault(require("../models/vsr")); const emails_1 = require("../services/emails"); +const vsrs_1 = require("../services/vsrs"); const validationErrorParser_1 = __importDefault(require("../util/validationErrorParser")); const exceljs_1 = __importDefault(require("exceljs")); -const mongodb_1 = require("mongodb"); /** * Gets all VSRs in the database. Requires the user to be signed in and have * staff or admin permission. */ const getAllVSRS = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { try { - const vsrs = yield vsr_1.default.find(); + const vsrs = yield (0, vsrs_1.retrieveVSRs)(req.query.search, req.query.status, req.query.incomeLevel, req.query.zipCode ? req.query.zipCode.split(",") : undefined, undefined); res.status(200).json({ vsrs }); } catch (error) { @@ -238,30 +238,14 @@ const writeSpreadsheet = (plainVsrs, res) => __awaiter(void 0, void 0, void 0, f yield workbook.xlsx.write(res); }); const bulkExportVSRS = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { - var _a, _b; try { - const filename = "vsrs.xlsx"; + const filename = `vsrs_${new Date().toISOString()}.xlsx`; // Set some headers on the response so the client knows that a file is attached res.set({ "Content-Disposition": `attachment; filename="${filename}"`, "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); - let vsrs; - if (req.query.vsrIds && ((_a = req.query.vsrIds.length) !== null && _a !== void 0 ? _a : 0) > 0) { - // If the "vsrIds" query parameter exists and is non-empty, then find & export all VSRs - // with an _id in the vsrIds list - // Need to convert each ID string to an ObjectId object - const vsrObjectIds = (_b = req.query.vsrIds) === null || _b === void 0 ? void 0 : _b.split(",").map((_id) => new mongodb_1.ObjectId(_id)); - vsrs = (yield vsr_1.default.find({ - _id: { - $in: vsrObjectIds, - }, - })).map((doc) => doc.toObject()); - } - else { - // If the "vsrIds" query parameter is not provided or is empty, export all VSRs in the database - vsrs = (yield vsr_1.default.find()).map((doc) => doc.toObject()); - } + const vsrs = yield (0, vsrs_1.retrieveVSRs)(req.query.search, req.query.status, req.query.incomeLevel, req.query.zipCode ? req.query.zipCode.split(",") : undefined, req.query.vsrIds ? req.query.vsrIds.split(",") : undefined); yield writeSpreadsheet(vsrs, res); } catch (error) { diff --git a/backend/dist/src/models/confirmationEmail.js b/backend/dist/src/models/confirmationEmail.js new file mode 100644 index 0000000..54041cb --- /dev/null +++ b/backend/dist/src/models/confirmationEmail.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const mongoose_1 = require("mongoose"); +/** + * A model for the confirmation email sent to veterans when they fill out the + * VSR. Only one instance of this model will ever exist at once. + */ +const confirmationEmailSchema = new mongoose_1.Schema({ + // The HTML of the email + html: { type: String, required: true }, +}); +exports.default = (0, mongoose_1.model)("ConfirmationEmail", confirmationEmailSchema); diff --git a/backend/dist/src/models/vsr.js b/backend/dist/src/models/vsr.js index 963323a..c7acc90 100644 --- a/backend/dist/src/models/vsr.js +++ b/backend/dist/src/models/vsr.js @@ -51,4 +51,5 @@ const vsrSchema = new mongoose_1.Schema({ lastUpdated: { type: Date, required: true }, status: { type: String, required: true }, }); +vsrSchema.index({ name: "text" }); exports.default = (0, mongoose_1.model)("VSR", vsrSchema); diff --git a/backend/dist/src/routes/confirmationEmail.js b/backend/dist/src/routes/confirmationEmail.js new file mode 100644 index 0000000..ab73417 --- /dev/null +++ b/backend/dist/src/routes/confirmationEmail.js @@ -0,0 +1,36 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = __importDefault(require("express")); +const auth_1 = require("../middleware/auth"); +const ConfirmationEmailController = __importStar(require("../controllers/confirmationEmail")); +const ConfirmationEmailValidator = __importStar(require("../validators/confirmationEmail")); +const router = express_1.default.Router(); +router.get("/", auth_1.requireSignedIn, auth_1.requireAdmin, ConfirmationEmailController.getConfirmationEmail); +router.put("/", auth_1.requireSignedIn, auth_1.requireAdmin, ConfirmationEmailValidator.updateConfirmationEmail, ConfirmationEmailController.updateConfirmationEmail); +exports.default = router; diff --git a/backend/dist/src/routes/furnitureItem.js b/backend/dist/src/routes/furnitureItem.js index 26300a8..1739522 100644 --- a/backend/dist/src/routes/furnitureItem.js +++ b/backend/dist/src/routes/furnitureItem.js @@ -27,7 +27,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); +const auth_1 = require("../middleware/auth"); const FurnitureItemController = __importStar(require("../controllers/furnitureItem")); +const FurnitureItemValidator = __importStar(require("../validators/furnitureItem")); const router = express_1.default.Router(); router.get("/", FurnitureItemController.getFurnitureItems); +router.post("/", auth_1.requireSignedIn, auth_1.requireAdmin, FurnitureItemValidator.createFurnitureItem, FurnitureItemController.createFurnitureItem); +router.delete("/:id", auth_1.requireSignedIn, auth_1.requireAdmin, FurnitureItemController.deleteFurnitureItem); +router.put("/:id", auth_1.requireSignedIn, auth_1.requireAdmin, FurnitureItemValidator.updateFurnitureItem, FurnitureItemController.updateFurnitureItem); exports.default = router; diff --git a/backend/dist/src/routes/user.js b/backend/dist/src/routes/user.js index 899285e..523b544 100644 --- a/backend/dist/src/routes/user.js +++ b/backend/dist/src/routes/user.js @@ -29,6 +29,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const auth_1 = require("../middleware/auth"); const UserController = __importStar(require("../controllers/user")); +const UserValidator = __importStar(require("../validators/user")); const router = express_1.default.Router(); router.get("/whoami", auth_1.requireSignedIn, UserController.getWhoAmI); +router.get("/", auth_1.requireSignedIn, auth_1.requireAdmin, UserController.getUsers); +router.post("/", auth_1.requireSignedIn, auth_1.requireAdmin, UserValidator.createUser, UserController.createUser); +router.patch("/:uid/password", auth_1.requireSignedIn, auth_1.requireAdmin, UserValidator.changeUserPassword, UserController.changeUserPassword); +router.post("/notifyResetPassword", auth_1.requireSignedIn, auth_1.requireStaffOrAdmin, UserController.notifyResetPassword); +router.delete("/:uid", auth_1.requireSignedIn, auth_1.requireAdmin, UserController.deleteUser); exports.default = router; diff --git a/backend/dist/src/services/confirmationEmails.js b/backend/dist/src/services/confirmationEmails.js new file mode 100644 index 0000000..de5aff2 --- /dev/null +++ b/backend/dist/src/services/confirmationEmails.js @@ -0,0 +1,45 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateConfirmationEmail = exports.retrieveConfirmaionEmail = exports.PAP_LOGO_HTML = exports.RECIPIENT_TEXT = void 0; +const confirmationEmail_1 = __importDefault(require("../models/confirmationEmail")); +const DEFAULT_CONFIRMATION_EMAIL_HTML = "

Dear [Recipient],

\n

\n

Thank you for submitting a Veteran Service Request (VSR) form.

\n

\n

Your VSR form has been approved. The next step is to schedule your pick-up appointment. To ensure a smooth process, please review the following information:

\n

\n

Appointment Availability & Upcoming Closure Dates

\n\n

\n

Appointment Confirmation & Details

\n\n

\n

Eligibility & Proof of Service

\n\n

\n

Transportation

\n\n

\n

Location Information

\n\n

\n

Note

\n\n

\n

Stay connected with us

\n

Facebook: @patriotsandpaws

\n

Instagram: @patriotsandpaws

\n

X: @PatriotsandPaws

\n

\n

Thank you for reaching out to us. We're here to support you.

\n

\n

Best regards,

\n

Patriots and Paws

\n

veteran@patriotsandpaws.org

\n"; +exports.RECIPIENT_TEXT = "[Recipient]"; +exports.PAP_LOGO_HTML = 'Patriots & Paws Logo'; +/** + * Retrieves the current confirmation email template from the MongoDB database, + * creating it with the default HTML if it does not exist. + */ +const retrieveConfirmaionEmail = () => __awaiter(void 0, void 0, void 0, function* () { + let confirmationEmail = yield confirmationEmail_1.default.findOne(); + if (confirmationEmail === null) { + confirmationEmail = yield confirmationEmail_1.default.create({ + html: DEFAULT_CONFIRMATION_EMAIL_HTML, + }); + } + return confirmationEmail; +}); +exports.retrieveConfirmaionEmail = retrieveConfirmaionEmail; +/** + * Saves the new HTML for the confirmation email to the database. Assumes + * that the HTML has already been validated and sanitized. + */ +const updateConfirmationEmail = (newHTML) => __awaiter(void 0, void 0, void 0, function* () { + const confirmationEmail = yield (0, exports.retrieveConfirmaionEmail)(); + const updatedConfirmationEmail = yield confirmationEmail_1.default.findByIdAndUpdate(confirmationEmail._id, { + html: newHTML, + }, { new: true }); + return updatedConfirmationEmail; +}); +exports.updateConfirmationEmail = updateConfirmationEmail; diff --git a/backend/dist/src/services/emails.js b/backend/dist/src/services/emails.js index 0893623..782259b 100644 --- a/backend/dist/src/services/emails.js +++ b/backend/dist/src/services/emails.js @@ -12,13 +12,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendVSRConfirmationEmailToVeteran = exports.sendVSRNotificationEmailToStaff = void 0; +exports.sendPasswordChangedEmailToAdmin = exports.sendOwnPasswordChangedNotificationEmail = exports.sendVSRConfirmationEmailToVeteran = exports.sendVSRNotificationEmailToStaff = void 0; require("dotenv/config"); const nodemailer_1 = __importDefault(require("nodemailer")); +const confirmationEmails_1 = require("../services/confirmationEmails"); const validateEnv_1 = __importDefault(require("../util/validateEnv")); const trimmedFrontendUrl = validateEnv_1.default.FRONTEND_ORIGIN.replace( // Trim trailing slash from frontend URL, if there is one /\/$/gi, ""); +const sendEmail = (options) => __awaiter(void 0, void 0, void 0, function* () { + const transporter = nodemailer_1.default.createTransport({ + service: "gmail", + auth: { + user: validateEnv_1.default.EMAIL_USER, + pass: validateEnv_1.default.EMAIL_APP_PASSWORD, + }, + }); + const mailOptions = Object.assign({ from: validateEnv_1.default.EMAIL_USER }, options); + yield transporter.sendMail(mailOptions); +}); /** * Sends a notification email to PAP staff when a VSR is submitted. * Throws an error if the email could not be sent. @@ -30,20 +42,11 @@ const trimmedFrontendUrl = validateEnv_1.default.FRONTEND_ORIGIN.replace( const sendVSRNotificationEmailToStaff = (name, email, id) => __awaiter(void 0, void 0, void 0, function* () { const EMAIL_SUBJECT = "New VSR Submitted"; const EMAIL_BODY = `A new VSR was submitted by ${name} from ${email}. You can view it at ${trimmedFrontendUrl}/staff/vsr/${id}`; - const transporter = nodemailer_1.default.createTransport({ - service: "gmail", - auth: { - user: validateEnv_1.default.EMAIL_USER, - pass: validateEnv_1.default.EMAIL_APP_PASSWORD, - }, - }); - const mailOptions = { - from: validateEnv_1.default.EMAIL_USER, + yield sendEmail({ to: validateEnv_1.default.EMAIL_NOTIFICATIONS_RECIPIENT, subject: EMAIL_SUBJECT, text: EMAIL_BODY, - }; - yield transporter.sendMail(mailOptions); + }); }); exports.sendVSRNotificationEmailToStaff = sendVSRNotificationEmailToStaff; /** @@ -55,82 +58,12 @@ exports.sendVSRNotificationEmailToStaff = sendVSRNotificationEmailToStaff; */ const sendVSRConfirmationEmailToVeteran = (name, email) => __awaiter(void 0, void 0, void 0, function* () { const EMAIL_SUBJECT = "VSR Submission Confirmation"; - const EMAIL_HTML = `

Dear ${name},

\ -

Thank you for submitting a veteran service request (VSR).

\ -

\ - We schedule appointments on Tuesdays, Thursday, and Saturdays from 10-3pm. You need to be\ - specific about what day and time you want to make your appointment.\ - \ -

\ - \ -

\ - PLEASE NOTE: We will not be making appointments on THURSDAY, NOVEMBER 23RD (THANKSGIVING) OR\ - SATURDAY, DECEMBER 2nd.\ -

\ - \ -

\ - Respond to this email and let us know what day and time will work and we will put you\ - down.\ -

\ - \ -

\ - You pick up your items on the date and time of your appointment. You need to come prepared on that\ - day.\ -

\ -

We will help you with as much as we can on your list.

\ -

\ - Please remember that items are donated from the community as a way of saying THANK YOU FOR YOUR\ - SERVICE, so we are like a cashless thrift store environment.\ -

\ -

You will need to provide your own transportation for all your items you are requesting.

\ -

\ - \ - Wherever you are going to rent a truck, don't wait to the last minute to rent a truck as there\ - might not be one available.\ -

\ -

We are located in the Anaheim, in Orange County.

\ -

Once we confirm your appointment we will send you the warehouse protocol and address.

\ -

\ - \ - Items in the warehouse have been donated from the General Public and are strictly for Veterans,\ - Active Military and Reservists and their immediate family (wives and school aged children) who\ - live with them. The Veteran/Active Duty/Reservist has to be at appointment and will need to show\ - proof of service, such as a VA Card, DD214 or Active Military card.\ -

\ -

They are not for family members and friends who have not served our Country.

\ -

Thank you for contacting us.

\ -

Volunteer

\ - \ -

veteran@patriotsandpaws.org

\ - Patriots & Paws Logo\ -

\ - Facebook\ - https://www.facebook.com/pages/Patriots-and-Paws/283613748323930\ -

\ -

Twitter @patriotsandpaws

\ -

Instagram patriotsandpaws

\ - `; - const transporter = nodemailer_1.default.createTransport({ - service: "gmail", - auth: { - user: validateEnv_1.default.EMAIL_USER, - pass: validateEnv_1.default.EMAIL_APP_PASSWORD, - }, - }); - const mailOptions = { - from: validateEnv_1.default.EMAIL_USER, + const confirmationEmail = yield (0, confirmationEmails_1.retrieveConfirmaionEmail)(); + const emailHTML = confirmationEmail.html.replace(confirmationEmails_1.RECIPIENT_TEXT, name) + confirmationEmails_1.PAP_LOGO_HTML; + yield sendEmail({ to: email, subject: EMAIL_SUBJECT, - html: EMAIL_HTML, + html: emailHTML, attachments: [ { filename: "pap_logo.png", @@ -138,7 +71,26 @@ const sendVSRConfirmationEmailToVeteran = (name, email) => __awaiter(void 0, voi cid: "pap_logo.png", }, ], - }; - yield transporter.sendMail(mailOptions); + }); }); exports.sendVSRConfirmationEmailToVeteran = sendVSRConfirmationEmailToVeteran; +const sendOwnPasswordChangedNotificationEmail = (email) => __awaiter(void 0, void 0, void 0, function* () { + const EMAIL_SUBJECT = "PAP Application Password Change Confirmation"; + const EMAIL_BODY = `Your password for the ${email} account has been changed.`; + yield sendEmail({ + to: email, + subject: EMAIL_SUBJECT, + text: EMAIL_BODY, + }); +}); +exports.sendOwnPasswordChangedNotificationEmail = sendOwnPasswordChangedNotificationEmail; +const sendPasswordChangedEmailToAdmin = (email) => __awaiter(void 0, void 0, void 0, function* () { + const EMAIL_SUBJECT = "PAP Application Password Change Notification"; + const EMAIL_BODY = `The password for the ${email} account has been changed.`; + yield sendEmail({ + to: validateEnv_1.default.EMAIL_NOTIFICATIONS_RECIPIENT, + subject: EMAIL_SUBJECT, + text: EMAIL_BODY, + }); +}); +exports.sendPasswordChangedEmailToAdmin = sendPasswordChangedEmailToAdmin; diff --git a/backend/dist/src/services/vsrs.js b/backend/dist/src/services/vsrs.js new file mode 100644 index 0000000..4ceb555 --- /dev/null +++ b/backend/dist/src/services/vsrs.js @@ -0,0 +1,68 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.retrieveVSRs = void 0; +const vsr_1 = __importDefault(require("../models/vsr")); +const retrieveVSRs = (search, status, incomeLevel, zipCodes, vsrIds) => __awaiter(void 0, void 0, void 0, function* () { + let vsrs = yield vsr_1.default.aggregate([ + ...(search + ? [ + { + $match: { name: { $regex: new RegExp(search) } }, + }, + ] + : []), + { + $addFields: { + statusOrder: { + $switch: { + branches: [ + { case: { $eq: ["$status", "Received"] }, then: 1 }, + { case: { $eq: ["$status", "Approved"] }, then: 2 }, + { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, + { case: { $eq: ["$status", "Complete"] }, then: 4 }, + { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, + { case: { $eq: ["$status", "Archived"] }, then: 6 }, + ], + default: 99, + }, + }, + }, + }, + { $sort: { statusOrder: 1, dateReceived: -1 } }, + ]); + if (vsrIds && vsrIds.length > 0) { + const vsrIdsSet = new Set(vsrIds); + vsrs = vsrs.filter((vsr) => vsrIdsSet.has(vsr._id.toString())); + } + if (status) { + vsrs = vsrs.filter((vsr) => vsr.status === status); + } + if (incomeLevel) { + const incomeMap = { + "50000": "$50,001 and over", + "25000": "$25,001 - $50,000", + "12500": "$12,501 - $25,000", + "0": "$12,500 and under", + }; + vsrs = vsrs.filter((vsr) => { + return vsr.incomeLevel === incomeMap[incomeLevel]; + }); + } + if (zipCodes && zipCodes.length > 0) { + vsrs = vsrs.filter((vsr) => zipCodes.includes(vsr.zipCode.toString())); + } + return vsrs; +}); +exports.retrieveVSRs = retrieveVSRs; diff --git a/backend/dist/src/validators/confirmationEmail.js b/backend/dist/src/validators/confirmationEmail.js new file mode 100644 index 0000000..885ea93 --- /dev/null +++ b/backend/dist/src/validators/confirmationEmail.js @@ -0,0 +1,32 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateConfirmationEmail = void 0; +const express_validator_1 = require("express-validator"); +const html_validator_1 = __importDefault(require("html-validator")); +const makeHTMLValidator = () => (0, express_validator_1.body)("html") + .exists({ checkFalsy: true }) + .withMessage("HTML is required") + .isString() + .withMessage("HTML must be a string") + .custom((html) => __awaiter(void 0, void 0, void 0, function* () { + try { + (0, html_validator_1.default)({ data: html }); + return true; + } + catch (error) { + return false; + } +})); +exports.updateConfirmationEmail = [makeHTMLValidator()]; diff --git a/backend/dist/src/validators/furnitureItem.js b/backend/dist/src/validators/furnitureItem.js new file mode 100644 index 0000000..c7e4f9b --- /dev/null +++ b/backend/dist/src/validators/furnitureItem.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updateFurnitureItem = exports.createFurnitureItem = void 0; +const express_validator_1 = require("express-validator"); +const makeCategoryValidator = () => (0, express_validator_1.body)("category") + .exists({ checkFalsy: true }) + .withMessage("Category is required") + .isString() + .withMessage("Category must be a string"); +const makeNameValidator = () => (0, express_validator_1.body)("name") + .exists({ checkFalsy: true }) + .withMessage("Name is required") + .isString() + .withMessage("Name must be a string"); +const makeAllowMultipleValidator = () => (0, express_validator_1.body)("allowMultiple") + .exists({ checkFalsy: false }) + .withMessage("Must specify if multiple of these items can be requested") + .isBoolean() + .withMessage("allowMultiple must be a boolean"); +const makeCategoryIndexValidator = () => (0, express_validator_1.body)("categoryIndex") + .exists({ checkFalsy: true }) + .withMessage("Category index is required") + .isInt({ min: 0 }) + .withMessage("Category index must be positive and an integer"); +exports.createFurnitureItem = [ + makeCategoryValidator(), + makeNameValidator(), + makeAllowMultipleValidator(), + makeCategoryIndexValidator(), +]; +exports.updateFurnitureItem = [ + makeCategoryValidator(), + makeNameValidator(), + makeAllowMultipleValidator(), + makeCategoryIndexValidator(), +]; diff --git a/backend/dist/src/validators/user.js b/backend/dist/src/validators/user.js new file mode 100644 index 0000000..8440816 --- /dev/null +++ b/backend/dist/src/validators/user.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.changeUserPassword = exports.createUser = void 0; +const express_validator_1 = require("express-validator"); +/** + * Validators for creating and updating users + */ +const makeNameValidator = () => (0, express_validator_1.body)("name") + .exists({ checkFalsy: true }) + .withMessage("Name is required") + .isString() + .withMessage("Name must be a string"); +const makeEmailValidator = () => (0, express_validator_1.body)("email") + .exists({ checkFalsy: true }) + .withMessage("Email is required") + .isString() + .withMessage("Email must be a string"); +const makePasswordValidator = () => (0, express_validator_1.body)("password") + .exists({ checkFalsy: true }) + .withMessage("Password is required") + .isString() + .withMessage("Password must be a string"); +exports.createUser = [makeNameValidator(), makeEmailValidator(), makePasswordValidator()]; +exports.changeUserPassword = [makePasswordValidator()];