Skip to content

Commit

Permalink
Merge pull request #99 from VRI-UFPR/98-fix-automatic-deletion-of-loc…
Browse files Browse the repository at this point in the history
…al-files-on-the-server

#98 [FIX]: Automatic deletion of local files on the server and authorization fixes
  • Loading branch information
IosBonaldi authored Jul 8, 2024
2 parents 1958993 + a7ffec6 commit 16e9dfe
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 34 deletions.
11 changes: 9 additions & 2 deletions src/controllers/applicationAnswerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ApplicationAnswer, User, UserRole, VisibilityMode } from '@prisma/clien
import * as yup from 'yup';
import prismaClient from '../services/prismaClient';
import errorFormatter from '../services/errorFormatter';
import { text } from 'body-parser';
import { unlinkSync } from 'fs';

const checkAuthorization = async (
user: User,
Expand Down Expand Up @@ -323,6 +323,8 @@ export const createApplicationAnswer = async (req: Request, res: Response) => {

res.status(201).json({ message: 'Application answer created.', data: createdApplicationAnswer });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down Expand Up @@ -429,12 +431,15 @@ export const updateApplicationAnswer = async (req: Request, res: Response): Prom
},
});
//Remove files that are not in the updated item answer
await prisma.file.deleteMany({
const filesToDelete = await prisma.file.findMany({
where: {
id: { notIn: itemAnswer.filesIds.filter((fileId) => fileId).map((fileId) => fileId as number) },
itemAnswerId: upsertedItemAnswer.id,
},
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
// Create new files (udpating files is not supported)
const itemAnswerFiles = files
.filter(
Expand Down Expand Up @@ -506,6 +511,8 @@ export const updateApplicationAnswer = async (req: Request, res: Response): Prom

res.status(200).json({ message: 'Application answer updated.', data: upsertedApplicationAnswer });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down
13 changes: 10 additions & 3 deletions src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import prismaClient from '../services/prismaClient';
import jwt from 'jsonwebtoken';
import errorFormatter from '../services/errorFormatter';
import ms from 'ms';
import { profile } from 'console';

export const signUp = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -34,6 +35,7 @@ export const signUp = async (req: Request, res: Response) => {
institutionId: signingUser.institutionId,
classrooms: { connect: signingUser.classrooms.map((classroomId) => ({ id: classroomId })) },
},
include: { profileImage: true },
});
// JWT token creation
const token = jwt.sign({ id: createdUser.id, username: createdUser.username }, process.env.JWT_SECRET as string, {
Expand All @@ -48,6 +50,7 @@ export const signUp = async (req: Request, res: Response) => {
token: token,
expiresIn: process.env.JWT_EXPIRATION,
institutionId: createdUser.institutionId,
profileImage: createdUser.profileImage ? { path: createdUser.profileImage.path } : undefined,
},
});
} catch (error: any) {
Expand All @@ -65,10 +68,11 @@ export const signIn = async (req: Request, res: Response) => {
// Yup parsing/validation
const signingUser = await signInSchema.validate(req.body, { stripUnknown: false });
// Prisma operation
const user: User = await prismaClient.user.findUniqueOrThrow({
const user = await prismaClient.user.findUniqueOrThrow({
where: {
username: signingUser.username,
},
include: { profileImage: true },
});
// Password check
if (user.hash !== signingUser.hash) {
Expand All @@ -88,6 +92,7 @@ export const signIn = async (req: Request, res: Response) => {
token: token,
expiresIn: ms(process.env.JWT_EXPIRATION as string),
institutionId: user.institutionId,
profileImage: user.profileImage ? { path: user.profileImage.path } : undefined,
},
});
} catch (error: any) {
Expand All @@ -98,7 +103,7 @@ export const signIn = async (req: Request, res: Response) => {
export const passwordlessSignIn = async (req: Request, res: Response) => {
try {
// Prisma operation
const user = await prismaClient.user.findUniqueOrThrow({ where: { id: 1 } });
const user = await prismaClient.user.findUniqueOrThrow({ where: { id: 1 }, include: { profileImage: true } });
// JWT token creation
const token = jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET as string, {
expiresIn: process.env.JWT_EXPIRATION,
Expand All @@ -112,6 +117,7 @@ export const passwordlessSignIn = async (req: Request, res: Response) => {
token: token,
expiresIn: ms(process.env.JWT_EXPIRATION as string),
institutionId: user.institutionId,
profileImage: user.profileImage ? { path: user.profileImage.path } : undefined,
},
});
} catch (error: any) {
Expand All @@ -122,7 +128,7 @@ export const passwordlessSignIn = async (req: Request, res: Response) => {
export const renewSignIn = async (req: Request, res: Response) => {
try {
// User from Passport-JWT
const user = req.user as User;
const user = req.user as any;
// JWT token creation
const token = jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET as string, {
expiresIn: process.env.JWT_EXPIRATION,
Expand All @@ -136,6 +142,7 @@ export const renewSignIn = async (req: Request, res: Response) => {
token: token,
expiresIn: ms(process.env.JWT_EXPIRATION as string),
institutionId: user.institutionId,
profileImage: user.profileImage ? { path: user.profileImage.path } : undefined,
},
});
} catch (error) {
Expand Down
27 changes: 20 additions & 7 deletions src/controllers/protocolController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ItemType, ItemGroupType, PageType, ItemValidationType, User, UserRole,
import * as yup from 'yup';
import prismaClient from '../services/prismaClient';
import errorFormatter from '../services/errorFormatter';
import { unlinkSync } from 'fs';

const checkAuthorization = async (user: User, protocolId: number | undefined, action: string) => {
switch (action) {
Expand Down Expand Up @@ -558,6 +559,8 @@ export const createProtocol = async (req: Request, res: Response) => {
});
res.status(201).json({ message: 'Protocol created.', data: createdProtocol });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down Expand Up @@ -829,9 +832,12 @@ export const updateProtocol = async (req: Request, res: Response): Promise<void>
});
tempIdMap.set(item.tempId, upsertedItem.id);
// Remove files that are not in the updated item
await prisma.file.deleteMany({
where: { id: { notIn: item.filesIds as number[] }, itemId: upsertedItem.id },
const filesToDelete = await prisma.file.findMany({
where: { id: { notIn: item.filesIds as number[] } },
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
const itemFiles = files
.filter((file) =>
file.fieldname.startsWith(`pages[${pageId}][itemGroups][${itemGroupId}][items][${itemId}][files]`)
Expand Down Expand Up @@ -864,9 +870,12 @@ export const updateProtocol = async (req: Request, res: Response): Promise<void>
},
});
// Remove files that are not in the updated itemOption
await prisma.file.deleteMany({
const filesToDelete = await prisma.file.findMany({
where: { id: { notIn: itemOption.filesIds as number[] }, itemOptionId: upsertedItemOption.id },
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
const itemOptionFiles = files
.filter((file) =>
file.fieldname.startsWith(
Expand Down Expand Up @@ -973,6 +982,8 @@ export const updateProtocol = async (req: Request, res: Response): Promise<void>
});
res.status(200).json({ message: 'Protocol updated.', data: upsertedProtocol });
} catch (error: any) {
const files = req.files as Express.Multer.File[];
for (const file of files) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down Expand Up @@ -1063,10 +1074,12 @@ export const getProtocol = async (req: Request, res: Response): Promise<void> =>
});

const visibleProtocol =
user.role === UserRole.ADMIN ||
protocol.creator.id === user.id ||
protocol.managers.some((owner) => owner.id === user.id) ||
protocol.appliers.some((applier) => applier.id === user.id)
user.role !== UserRole.USER &&
(user.role === UserRole.ADMIN ||
protocol.creator.id === user.id ||
protocol.managers.some((owner) => owner.id === user.id) ||
protocol.appliers.some((applier) => applier.id === user.id) ||
protocol.applicability === VisibilityMode.PUBLIC)
? protocol
: {
...protocol,
Expand Down
67 changes: 47 additions & 20 deletions src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { User, UserRole } from '@prisma/client';
import * as yup from 'yup';
import prismaClient from '../services/prismaClient';
import errorFormatter from '../services/errorFormatter';
import { unlinkSync } from 'fs';

// Only admins or the user itself can perform --UD operations on users
const checkAuthorization = async (curUser: User, userId: number | undefined, role: UserRole | undefined, action: string) => {
Expand All @@ -21,13 +22,16 @@ const checkAuthorization = async (curUser: User, userId: number | undefined, rol
case 'update':
if (
// Only admins or the user itself can perform update operations on it, respecting the hierarchy
(curUser.role !== UserRole.ADMIN && curUser.id !== userId) ||
(curUser.role === UserRole.COORDINATOR && (role === UserRole.ADMIN || role === UserRole.COORDINATOR)) ||
(curUser.role === UserRole.PUBLISHER && role !== UserRole.USER) ||
curUser.role === UserRole.APPLIER ||
curUser.role === UserRole.USER
(curUser.role !== UserRole.ADMIN && Number(curUser.id) !== userId) ||
(curUser.role === UserRole.COORDINATOR && role === UserRole.ADMIN) ||
(curUser.role === UserRole.PUBLISHER &&
role !== UserRole.USER &&
role !== UserRole.APPLIER &&
role !== UserRole.PUBLISHER) ||
(curUser.role === UserRole.APPLIER && role !== UserRole.USER && role !== UserRole.APPLIER) ||
(curUser.role === UserRole.USER && role !== UserRole.USER)
) {
throw new Error('This user is not authorized to perform this action');
throw new Error('This user is not authorized to perform this action ' + curUser.id + ' ' + userId);
}
break;
case 'getAll':
Expand Down Expand Up @@ -64,6 +68,7 @@ const fields = {
role: true,
institution: { select: { id: true, name: true } },
classrooms: { select: { id: true, name: true } },
profileImage: { select: { id: true, path: true } },
acceptedTerms: true,
createdAt: true,
updatedAt: true,
Expand All @@ -90,22 +95,26 @@ export const createUser = async (req: Request, res: Response) => {
const curUser = req.user as User;
// Check if user is authorized to create a user
await checkAuthorization(curUser, undefined, user.role as UserRole, 'create');
// Multer single file
const file = req.file as Express.Multer.File;
// Prisma operation
const createdUser = await prismaClient.user.create({
data: {
id: user.id,
name: user.name,
username: user.username,
hash: user.hash,
role: user.role,
institutionId: user.institutionId,
classrooms: { connect: user.classrooms.map((id) => ({ id: id })) },
profileImage: file ? { create: { path: file.path } } : undefined,
institution: { connect: { id: user.institutionId } },
},
select: fields,
});

res.status(201).json({ message: 'User created.', data: createdUser });
} catch (error: any) {
const file = req.file as Express.Multer.File;
if (file) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand All @@ -124,6 +133,7 @@ export const updateUser = async (req: Request, res: Response): Promise<void> =>
role: yup.string().oneOf(Object.values(UserRole)),
institutionId: yup.number(),
classrooms: yup.array().of(yup.number()),
profileImageId: yup.number(),
})
.noUnknown();
// Yup parsing/validation
Expand All @@ -132,22 +142,39 @@ export const updateUser = async (req: Request, res: Response): Promise<void> =>
const curUser = req.user as User;
// Check if user is authorized to update the user
await checkAuthorization(curUser, userId, user.role as UserRole, 'update');
// Prisma operation
const updatedUser = await prismaClient.user.update({
where: { id: userId },
data: {
name: user.name,
username: user.username,
hash: user.hash,
role: user.role,
institutionId: user.institutionId,
classrooms: { set: [], connect: user.classrooms?.map((id) => ({ id: id })) },
},
select: fields,
// Multer single file
const file = req.file as Express.Multer.File;
// Prisma transaction
const updatedUser = await prismaClient.$transaction(async (prisma) => {
const filesToDelete = await prisma.file.findMany({
where: { id: { not: user.profileImageId }, users: { some: { id: userId } } },
select: { id: true, path: true },
});
for (const file of filesToDelete) unlinkSync(file.path);
await prisma.file.deleteMany({ where: { id: { in: filesToDelete.map((file) => file.id) } } });
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
name: user.name,
username: user.username,
hash: user.hash,
role: user.role,
institution: { connect: user.institutionId ? { id: user.institutionId } : undefined },
classrooms: { set: [], connect: user.classrooms?.map((id) => ({ id: id })) },
profileImage: {
create: !user.profileImageId && file ? { path: file.path } : undefined,
},
},
select: fields,
});

return updatedUser;
});

res.status(200).json({ message: 'User updated.', data: updatedUser });
} catch (error: any) {
const file = req.file as Express.Multer.File;
if (file) unlinkSync(file.path);
res.status(400).json(errorFormatter(error));
}
};
Expand Down
4 changes: 2 additions & 2 deletions src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ const router = express.Router();
* error:
* message: Internal Server Error.
*/
router.post('/createUser', passport.authenticate('jwt', { session: false }), uploader.none(), createUser);
router.post('/createUser', passport.authenticate('jwt', { session: false }), uploader.single('profileImage'), createUser);

/**
* @swagger
Expand Down Expand Up @@ -219,7 +219,7 @@ router.post('/createUser', passport.authenticate('jwt', { session: false }), upl
* error:
* message: Internal Server Error.
*/
router.put('/updateUser/:userId', passport.authenticate('jwt', { session: false }), uploader.none(), updateUser);
router.put('/updateUser/:userId', passport.authenticate('jwt', { session: false }), uploader.single('profileImage'), updateUser);

/**
* @swagger
Expand Down

0 comments on commit 16e9dfe

Please sign in to comment.