From bbfc9d6f147eea404f011c3af9d7dc7655c3d21d Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Mon, 23 Oct 2023 15:17:47 +0200 Subject: [PATCH] feat: ability to limit the max expiration of a share --- backend/prisma/seed/config.seed.ts | 5 ++ .../src/reverseShare/reverseShare.service.ts | 12 +++ backend/src/share/share.service.ts | 25 +++--- backend/src/utils/date.util.ts | 12 +++ .../modals/showCreateReverseShareModal.tsx | 57 +++++++++---- .../upload/modals/showCreateUploadModal.tsx | 83 +++++++++++++------ frontend/src/i18n/translations/en-US.ts | 4 + frontend/src/pages/account/reverseShares.tsx | 1 + frontend/src/pages/upload/index.tsx | 10 ++- 9 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 backend/src/utils/date.util.ts diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index e48b591..80eeefa 100644 --- a/backend/prisma/seed/config.seed.ts +++ b/backend/prisma/seed/config.seed.ts @@ -37,6 +37,11 @@ const configVariables: ConfigVariables = { defaultValue: "false", secret: false, }, + maxExpiration: { + type: "number", + defaultValue: "0", + secret: false, + }, maxSize: { type: "number", defaultValue: "1000000000", diff --git a/backend/src/reverseShare/reverseShare.service.ts b/backend/src/reverseShare/reverseShare.service.ts index 29eeec5..134b72d 100644 --- a/backend/src/reverseShare/reverseShare.service.ts +++ b/backend/src/reverseShare/reverseShare.service.ts @@ -3,6 +3,7 @@ import * as moment from "moment"; import { ConfigService } from "src/config/config.service"; import { FileService } from "src/file/file.service"; import { PrismaService } from "src/prisma/prisma.service"; +import { parseRelativeDateToAbsolute } from "src/utils/date.util"; import { CreateReverseShareDTO } from "./dto/createReverseShare.dto"; @Injectable() @@ -24,6 +25,17 @@ export class ReverseShareService { ) .toDate(); + const parsedExpiration = parseRelativeDateToAbsolute(data.shareExpiration); + if ( + this.config.get("share.maxExpiration") !== 0 && + parsedExpiration > + moment().add(this.config.get("share.maxExpiration"), "hours").toDate() + ) { + throw new BadRequestException( + "Expiration date exceeds maximum expiration date", + ); + } + const globalMaxShareSize = this.config.get("share.maxSize"); if (globalMaxShareSize < data.maxShareSize) diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index 6276d4e..b3c745f 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -16,6 +16,7 @@ import { EmailService } from "src/email/email.service"; import { FileService } from "src/file/file.service"; import { PrismaService } from "src/prisma/prisma.service"; import { ReverseShareService } from "src/reverseShare/reverseShare.service"; +import { parseRelativeDateToAbsolute } from "src/utils/date.util"; import { SHARE_DIRECTORY } from "../constants"; import { CreateShareDTO } from "./dto/createShare.dto"; @@ -51,19 +52,19 @@ export class ShareService { if (reverseShare) { expirationDate = reverseShare.shareExpiration; } else { - // We have to add an exception for "never" (since moment won't like that) - if (share.expiration !== "never") { - expirationDate = moment() - .add( - share.expiration.split("-")[0], - share.expiration.split( - "-", - )[1] as moment.unitOfTime.DurationConstructor, - ) - .toDate(); - } else { - expirationDate = moment(0).toDate(); + const parsedExpiration = parseRelativeDateToAbsolute(share.expiration); + + if ( + this.config.get("share.maxExpiration") !== 0 && + parsedExpiration > + moment().add(this.config.get("share.maxExpiration"), "hours").toDate() + ) { + throw new BadRequestException( + "Expiration date exceeds maximum expiration date", + ); } + + expirationDate = parsedExpiration; } fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, { diff --git a/backend/src/utils/date.util.ts b/backend/src/utils/date.util.ts new file mode 100644 index 0000000..ab56615 --- /dev/null +++ b/backend/src/utils/date.util.ts @@ -0,0 +1,12 @@ +import * as moment from "moment"; + +export function parseRelativeDateToAbsolute(relativeDate: string) { + if (relativeDate == "never") return moment(0).toDate(); + + return moment() + .add( + relativeDate.split("-")[0], + relativeDate.split("-")[1] as moment.unitOfTime.DurationConstructor, + ) + .toDate(); +} diff --git a/frontend/src/components/share/modals/showCreateReverseShareModal.tsx b/frontend/src/components/share/modals/showCreateReverseShareModal.tsx index a31f480..46be7c4 100644 --- a/frontend/src/components/share/modals/showCreateReverseShareModal.tsx +++ b/frontend/src/components/share/modals/showCreateReverseShareModal.tsx @@ -12,6 +12,7 @@ import { import { useForm } from "@mantine/form"; import { useModals } from "@mantine/modals"; import { ModalsContextProps } from "@mantine/modals/lib/context"; +import moment from "moment"; import { FormattedMessage } from "react-intl"; import useTranslate, { translateOutsideContext, @@ -25,6 +26,7 @@ import showCompletedReverseShareModal from "./showCompletedReverseShareModal"; const showCreateReverseShareModal = ( modals: ModalsContextProps, showSendEmailNotificationOption: boolean, + maxExpirationInHours: number, getReverseShares: () => void, ) => { const t = translateOutsideContext(); @@ -34,6 +36,7 @@ const showCreateReverseShareModal = ( ), }); @@ -42,9 +45,11 @@ const showCreateReverseShareModal = ( const Body = ({ getReverseShares, showSendEmailNotificationOption, + maxExpirationInHours, }: { getReverseShares: () => void; showSendEmailNotificationOption: boolean; + maxExpirationInHours: number; }) => { const modals = useModals(); const t = useTranslate(); @@ -58,27 +63,45 @@ const Body = ({ expiration_unit: "-days", }, }); + + const onSubmit = form.onSubmit(async (values) => { + const expirationDate = moment().add( + form.values.expiration_num, + form.values.expiration_unit.replace( + "-", + "", + ) as moment.unitOfTime.DurationConstructor, + ); + if (expirationDate.isAfter(moment().add(maxExpirationInHours, "hours"))) { + form.setFieldError( + "expiration_num", + t("upload.modal.expires.error.too-long", { + max: moment.duration(maxExpirationInHours, "hours").humanize(), + }), + ); + return; + } + + shareService + .createReverseShare( + values.expiration_num + values.expiration_unit, + values.maxShareSize, + values.maxUseCount, + values.sendEmailNotification, + ) + .then(({ link }) => { + modals.closeAll(); + showCompletedReverseShareModal(modals, link, getReverseShares); + }) + .catch(toast.axiosError); + }); + return ( -
{ - shareService - .createReverseShare( - values.expiration_num + values.expiration_unit, - values.maxShareSize, - values.maxUseCount, - values.sendEmailNotification, - ) - .then(({ link }) => { - modals.closeAll(); - showCompletedReverseShareModal(modals, link, getReverseShares); - }) - .catch(toast.axiosError); - })} - > +
- + void, @@ -69,6 +71,7 @@ const CreateUploadModalBody = ({ appUrl: string; allowUnauthenticatedShares: boolean; enableEmailRecepients: boolean; + maxExpirationInHours: number; }; }) => { const modals = useModals(); @@ -92,6 +95,7 @@ const CreateUploadModalBody = ({ password: yup.string().min(3).max(30), maxViews: yup.number().min(1), }); + const form = useForm({ initialValues: { link: generatedLink, @@ -105,6 +109,55 @@ const CreateUploadModalBody = ({ }, validate: yupResolver(validationSchema), }); + + const onSubmit = form.onSubmit(async (values) => { + if (!(await shareService.isShareIdAvailable(values.link))) { + form.setFieldError("link", t("upload.modal.link.error.taken")); + } else { + const expirationString = form.values.never_expires + ? "never" + : form.values.expiration_num + form.values.expiration_unit; + + const expirationDate = moment().add( + form.values.expiration_num, + form.values.expiration_unit.replace( + "-", + "", + ) as moment.unitOfTime.DurationConstructor, + ); + if ( + expirationDate.isAfter( + moment().add(options.maxExpirationInHours, "hours"), + ) + ) { + form.setFieldError( + "expiration_num", + t("upload.modal.expires.error.too-long", { + max: moment + .duration(options.maxExpirationInHours, "hours") + .humanize(), + }), + ); + return; + } + + uploadCallback( + { + id: values.link, + expiration: expirationString, + recipients: values.recipients, + description: values.description, + security: { + password: values.password, + maxViews: values.maxViews, + }, + }, + files, + ); + modals.closeAll(); + } + }); + return ( <> {showNotSignedInAlert && !options.isUserSignedIn && ( @@ -118,33 +171,9 @@ const CreateUploadModalBody = ({ )} - { - if (!(await shareService.isShareIdAvailable(values.link))) { - form.setFieldError("link", t("upload.modal.link.error.taken")); - } else { - const expiration = form.values.never_expires - ? "never" - : form.values.expiration_num + form.values.expiration_unit; - uploadCallback( - { - id: values.link, - expiration: expiration, - recipients: values.recipients, - description: values.description, - security: { - password: values.password, - maxViews: values.maxViews, - }, - }, - files, - ); - modals.closeAll(); - } - })} - > + - + {!options.isReverseShare && ( <> - + { showCreateReverseShareModal( modals, config.get("smtp.enabled"), + config.get("share.maxExpiration"), getReverseShares, ) } diff --git a/frontend/src/pages/upload/index.tsx b/frontend/src/pages/upload/index.tsx index d5c64d1..1b68852 100644 --- a/frontend/src/pages/upload/index.tsx +++ b/frontend/src/pages/upload/index.tsx @@ -42,7 +42,14 @@ const Upload = ({ const uploadFiles = async (share: CreateShare, files: FileUpload[]) => { setisUploading(true); - createdShare = await shareService.create(share); + + try { + createdShare = await shareService.create(share); + } catch (e) { + toast.axiosError(e); + setisUploading(false); + return; + } const fileUploadPromises = files.map(async (file, fileIndex) => // Limit the number of concurrent uploads to 3 @@ -132,6 +139,7 @@ const Upload = ({ "share.allowUnauthenticatedShares", ), enableEmailRecepients: config.get("email.enableShareEmailRecipients"), + maxExpirationInHours: config.get("share.maxExpiration"), }, files, uploadFiles,