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

Image uploading #220

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
"@prisma/client": "^4.14.1",
"@types/supertest": "^2.0.12",
"bcrypt": "^5.1.0",
"cloudinary": "^2.0.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"dotenv-cli": "^7.1.0",
"express": "^4.17.3",
"express-async-errors": "^3.1.1",
"express-form-data": "^2.0.23",
"jsonwebtoken": "^9.0.0",
"pg": "^8.10.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "image_url" VARCHAR(400);
1 change: 1 addition & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ model User {
first_name String? @db.VarChar(25)
last_name String? @db.VarChar(25)
dob DateTime? @db.Date
image_url String? @db.VarChar(400)
gender_id Int?
bio String? @db.VarChar(500)
adminOf Neighborhood[] @relation("admin")
Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from 'express';
import * as formData from 'express-form-data';
import neighborhoodsRouter from './controllers/neighborhoods';
import notificationsRouter from './controllers/notifications';
import usersRouter from './controllers/users';
Expand All @@ -17,6 +18,15 @@ app.use(express.json());
app.use(middleware.requestLogger);
app.use(middleware.tokenExtractor);

// parse data with connect-multiparty.
app.use(formData.parse());
// delete from the request all empty files (size == 0)
app.use(formData.format());
// change the file objects to fs.ReadStream
app.use(formData.stream());
// union the body and the files
app.use(formData.union());

Comment on lines +21 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to find the docs to this express-form-data package but I'm having no luck. I presume this middleware intercepts requests that contain multipart forms and parses them?

// routes
app.use('/api/neighborhoods', neighborhoodsRouter);
app.use('/api/users', usersRouter);
Expand Down
19 changes: 19 additions & 0 deletions apps/backend/src/controllers/images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// import express, { Response } from 'express';
// import catchError from '../utils/catchError';
// import middleware from '../utils/middleware';

// const imageRouter = express.Router();

// imageRouter.post(
// '/upload',
// middleware.isUserLoggedIn,
// catchError(async (request: RequestWithAuthentication, response: Response) => {
// const userIsLoggedIn = typeof request.loggedUserId === 'number';



// return response.status(200));
// }),
// );

// export default imageRouter;
24 changes: 22 additions & 2 deletions apps/backend/src/controllers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ import { UserWithoutPasswordHash, RequestWithAuthentication } from '../types';

const usersRouter = express.Router();

// parse data with connect-multiparty.
// usersRouter.use(formData.parse());
// // delete from the request all empty files (size == 0)
// usersRouter.use(formData.format());
// // change the file objects to fs.ReadStream
// usersRouter.use(formData.stream());
// // union the body and the files
// usersRouter.use(formData.union());

usersRouter.get(
'/',
middleware.userIdExtractorAndLoginValidator,
catchError(async (_req: Request, res: Response) => {
const users: Array<UserWithoutPasswordHash> = await userServices.getAllUsers();

Expand All @@ -18,6 +28,8 @@ usersRouter.get(

usersRouter.get(
'/:id',
middleware.userIdExtractorAndLoginValidator,
middleware.validateURLParams,
catchError(async (req: Request, res: Response) => {
const userId: number = Number(req.params.id);

Expand All @@ -39,6 +51,7 @@ usersRouter.post(
newUser.username,
newUser.first_name || '',
newUser.last_name || '',
newUser.image_url || '',
);

return res.status(201).json(newUser);
Expand All @@ -51,10 +64,17 @@ usersRouter.put(
middleware.userIdExtractorAndLoginValidator,
catchError(async (req: RequestWithAuthentication, res: Response) => {
const userId = Number(req.params.id);
if (!(userId === Number(req.loggedUserId))) {
if (userId !== Number(req.loggedUserId)) {
return res.status(401).json('Logged user is not the owner of this profile');
}
const updatedUser = await userServices.updateUser(req.body, userId);

if ('image_url' in req.body && 'path' in req.body.image_url) {
req.body.image_url = req.body.image_url.path;
} else {
req.body.image_url = undefined;
}

const updatedUser = await userServices.updateUser(req.body, userId);
return res.status(200).json(updatedUser);
}),
);
Expand Down
74 changes: 74 additions & 0 deletions apps/backend/src/services/imageServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ErrorObj } from "../types";

// Require the cloudinary library
const cloudinary = require('cloudinary').v2;

export const URL =
`https://res.cloudinary.com/${process.env.CLOUD_NAME}/image/upload/v1710605561/`;

// Return "https" URLs by setting secure: true
cloudinary.config({
secure: true,
});

// Log the configuration
console.log(cloudinary.config());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? Or was it just for development purposes?


// Uploads an image file //
const uploadImage = async (imagePath: File | string, publicId: string): Promise<string | ErrorObj> => {
// Allow overwriting the asset with new versions
const options = {
public_id: publicId,
overwrite: true,
};

try {
const result = await cloudinary.uploader.upload(imagePath, options);
console.log(result);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary anymore

return result.public_id;
} catch (error) {
console.error(error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


return { error: 'Sorry, we couldn\'t upload your image.'}
}
};

// Gets details of an uploaded image //
// const getAssetInfo = async (publicId: number) => {
// // Return colors in the response
// const options = {
// colors: true,
// };

// try {
// // Get details about the asset
// const result = await cloudinary.api.resource(publicId, options);
// console.log(result);
// return result.colors;
// } catch (error) {
// console.error(error);
// }
// };

// Creates an HTML image tag with a transformation that
// results in a circular thumbnail crop of the image
// focused on the faces, applying an outline of the
// first color, and setting a background of the second color.
// const createImageTag = (publicId: number, ...colors: string[]) => {
// // Set the effect color and background color
// const [effectColor, backgroundColor] = colors;

// // Create an image tag with transformations applied to the src URL
// const imageTag = cloudinary.image(publicId, {
// transformation: [
// { width: 250, height: 250, gravity: 'faces', crop: 'thumb' },
// { radius: 'max' },
// { effect: 'outline:10', color: effectColor },
// { background: backgroundColor },
// ],
// });

// return imageTag;
// };

export { uploadImage };
71 changes: 70 additions & 1 deletion apps/backend/src/services/notificationServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,24 @@ export async function getTopics(numOfTopics: number): Promise<Topic[]> {

/**
* Creates a new subscriber to receive notifications
* @param id
* @param id to use as subscriberId
* @param username
* @param firstName
* @param lastName
* @param imageURL (optional) to use as the user's avatar
*/
export async function createSubscriber(
id: string,
username: string,
firstName: string,
lastName: string,
imageURL?: string,
) {
try {
await novu.subscribers.identify(id, {
firstName,
lastName,
avatar: imageURL,
data: {
username,
},
Expand All @@ -122,6 +126,63 @@ export async function getSubscriber(subscriberId: string) {
}
}

/**
* Updates a subscriber's info
* @param subscriberId
*/
export async function updateSubcriber({
subscriberId,
firstName,
lastName,
email,
avatar,
}: {
subscriberId: string;
firstName?: string;
lastName?: string;
email?: string;
avatar?: string;
}) {
try {
// If none of the values were updated ('' or undefined), return immediately
if ([firstName, lastName, email, avatar].every((val) => !val)) return;

// interface UpdateSubData {
// firstName?: string;
// lastName?: string;
// email?: string;
// imageUrl?: string;
// }

const subscriberInfo = { firstName, lastName, email, avatar };
console.log({ subscriberInfo });

const options = {
method: 'PUT',
headers: { Authorization: `ApiKey ${NOVU_API_KEY}` },
body: subscriberInfo as unknown as BodyInit,
};

const res = await fetch(`https://api.novu.co/v1/subscribers/${subscriberId}`, options)
.then((response) => response.json())
.then((response) => response.data)
.catch((err) => {
console.error(err);
return { error: `Could not update subscriber ${subscriberId}.` };
});

// const res = await novu.subscribers.update(subscriberId, {
// firstName,
// lastName,
// email,
// avatar: imageUrl,
// });
console.log('novu res', res);
} catch (error) {
console.error(error);
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will have to explain to me one day how the entire notification system works. I don't understand exactly what updateSubscriber does.

/**
* Retrieves all subscribers.
* @returns all subcribers or a custom error object if an error occurs
Expand Down Expand Up @@ -202,6 +263,11 @@ export const triggers = {
username,
}: JoinNeighborhoodArgs) {
try {
/*
* Checks if there is an identical notification in the recent history.
* Note that this implementation only checks the first page of results
* and not ALL the admin's notifications.
*/
const notifications = await getSubscriberNotifications(adminId);

const identicalNotification = notifications.some(
Expand All @@ -218,6 +284,9 @@ export const triggers = {
to: {
subscriberId: adminId,
},
actor: {
subscriberId: userId,
},
payload: {
neighborhoodId,
neighborhoodName,
Expand Down
Loading
Loading