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",
|
defaultValue: "false",
|
||||||
secret: false,
|
secret: false,
|
||||||
},
|
},
|
||||||
|
maxExpiration: {
|
||||||
|
type: "number",
|
||||||
|
defaultValue: "0",
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
maxSize: {
|
maxSize: {
|
||||||
type: "number",
|
type: "number",
|
||||||
defaultValue: "1000000000",
|
defaultValue: "1000000000",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}`, {
|
||||||
|
|||||||
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 { 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,10 +63,25 @@ const Body = ({
|
|||||||
expiration_unit: "-days",
|
expiration_unit: "-days",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
|
||||||
<Group>
|
const onSubmit = form.onSubmit(async (values) => {
|
||||||
<form
|
const expirationDate = moment().add(
|
||||||
onSubmit={form.onSubmit(async (values) => {
|
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
|
shareService
|
||||||
.createReverseShare(
|
.createReverseShare(
|
||||||
values.expiration_num + values.expiration_unit,
|
values.expiration_num + values.expiration_unit,
|
||||||
@@ -74,11 +94,14 @@ const Body = ({
|
|||||||
showCompletedReverseShareModal(modals, link, getReverseShares);
|
showCompletedReverseShareModal(modals, link, getReverseShares);
|
||||||
})
|
})
|
||||||
.catch(toast.axiosError);
|
.catch(toast.axiosError);
|
||||||
})}
|
});
|
||||||
>
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
<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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const MyShares = () => {
|
|||||||
showCreateReverseShareModal(
|
showCreateReverseShareModal(
|
||||||
modals,
|
modals,
|
||||||
config.get("smtp.enabled"),
|
config.get("smtp.enabled"),
|
||||||
|
config.get("share.maxExpiration"),
|
||||||
getReverseShares,
|
getReverseShares,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,14 @@ const Upload = ({
|
|||||||
|
|
||||||
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
|
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
|
||||||
setisUploading(true);
|
setisUploading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
createdShare = await shareService.create(share);
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user