feat: ability to limit the max expiration of a share

This commit is contained in:
Elias Schneider
2023-10-23 15:17:47 +02:00
parent 46b6e56c06
commit bbfc9d6f14
9 changed files with 152 additions and 57 deletions

View File

@@ -37,6 +37,11 @@ const configVariables: ConfigVariables = {
defaultValue: "false",
secret: false,
},
maxExpiration: {
type: "number",
defaultValue: "0",
secret: false,
},
maxSize: {
type: "number",
defaultValue: "1000000000",

View File

@@ -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)

View File

@@ -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}`, {

View File

@@ -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();
}

View File

@@ -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 = (
<Body
showSendEmailNotificationOption={showSendEmailNotificationOption}
getReverseShares={getReverseShares}
maxExpirationInHours={maxExpirationInHours}
/>
),
});
@@ -42,9 +45,11 @@ const showCreateReverseShareModal = (
const Body = ({
getReverseShares,
showSendEmailNotificationOption,
maxExpirationInHours,
}: {
getReverseShares: () => void;
showSendEmailNotificationOption: boolean;
maxExpirationInHours: number;
}) => {
const modals = useModals();
const t = useTranslate();
@@ -58,10 +63,25 @@ const Body = ({
expiration_unit: "-days",
},
});
return (
<Group>
<form
onSubmit={form.onSubmit(async (values) => {
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,
@@ -74,11 +94,14 @@ const Body = ({
showCompletedReverseShareModal(modals, link, getReverseShares);
})
.catch(toast.axiosError);
})}
>
});
return (
<Group>
<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}

View File

@@ -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";
@@ -38,6 +39,7 @@ const showCreateUploadModal = (
appUrl: string;
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
maxExpirationInHours: number;
},
files: FileUpload[],
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => 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 = ({
<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"
@@ -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}

View File

@@ -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",
@@ -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",

View File

@@ -77,6 +77,7 @@ const MyShares = () => {
showCreateReverseShareModal(
modals,
config.get("smtp.enabled"),
config.get("share.maxExpiration"),
getReverseShares,
)
}

View File

@@ -42,7 +42,14 @@ const Upload = ({
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
setisUploading(true);
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,