Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile Backend and Frontend #86

Merged
merged 22 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2dc18b7
Contact and Password Frame Render
mraysu Jan 29, 2024
078c894
Added Profile Page UI
mraysu Feb 5, 2024
b5861f3
Merge branch 'main' of github.com:TritonSE/PIA-Program-Manager into f…
mraysu Feb 14, 2024
80817e9
feat: add basic info dialogs
aaronchan32 Feb 28, 2024
18f1d0c
Merge with main
aaronchan32 Feb 28, 2024
dcc0769
feat: add contact info and password info dialogs
aaronchan32 Feb 28, 2024
4577fc3
Merge remote-tracking branch 'origin/feature/aaronchan32/edit-student…
aaronchan32 Mar 6, 2024
12471f5
feat: add support for uploading profile images to mongodb using multer
aaronchan32 Apr 9, 2024
9ef55fb
Squashed merge with main
aaronchan32 Apr 9, 2024
9d618c0
feat: add support for uploading profile images to mongodb using multer
aaronchan32 Apr 9, 2024
42987d5
feat: add backend support for all profile fields
aaronchan32 Apr 10, 2024
d54fac5
Squashed merge with main
aaronchan32 Apr 10, 2024
fc737d1
Merge branch 'main' into feature/mraysu/profile_page_ui
aaronchan32 Apr 10, 2024
6af0829
fix: fix linting and styling issues
aaronchan32 Apr 10, 2024
9bb897f
fix: fix merge bugs
aaronchan32 Apr 10, 2024
3b4aa78
fix: fix api url and refactor frontend api files
aaronchan32 Apr 10, 2024
2466cd2
fix: use env variable for base api url
aaronchan32 Apr 10, 2024
41bb7cc
move method variable outside of req body for profile pic
adhi0331 Apr 10, 2024
acfcf94
added log statments to diagnose errors
adhi0331 Apr 10, 2024
3a900c0
fix: log out piaUser and mongoDB user
aaronchan32 Apr 10, 2024
fc75499
fix: testing firebase backend log
aaronchan32 Apr 10, 2024
ffd22f1
fix: use busboy to parse image data instead of multer to attempt to f…
aaronchan32 Apr 12, 2024
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
32 changes: 32 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 @@ -19,6 +19,8 @@
"node": "18"
},
"dependencies": {
"@types/busboy": "^1.5.3",
"busboy": "^1.6.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
Expand Down
232 changes: 229 additions & 3 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import busboy from "busboy";
import { NextFunction, Request, Response } from "express";
import { validationResult } from "express-validator";
import admin from "firebase-admin";
import mongoose from "mongoose";

import { InternalError } from "../errors";
import { ServiceError } from "../errors/service";
import { ValidationError } from "../errors/validation";
import { Image } from "../models/image";
import UserModel from "../models/user";
import { firebaseAdminAuth } from "../util/firebase";
import { saveImage } from "../util/image";
import validationErrorParser from "../util/validationErrorParser";

// Define the type for req.body
Expand All @@ -19,6 +25,22 @@ type LoginUserRequestBody = {
uid: string;
};

type UserId = {
userId: string;
};

type EditNameRequestBody = UserId & {
newName: string;
};

type EditEmailRequestBody = UserId & {
newEmail: string;
};

type EditLastChangedPasswordRequestBody = UserId & {
currentDate: string;
};

export const createUser = async (
req: Request<Record<string, never>, Record<string, never>, CreateUserRequestBody>,
res: Response,
Expand All @@ -45,6 +67,9 @@ export const createUser = async (
_id: userRecord.uid, // Set document id to firebaseUID (Linkage between Firebase and MongoDB)
name,
accountType,
email,
// profilePicture default "default" in User constructor
// lastChangedPassword default Date.now() in User constructor
// approvalStatus default false in User constructor
});

Expand All @@ -68,9 +93,15 @@ export const loginUser = async (
if (!user) {
throw ValidationError.USER_NOT_FOUND;
}
res
.status(200)
.json({ uid: user._id, role: user.accountType, approvalStatus: user.approvalStatus });
res.status(200).json({
uid: user._id,
role: user.accountType,
approvalStatus: user.approvalStatus,
profilePicture: user.profilePicture,
name: user.name,
email: user.email,
lastChangedPassword: user.lastChangedPassword,
});
return;
} catch (e) {
nxt();
Expand All @@ -80,3 +111,198 @@ export const loginUser = async (
});
}
};

export type SaveImageRequest = {
body: {
previousImageId: string;
userId: string;
};
file: {
buffer: Buffer;
originalname: string;
mimetype: string;
size: number;
};
};

type CustomRequest = Request & {
userId: string;
rawBody?: Buffer;
};

export const editPhoto = (req: Request, res: Response, nxt: NextFunction) => {
try {
const customReq = req as CustomRequest;
let previousImageId = "";
//req.userId is assigned in verifyAuthToken middleware
const userId: string = customReq.userId;

const bb = busboy({ headers: req.headers });

let fileBuffer = Buffer.alloc(0);
bb.on("field", (fieldname, val) => {
if (fieldname === "previousImageId") {
previousImageId = val;
}
});
bb.on("file", (name, file, info) => {
const { filename, mimeType } = info;

file
.on("data", (data) => {
fileBuffer = Buffer.concat([fileBuffer, data]);
})
.on("close", () => {
const saveImageRequest: SaveImageRequest = {
body: {
previousImageId,
userId,
},
file: {
buffer: fileBuffer,
originalname: filename,
mimetype: mimeType,
size: fileBuffer.length,
},
};

// Validate file in form data
try {
const acceptableTypes = ["image/jpeg", "image/png", "image/webp"];
if (!acceptableTypes.includes(mimeType)) {
throw ValidationError.IMAGE_UNSUPPORTED_TYPE;
}

// Check file size (2MB limit)
const maxSize = 2 * 1024 * 1024;
if (fileBuffer.length > maxSize) {
throw ValidationError.IMAGE_EXCEED_SIZE;
}

saveImage(saveImageRequest)
.then((savedImageId) => {
res.status(200).json(savedImageId);
})
.catch((error) => {
console.error("Error saving image:", error);
nxt(error); // Properly forward the error
});
} catch (error) {
console.error("Error parsing form with Busboy:", error);
nxt(error); // Properly forward the error
}
});
});

if (customReq.rawBody) {
bb.end(customReq.rawBody);
} else {
customReq.pipe(bb);
}
} catch (e) {
console.log(e);
nxt(e);
}
};

export const getPhoto = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const imageId = req.params.id;
if (!mongoose.Types.ObjectId.isValid(imageId)) {
return res
.status(ServiceError.INVALID_MONGO_ID.status)
.send({ error: ServiceError.INVALID_MONGO_ID.message });
}

const image = await Image.findById(imageId);
if (!image) {
throw ServiceError.IMAGE_NOT_FOUND;
}

return res.status(200).set("Content-type", image.mimetype).send(image.buffer);
} catch (e) {
console.log(e);
if (e instanceof ServiceError) {
nxt(e);
}
return res
.status(InternalError.ERROR_GETTING_IMAGE.status)
.send(InternalError.ERROR_GETTING_IMAGE.displayMessage(true));
}
};

export const editName = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const errors = validationResult(req);

validationErrorParser(errors);

console.log("test firebase log");

const { newName, userId } = req.body as EditNameRequestBody;

const user = await UserModel.findById(userId);

if (!user) {
throw ValidationError.USER_NOT_FOUND;
}

await UserModel.findByIdAndUpdate(userId, { name: newName });

res.status(200).json(newName);
} catch (error) {
nxt(error);
return res.status(400).json({
error,
});
}
};

export const editEmail = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const errors = validationResult(req);

validationErrorParser(errors);

const { newEmail, userId } = req.body as EditEmailRequestBody;

await firebaseAdminAuth.updateUser(userId, { email: newEmail });

const user = await UserModel.findById(userId);

if (!user) {
throw ValidationError.USER_NOT_FOUND;
}

await UserModel.findByIdAndUpdate(userId, { email: newEmail });

return res.status(200).json(newEmail);
} catch (error) {
nxt(error);
}
};

export const editLastChangedPassword = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const errors = validationResult(req);

validationErrorParser(errors);

const { currentDate, userId } = req.body as EditLastChangedPasswordRequestBody;

const user = await UserModel.findById(userId);

if (!user) {
throw ValidationError.USER_NOT_FOUND;
}

await UserModel.findByIdAndUpdate(userId, { lastChangedPassword: currentDate });

res.status(200).json(currentDate);
} catch (error) {
nxt(error);
return res.status(400).json({
error,
});
}
};
4 changes: 4 additions & 0 deletions backend/src/errors/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import { CustomError } from "./errors";

const LOGIN_ERROR = "Login Failed. Please check the username and password.";

const DECODE_ERROR = "Error decoding the auth token. Make sure the auth token is valid";
const TOKEN_NOT_IN_HEADER =
"Token was not found in the header. Be sure to use Bearer <Token> syntax";
Expand All @@ -15,4 +17,6 @@ export class AuthError extends CustomError {
static TOKEN_NOT_IN_HEADER = new AuthError(1, 401, TOKEN_NOT_IN_HEADER);

static INVALID_AUTH_TOKEN = new AuthError(2, 401, INVALID_AUTH_TOKEN);

static LOGIN_ERROR = new AuthError(3, 401, LOGIN_ERROR);
}
2 changes: 1 addition & 1 deletion backend/src/errors/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const errorHandler = (err: Error, _req: Request, res: Response, _nxt: Nex
if (!err) return;
if (err instanceof CustomError && !(err instanceof InternalError)) {
console.log(err.displayMessage(true));
res.status(err.status).send(err.displayMessage(true));
res.status(err.status).send({ error: err.message });
return;
}

Expand Down
3 changes: 3 additions & 0 deletions backend/src/errors/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const NO_APP_PORT = "Could not find app port env variable";
const NO_MONGO_URI = "Could not find mongo uri env variable";
const NO_SERVICE_ACCOUNT_KEY = "Could not find service account key env variable";
const NO_FIREBASE_CONFIG = "Could not firebase config env variable";
const ERROR_GETTING_IMAGE = "There was some error getting the image";

export class InternalError extends CustomError {
static NO_APP_PORT = new InternalError(0, 500, NO_APP_PORT);
Expand All @@ -13,4 +14,6 @@ export class InternalError extends CustomError {
static NO_SERVICE_ACCOUNT_KEY = new InternalError(5, 500, NO_SERVICE_ACCOUNT_KEY);

static NO_FIREBASE_CONFIG = new InternalError(5, 500, NO_FIREBASE_CONFIG);

static ERROR_GETTING_IMAGE = new InternalError(6, 500, ERROR_GETTING_IMAGE);
}
15 changes: 15 additions & 0 deletions backend/src/errors/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CustomError } from "./errors";

const IMAGE_NOT_SAVED =
"Image was not able to be saved. Be sure to specify that image key is image";
const IMAGE_NOT_FOUND = "Image was not found. Please make sure id passed in route is valid";
const INVALID_MONGO_ID = "Mongo ID was invalid. Please ensure that the id is correct";
const IMAGE_USER_MISMATCH = "Image does not belong to the user";

export class ServiceError extends CustomError {
static IMAGE_NOT_SAVED = new ServiceError(0, 404, IMAGE_NOT_SAVED);

static IMAGE_NOT_FOUND = new ServiceError(1, 404, IMAGE_NOT_FOUND);
static INVALID_MONGO_ID = new ServiceError(2, 404, INVALID_MONGO_ID);
static IMAGE_USER_MISMATCH = new ServiceError(3, 404, IMAGE_USER_MISMATCH);
}
Loading
Loading