Skip to content

Commit

Permalink
feat: ability to limit the max expiration of a share
Browse files Browse the repository at this point in the history
  • Loading branch information
stonith404 committed Oct 23, 2023
1 parent 46b6e56 commit bbfc9d6
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 57 deletions.
5 changes: 5 additions & 0 deletions backend/prisma/seed/config.seed.ts
Expand Up @@ -37,6 +37,11 @@ const configVariables: ConfigVariables = {
defaultValue: "false",
secret: false,
},
maxExpiration: {
type: "number",
defaultValue: "0",
secret: false,
},
maxSize: {
type: "number",
defaultValue: "1000000000",
Expand Down
12 changes: 12 additions & 0 deletions backend/src/reverseShare/reverseShare.service.ts
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
25 changes: 13 additions & 12 deletions backend/src/share/share.service.ts
Expand Up @@ -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";

Expand Down Expand Up @@ -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}`, {
Expand Down
12 changes: 12 additions & 0 deletions 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();
}
Expand Up @@ -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,
Expand All @@ -25,6 +26,7 @@ import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
const showCreateReverseShareModal = (
modals: ModalsContextProps,
showSendEmailNotificationOption: boolean,
maxExpirationInHours: number,
getReverseShares: () => void,
) => {
const t = translateOutsideContext();
Expand All @@ -34,6 +36,7 @@ const showCreateReverseShareModal = (
<Body
showSendEmailNotificationOption={showSendEmailNotificationOption}
getReverseShares={getReverseShares}
maxExpirationInHours={maxExpirationInHours}
/>
),
});
Expand All @@ -42,9 +45,11 @@ const showCreateReverseShareModal = (
const Body = ({
getReverseShares,
showSendEmailNotificationOption,
maxExpirationInHours,
}: {
getReverseShares: () => void;
showSendEmailNotificationOption: boolean;
maxExpirationInHours: number;
}) => {
const modals = useModals();
const t = useTranslate();
Expand All @@ -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 (
<Group>
<form
onSubmit={form.onSubmit(async (values) => {
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);
})}
>
<form onSubmit={onSubmit}>
<Stack align="stretch">
<div>
<Grid align={form.errors.link ? "center" : "flex-end"}>
<Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
<Col xs={6}>
<NumberInput
min={1}
Expand Down
83 changes: 56 additions & 27 deletions frontend/src/components/upload/modals/showCreateUploadModal.tsx
Expand Up @@ -18,6 +18,7 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
Expand All @@ -38,6 +39,7 @@ const showCreateUploadModal = (
appUrl: string;
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
maxExpirationInHours: number;
},
files: FileUpload[],
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
Expand Down Expand Up @@ -69,6 +71,7 @@ const CreateUploadModalBody = ({
appUrl: string;
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
maxExpirationInHours: number;
};
}) => {
const modals = useModals();
Expand All @@ -92,6 +95,7 @@ const CreateUploadModalBody = ({
password: yup.string().min(3).max(30),
maxViews: yup.number().min(1),
});

const form = useForm({
initialValues: {
link: generatedLink,
Expand All @@ -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 && (
Expand All @@ -118,33 +171,9 @@ const CreateUploadModalBody = ({
<FormattedMessage id="upload.modal.not-signed-in-description" />
</Alert>
)}
<form
onSubmit={form.onSubmit(async (values) => {
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();
}
})}
>
<form onSubmit={onSubmit}>
<Stack align="stretch">
<Group align="end">
<Group align={form.errors.link ? "center" : "flex-end"}>
<TextInput
style={{ flex: "1" }}
variant="filled"
Expand Down Expand Up @@ -179,7 +208,7 @@ const CreateUploadModalBody = ({
</Text>
{!options.isReverseShare && (
<>
<Grid align={form.errors.link ? "center" : "flex-end"}>
<Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
<Col xs={6}>
<NumberInput
min={1}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/i18n/translations/en-US.ts
Expand Up @@ -288,6 +288,7 @@ export default {

"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",

"upload.modal.link.label": "Link",
"upload.modal.expires.label": "Expiration",
Expand Down Expand Up @@ -413,6 +414,9 @@ export default {
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthenticated users can create shares",
"admin.config.share.max-expiration": "Max expiration",
"admin.config.share.max-expiration.description":
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.share.zip-compression-level": "Zip compression level",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/account/reverseShares.tsx
Expand Up @@ -77,6 +77,7 @@ const MyShares = () => {
showCreateReverseShareModal(
modals,
config.get("smtp.enabled"),
config.get("share.maxExpiration"),
getReverseShares,
)
}
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/pages/upload/index.tsx
Expand Up @@ -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
Expand Down Expand Up @@ -132,6 +139,7 @@ const Upload = ({
"share.allowUnauthenticatedShares",
),
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
maxExpirationInHours: config.get("share.maxExpiration"),
},
files,
uploadFiles,
Expand Down

0 comments on commit bbfc9d6

Please sign in to comment.