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", defaultValue: "false",
secret: false, secret: false,
}, },
maxExpiration: {
type: "number",
defaultValue: "0",
secret: false,
},
maxSize: { maxSize: {
type: "number", type: "number",
defaultValue: "1000000000", defaultValue: "1000000000",

View File

@@ -3,6 +3,7 @@ import * as moment from "moment";
import { ConfigService } from "src/config/config.service"; import { ConfigService } from "src/config/config.service";
import { FileService } from "src/file/file.service"; import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { parseRelativeDateToAbsolute } from "src/utils/date.util";
import { CreateReverseShareDTO } from "./dto/createReverseShare.dto"; import { CreateReverseShareDTO } from "./dto/createReverseShare.dto";
@Injectable() @Injectable()
@@ -24,6 +25,17 @@ export class ReverseShareService {
) )
.toDate(); .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"); const globalMaxShareSize = this.config.get("share.maxSize");
if (globalMaxShareSize < data.maxShareSize) 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 { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ReverseShareService } from "src/reverseShare/reverseShare.service"; import { ReverseShareService } from "src/reverseShare/reverseShare.service";
import { parseRelativeDateToAbsolute } from "src/utils/date.util";
import { SHARE_DIRECTORY } from "../constants"; import { SHARE_DIRECTORY } from "../constants";
import { CreateShareDTO } from "./dto/createShare.dto"; import { CreateShareDTO } from "./dto/createShare.dto";
@@ -51,19 +52,19 @@ export class ShareService {
if (reverseShare) { if (reverseShare) {
expirationDate = reverseShare.shareExpiration; expirationDate = reverseShare.shareExpiration;
} else { } else {
// We have to add an exception for "never" (since moment won't like that) const parsedExpiration = parseRelativeDateToAbsolute(share.expiration);
if (share.expiration !== "never") {
expirationDate = moment() if (
.add( this.config.get("share.maxExpiration") !== 0 &&
share.expiration.split("-")[0], parsedExpiration >
share.expiration.split( moment().add(this.config.get("share.maxExpiration"), "hours").toDate()
"-", ) {
)[1] as moment.unitOfTime.DurationConstructor, throw new BadRequestException(
) "Expiration date exceeds maximum expiration date",
.toDate(); );
} else {
expirationDate = moment(0).toDate();
} }
expirationDate = parsedExpiration;
} }
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, { 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 { useForm } from "@mantine/form";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context"; import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import useTranslate, { import useTranslate, {
translateOutsideContext, translateOutsideContext,
@@ -25,6 +26,7 @@ import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
const showCreateReverseShareModal = ( const showCreateReverseShareModal = (
modals: ModalsContextProps, modals: ModalsContextProps,
showSendEmailNotificationOption: boolean, showSendEmailNotificationOption: boolean,
maxExpirationInHours: number,
getReverseShares: () => void, getReverseShares: () => void,
) => { ) => {
const t = translateOutsideContext(); const t = translateOutsideContext();
@@ -34,6 +36,7 @@ const showCreateReverseShareModal = (
<Body <Body
showSendEmailNotificationOption={showSendEmailNotificationOption} showSendEmailNotificationOption={showSendEmailNotificationOption}
getReverseShares={getReverseShares} getReverseShares={getReverseShares}
maxExpirationInHours={maxExpirationInHours}
/> />
), ),
}); });
@@ -42,9 +45,11 @@ const showCreateReverseShareModal = (
const Body = ({ const Body = ({
getReverseShares, getReverseShares,
showSendEmailNotificationOption, showSendEmailNotificationOption,
maxExpirationInHours,
}: { }: {
getReverseShares: () => void; getReverseShares: () => void;
showSendEmailNotificationOption: boolean; showSendEmailNotificationOption: boolean;
maxExpirationInHours: number;
}) => { }) => {
const modals = useModals(); const modals = useModals();
const t = useTranslate(); const t = useTranslate();
@@ -58,27 +63,45 @@ const Body = ({
expiration_unit: "-days", 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 ( return (
<Group> <Group>
<form <form onSubmit={onSubmit}>
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);
})}
>
<Stack align="stretch"> <Stack align="stretch">
<div> <div>
<Grid align={form.errors.link ? "center" : "flex-end"}> <Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
<Col xs={6}> <Col xs={6}>
<NumberInput <NumberInput
min={1} min={1}

View File

@@ -18,6 +18,7 @@ import {
import { useForm, yupResolver } from "@mantine/form"; import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context"; import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import { useState } from "react"; import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb"; import { TbAlertCircle } from "react-icons/tb";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
@@ -38,6 +39,7 @@ const showCreateUploadModal = (
appUrl: string; appUrl: string;
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
maxExpirationInHours: number;
}, },
files: FileUpload[], files: FileUpload[],
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void, uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
@@ -69,6 +71,7 @@ const CreateUploadModalBody = ({
appUrl: string; appUrl: string;
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
maxExpirationInHours: number;
}; };
}) => { }) => {
const modals = useModals(); const modals = useModals();
@@ -92,6 +95,7 @@ const CreateUploadModalBody = ({
password: yup.string().min(3).max(30), password: yup.string().min(3).max(30),
maxViews: yup.number().min(1), maxViews: yup.number().min(1),
}); });
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
link: generatedLink, link: generatedLink,
@@ -105,6 +109,55 @@ const CreateUploadModalBody = ({
}, },
validate: yupResolver(validationSchema), 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 ( return (
<> <>
{showNotSignedInAlert && !options.isUserSignedIn && ( {showNotSignedInAlert && !options.isUserSignedIn && (
@@ -118,33 +171,9 @@ const CreateUploadModalBody = ({
<FormattedMessage id="upload.modal.not-signed-in-description" /> <FormattedMessage id="upload.modal.not-signed-in-description" />
</Alert> </Alert>
)} )}
<form <form onSubmit={onSubmit}>
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();
}
})}
>
<Stack align="stretch"> <Stack align="stretch">
<Group align="end"> <Group align={form.errors.link ? "center" : "flex-end"}>
<TextInput <TextInput
style={{ flex: "1" }} style={{ flex: "1" }}
variant="filled" variant="filled"
@@ -179,7 +208,7 @@ const CreateUploadModalBody = ({
</Text> </Text>
{!options.isReverseShare && ( {!options.isReverseShare && (
<> <>
<Grid align={form.errors.link ? "center" : "flex-end"}> <Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
<Col xs={6}> <Col xs={6}>
<NumberInput <NumberInput
min={1} min={1}

View File

@@ -288,6 +288,7 @@ export default {
"upload.modal.expires.never": "never", "upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires", "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.link.label": "Link",
"upload.modal.expires.label": "Expiration", "upload.modal.expires.label": "Expiration",
@@ -413,6 +414,9 @@ export default {
"Allow unauthenticated shares", "Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description": "admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthenticated users can create shares", "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": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes", "admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.share.zip-compression-level": "Zip compression level", "admin.config.share.zip-compression-level": "Zip compression level",

View File

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

View File

@@ -42,7 +42,14 @@ const Upload = ({
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => { const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
setisUploading(true); 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) => const fileUploadPromises = files.map(async (file, fileIndex) =>
// Limit the number of concurrent uploads to 3 // Limit the number of concurrent uploads to 3
@@ -132,6 +139,7 @@ const Upload = ({
"share.allowUnauthenticatedShares", "share.allowUnauthenticatedShares",
), ),
enableEmailRecepients: config.get("email.enableShareEmailRecipients"), enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
maxExpirationInHours: config.get("share.maxExpiration"),
}, },
files, files,
uploadFiles, uploadFiles,