mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-11 10:27:01 +02:00
feat: ability to limit the max expiration of a share
This commit is contained in:
@@ -37,6 +37,11 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
maxExpiration: {
|
||||
type: "number",
|
||||
defaultValue: "0",
|
||||
secret: false,
|
||||
},
|
||||
maxSize: {
|
||||
type: "number",
|
||||
defaultValue: "1000000000",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
12
backend/src/utils/date.util.ts
Normal file
12
backend/src/utils/date.util.ts
Normal 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();
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -77,6 +77,7 @@ const MyShares = () => {
|
||||
showCreateReverseShareModal(
|
||||
modals,
|
||||
config.get("smtp.enabled"),
|
||||
config.get("share.maxExpiration"),
|
||||
getReverseShares,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user