Skip to content

Commit

Permalink
Feature/aammya8/new account approval (#104)
Browse files Browse the repository at this point in the history
* Add user approval and denial functionality, as well as email user about account approval updates

* backup

* backup

* fixed delete route

* change approve/deny/delete to use email

* approve/deny controllers do not get entered? but delete does

* Fix Notifications UI (immediately remove corresponding card when approve/deny button clicked)

* Modify routes for testing purposes

* Debug statements --> user does not get found in denyUser

* Email successfully sent for deny (accidentally deleted user before trying to send email earlier lol)

* Fix frontend (populate account type)

* Remove extra comments

* added auth protection and cleaned up code

* added env for emails

* fix user role bug

* fixed some bugs and deleted some log statements

* ran lint fix

---------

Co-authored-by: adhi0331 <[email protected]>
  • Loading branch information
aammya8 and adhi0331 authored Jun 15, 2024
1 parent b8865a7 commit b0b800a
Show file tree
Hide file tree
Showing 14 changed files with 399 additions and 30 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
echo SERVICE_ACCOUNT_KEY=${{ secrets.SERVICE_ACCOUNT_KEY }} >> .env
echo APP_PORT=${{ secrets.APP_PORT }} >> .env
echo APP_FIREBASE_CONFIG=${{ secrets.APP_FIREBASE_CONFIG }} >> .env
echo EMAIL_ADDRESS_1=${{ secrets.EMAIL_ADDRESS_1 }} >> .env
echo PASS_1=${{ secrets.PASS_1 }} >> .env
- name: Build Frontend
run: cd frontend && npm ci && npm run build
- name: Build Backend
Expand Down
19 changes: 19 additions & 0 deletions backend/package-lock.json

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

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@
"firebase-functions": "^4.7.0",
"mongodb": "^6.3.0",
"mongoose": "^8.3.1",
"nodemailer": "^6.9.13",
"tsc-alias": "^1.8.8"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/nodemailer": "^6.4.14",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"eslint": "^8.56.0",
Expand Down
94 changes: 91 additions & 3 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { ServiceError } from "../errors/service";
import { ValidationError } from "../errors/validation";
import { Image } from "../models/image";
import UserModel from "../models/user";
import { sendApprovalEmail, sendDenialEmail } from "../util/email";
import { firebaseAdminAuth } from "../util/firebase";
import { handleImageParsing } from "../util/image";
import { deleteUserFromFirebase, deleteUserFromMongoDB } from "../util/user";
import validationErrorParser from "../util/validationErrorParser";

import { UserIdRequestBody } from "./types/types";
Expand Down Expand Up @@ -49,9 +51,6 @@ export const createUser = async (
name,
accountType,
email,
// profilePicture default "default" in User constructor
// lastChangedPassword default Date.now() in User constructor
// approvalStatus default false in User constructor
});

res.status(201).json(newUser);
Expand All @@ -63,6 +62,95 @@ export const createUser = async (
return;
};

export const deleteUser = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const { email } = req.params;

// Find the user by email
const user = await UserModel.findOne({ email });
if (!user) {
throw new Error("User not found");
}

const userId = user._id; // _id is the uid in schema

// delete user from Firebase and MongoDB
await deleteUserFromFirebase(userId);
await deleteUserFromMongoDB(userId);

res.status(200).send("User deleted successfully");
} catch (error) {
console.error("Error deleting user:", error);
nxt(error);
}
};

export const getNotApprovedUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
// const notApprovedUsers: User[] = await UserModel.find({ approvalStatus: false }).exec();
const notApprovedUsers = await UserModel.find({ approvalStatus: false }).exec();

res.status(200).json(notApprovedUsers);
} catch (error) {
console.error("Error fetching not-approved users:", error);
next(error);
}
};

export const approveUser = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const { email } = req.body;

const user = await UserModel.findOne({ email });
if (!user) {
return res.status(404).send("User not found");
}

const userId = user._id;

await UserModel.findByIdAndUpdate(userId, { approvalStatus: true });

// await sendApprovalEmail(email);
await sendApprovalEmail(email as string);

res.status(200).send("User approved successfully");
} catch (error) {
console.error(error);
nxt(error);
}
};

export const denyUser = async (req: Request, res: Response, nxt: NextFunction) => {
console.log("Inside denyUser controller");

try {
const { email } = req.body;

console.log("Email from request body:", email);

// const user = await UserModel.findOne({ email });
const user = await UserModel.findOne({ email });

if (!user) {
return res.status(404).send("User not found");
}

console.log("User object:", user);

const userId = user._id;

await UserModel.findByIdAndUpdate(userId, { approvalStatus: false });

console.log(email as string);
await sendDenialEmail(email as string);

res.status(200).send("User denied successfully");
} catch (error) {
console.error(error);
nxt(error);
}
};

export const loginUser = async (
req: Request<Record<string, never>, Record<string, never>, LoginUserRequestBody>,
res: Response,
Expand Down
9 changes: 9 additions & 0 deletions backend/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ const router = express.Router();
router.use(express.json());

router.post("/create", UserValidator.createUser, UserController.createUser);

router.post("/approve", [verifyAuthToken], UserController.approveUser);

router.post("/deny", [verifyAuthToken], UserController.denyUser);

router.delete("/delete/:email", [verifyAuthToken], UserController.deleteUser);

router.get("/not-approved", [verifyAuthToken], UserController.getNotApprovedUsers);

router.get("/", [verifyAuthToken], UserController.loginUser);
router.post("/editPhoto", [verifyAuthToken], UserValidator.editPhoto, UserController.editPhoto);
router.get("/getPhoto/:id", [verifyAuthToken], UserController.getPhoto);
Expand Down
59 changes: 59 additions & 0 deletions backend/src/util/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import dotenv from "dotenv";
dotenv.config();

import nodemailer from "nodemailer";

// Create a transporter object using SMTP transport
const transporter = nodemailer.createTransport({
service: "Gmail",
auth: {
user: process.env.EMAIL_ADDRESS_1,
pass: process.env.PASS_1,
},
});

export const sendApprovalEmail = async (email: string) => {
try {
await transporter.sendMail({
from: process.env.EMAIL_ADDRESS_1,
to: email,
subject: "Welcome to PIA! Your Account Has Been Approved",
// text: `Hello,
// Thank you for your interest in Plant It Again.
// We are emailing to let you know that your account
// creation request has been approved.`
html: `<p>Hello,</p>
<p>Thank you for your interest in Plant It Again.</p>
<p>We are emailing to let you know that your account creation request
has been <strong>approved</strong>.</p>`,
});
console.log("Approval email sent successfully");
} catch (error) {
console.error("Error sending approval email:", error);
}
};

export const sendDenialEmail = async (email: string) => {
console.log("Sending Denial Email");
try {
await transporter.sendMail({
from: process.env.EMAIL_ADDRESS_1,
to: email,
subject: "An Update on Your PIA Account Approval Status",
// text: `Hello,
// Thank you for your interest in Plant It Again.
// We are emailing to let you know that your account
// creation request has been denied.
// If you believe this a mistake,
// please contact us through our website`
html: `<p>Hello,</p>
<p>Thank you for your interest in Plant It Again.</p>
<p>We are emailing to let you know that your account creation request
has been <strong>denied</strong>.</p>
<p>If you believe this is a mistake, please contact us through our website.</p>`,
});
console.log("Denial email sent successfully");
} catch (error) {
console.error("Error sending denial email:", error);
}
};
13 changes: 13 additions & 0 deletions backend/src/util/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import UserModel from "../models/user";

import { firebaseAdminAuth } from "./firebase";

// delete user from Firebase
export const deleteUserFromFirebase = async (userId: string): Promise<void> => {
await firebaseAdminAuth.deleteUser(userId);
};

// delete user from MongoDB
export const deleteUserFromMongoDB = async (userId: string): Promise<void> => {
await UserModel.findByIdAndDelete(userId);
};
70 changes: 69 additions & 1 deletion frontend/src/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { APIResult, GET, PATCH, handleAPIError } from "@/api/requests";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { APIResult, DELETE, GET, PATCH, POST, handleAPIError } from "@/api/requests";

export type User = {
uid: string;
Expand All @@ -24,6 +26,72 @@ export const verifyUser = async (firebaseToken: string): Promise<APIResult<User>
}
};

export async function getNotApprovedUsers(firebaseToken: string): Promise<APIResult<User[]>> {
try {
const headers = createAuthHeader(firebaseToken);
console.log(headers);
const response = await GET("/user/not-approved", headers);
const json = (await response.json()) as User[];
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}

export async function approveUser(email: string, firebaseToken: string): Promise<APIResult<void>> {
try {
const headers = createAuthHeader(firebaseToken);
const response = await POST(`/user/approve`, { email }, headers);
if (response.ok) {
// return { success: true };
return { success: true, data: undefined }; // return APIResult<void> with empty data
} else {
const error = await response.json();
throw new Error(error.message || "Failed to approve user");
}
} catch (error) {
return { success: false, error: "Failed to approve user" };
}
}

export async function denyUser(email: string, firebaseToken: string): Promise<APIResult<void>> {
console.log("In frontend/src/api/user.ts denyUser()");

try {
const headers = createAuthHeader(firebaseToken);
const response = await POST(`/user/deny`, { email }, headers);
if (response.ok) {
// return { success: true };
return { success: true, data: undefined }; // return APIResult<void> with empty data
} else {
const error = await response.json();
throw new Error(error.message || "Failed to deny user");
}
} catch (error) {
return { success: false, error: "Error denying user" };
}
}

// delete user by email
export async function deleteUserByEmail(
email: string,
firebaseToken: string,
): Promise<APIResult<void>> {
try {
const headers = createAuthHeader(firebaseToken);
const response = await DELETE(`/user/delete/${encodeURIComponent(email)}`, undefined, headers);
if (response.ok) {
// return { success: true };
return { success: true, data: undefined };
} else {
const error = await response.json();
throw new Error(error.message || "Failed to delete user");
}
} catch (error) {
return handleAPIError(error);
}
}

type ObjectId = string; // This is a placeholder for the actual ObjectId type
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api";
export async function editPhoto(
Expand Down
Loading

0 comments on commit b0b800a

Please sign in to comment.