Skip to content
This repository was archived by the owner on Jun 29, 2025. It is now read-only.

Commit bbfc9d6

Browse files
committed
feat: ability to limit the max expiration of a share
1 parent 46b6e56 commit bbfc9d6

File tree

9 files changed

+152
-57
lines changed

9 files changed

+152
-57
lines changed

backend/prisma/seed/config.seed.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const configVariables: ConfigVariables = {
3737
defaultValue: "false",
3838
secret: false,
3939
},
40+
maxExpiration: {
41+
type: "number",
42+
defaultValue: "0",
43+
secret: false,
44+
},
4045
maxSize: {
4146
type: "number",
4247
defaultValue: "1000000000",

backend/src/reverseShare/reverseShare.service.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as moment from "moment";
33
import { ConfigService } from "src/config/config.service";
44
import { FileService } from "src/file/file.service";
55
import { PrismaService } from "src/prisma/prisma.service";
6+
import { parseRelativeDateToAbsolute } from "src/utils/date.util";
67
import { CreateReverseShareDTO } from "./dto/createReverseShare.dto";
78

89
@Injectable()
@@ -24,6 +25,17 @@ export class ReverseShareService {
2425
)
2526
.toDate();
2627

28+
const parsedExpiration = parseRelativeDateToAbsolute(data.shareExpiration);
29+
if (
30+
this.config.get("share.maxExpiration") !== 0 &&
31+
parsedExpiration >
32+
moment().add(this.config.get("share.maxExpiration"), "hours").toDate()
33+
) {
34+
throw new BadRequestException(
35+
"Expiration date exceeds maximum expiration date",
36+
);
37+
}
38+
2739
const globalMaxShareSize = this.config.get("share.maxSize");
2840

2941
if (globalMaxShareSize < data.maxShareSize)

backend/src/share/share.service.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { EmailService } from "src/email/email.service";
1616
import { FileService } from "src/file/file.service";
1717
import { PrismaService } from "src/prisma/prisma.service";
1818
import { ReverseShareService } from "src/reverseShare/reverseShare.service";
19+
import { parseRelativeDateToAbsolute } from "src/utils/date.util";
1920
import { SHARE_DIRECTORY } from "../constants";
2021
import { CreateShareDTO } from "./dto/createShare.dto";
2122

@@ -51,19 +52,19 @@ export class ShareService {
5152
if (reverseShare) {
5253
expirationDate = reverseShare.shareExpiration;
5354
} else {
54-
// We have to add an exception for "never" (since moment won't like that)
55-
if (share.expiration !== "never") {
56-
expirationDate = moment()
57-
.add(
58-
share.expiration.split("-")[0],
59-
share.expiration.split(
60-
"-",
61-
)[1] as moment.unitOfTime.DurationConstructor,
62-
)
63-
.toDate();
64-
} else {
65-
expirationDate = moment(0).toDate();
55+
const parsedExpiration = parseRelativeDateToAbsolute(share.expiration);
56+
57+
if (
58+
this.config.get("share.maxExpiration") !== 0 &&
59+
parsedExpiration >
60+
moment().add(this.config.get("share.maxExpiration"), "hours").toDate()
61+
) {
62+
throw new BadRequestException(
63+
"Expiration date exceeds maximum expiration date",
64+
);
6665
}
66+
67+
expirationDate = parsedExpiration;
6768
}
6869

6970
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, {

backend/src/utils/date.util.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as moment from "moment";
2+
3+
export function parseRelativeDateToAbsolute(relativeDate: string) {
4+
if (relativeDate == "never") return moment(0).toDate();
5+
6+
return moment()
7+
.add(
8+
relativeDate.split("-")[0],
9+
relativeDate.split("-")[1] as moment.unitOfTime.DurationConstructor,
10+
)
11+
.toDate();
12+
}

frontend/src/components/share/modals/showCreateReverseShareModal.tsx

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { useForm } from "@mantine/form";
1313
import { useModals } from "@mantine/modals";
1414
import { ModalsContextProps } from "@mantine/modals/lib/context";
15+
import moment from "moment";
1516
import { FormattedMessage } from "react-intl";
1617
import useTranslate, {
1718
translateOutsideContext,
@@ -25,6 +26,7 @@ import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
2526
const showCreateReverseShareModal = (
2627
modals: ModalsContextProps,
2728
showSendEmailNotificationOption: boolean,
29+
maxExpirationInHours: number,
2830
getReverseShares: () => void,
2931
) => {
3032
const t = translateOutsideContext();
@@ -34,6 +36,7 @@ const showCreateReverseShareModal = (
3436
<Body
3537
showSendEmailNotificationOption={showSendEmailNotificationOption}
3638
getReverseShares={getReverseShares}
39+
maxExpirationInHours={maxExpirationInHours}
3740
/>
3841
),
3942
});
@@ -42,9 +45,11 @@ const showCreateReverseShareModal = (
4245
const Body = ({
4346
getReverseShares,
4447
showSendEmailNotificationOption,
48+
maxExpirationInHours,
4549
}: {
4650
getReverseShares: () => void;
4751
showSendEmailNotificationOption: boolean;
52+
maxExpirationInHours: number;
4853
}) => {
4954
const modals = useModals();
5055
const t = useTranslate();
@@ -58,27 +63,45 @@ const Body = ({
5863
expiration_unit: "-days",
5964
},
6065
});
66+
67+
const onSubmit = form.onSubmit(async (values) => {
68+
const expirationDate = moment().add(
69+
form.values.expiration_num,
70+
form.values.expiration_unit.replace(
71+
"-",
72+
"",
73+
) as moment.unitOfTime.DurationConstructor,
74+
);
75+
if (expirationDate.isAfter(moment().add(maxExpirationInHours, "hours"))) {
76+
form.setFieldError(
77+
"expiration_num",
78+
t("upload.modal.expires.error.too-long", {
79+
max: moment.duration(maxExpirationInHours, "hours").humanize(),
80+
}),
81+
);
82+
return;
83+
}
84+
85+
shareService
86+
.createReverseShare(
87+
values.expiration_num + values.expiration_unit,
88+
values.maxShareSize,
89+
values.maxUseCount,
90+
values.sendEmailNotification,
91+
)
92+
.then(({ link }) => {
93+
modals.closeAll();
94+
showCompletedReverseShareModal(modals, link, getReverseShares);
95+
})
96+
.catch(toast.axiosError);
97+
});
98+
6199
return (
62100
<Group>
63-
<form
64-
onSubmit={form.onSubmit(async (values) => {
65-
shareService
66-
.createReverseShare(
67-
values.expiration_num + values.expiration_unit,
68-
values.maxShareSize,
69-
values.maxUseCount,
70-
values.sendEmailNotification,
71-
)
72-
.then(({ link }) => {
73-
modals.closeAll();
74-
showCompletedReverseShareModal(modals, link, getReverseShares);
75-
})
76-
.catch(toast.axiosError);
77-
})}
78-
>
101+
<form onSubmit={onSubmit}>
79102
<Stack align="stretch">
80103
<div>
81-
<Grid align={form.errors.link ? "center" : "flex-end"}>
104+
<Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
82105
<Col xs={6}>
83106
<NumberInput
84107
min={1}

frontend/src/components/upload/modals/showCreateUploadModal.tsx

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { useForm, yupResolver } from "@mantine/form";
1919
import { useModals } from "@mantine/modals";
2020
import { ModalsContextProps } from "@mantine/modals/lib/context";
21+
import moment from "moment";
2122
import { useState } from "react";
2223
import { TbAlertCircle } from "react-icons/tb";
2324
import { FormattedMessage } from "react-intl";
@@ -38,6 +39,7 @@ const showCreateUploadModal = (
3839
appUrl: string;
3940
allowUnauthenticatedShares: boolean;
4041
enableEmailRecepients: boolean;
42+
maxExpirationInHours: number;
4143
},
4244
files: FileUpload[],
4345
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
@@ -69,6 +71,7 @@ const CreateUploadModalBody = ({
6971
appUrl: string;
7072
allowUnauthenticatedShares: boolean;
7173
enableEmailRecepients: boolean;
74+
maxExpirationInHours: number;
7275
};
7376
}) => {
7477
const modals = useModals();
@@ -92,6 +95,7 @@ const CreateUploadModalBody = ({
9295
password: yup.string().min(3).max(30),
9396
maxViews: yup.number().min(1),
9497
});
98+
9599
const form = useForm({
96100
initialValues: {
97101
link: generatedLink,
@@ -105,6 +109,55 @@ const CreateUploadModalBody = ({
105109
},
106110
validate: yupResolver(validationSchema),
107111
});
112+
113+
const onSubmit = form.onSubmit(async (values) => {
114+
if (!(await shareService.isShareIdAvailable(values.link))) {
115+
form.setFieldError("link", t("upload.modal.link.error.taken"));
116+
} else {
117+
const expirationString = form.values.never_expires
118+
? "never"
119+
: form.values.expiration_num + form.values.expiration_unit;
120+
121+
const expirationDate = moment().add(
122+
form.values.expiration_num,
123+
form.values.expiration_unit.replace(
124+
"-",
125+
"",
126+
) as moment.unitOfTime.DurationConstructor,
127+
);
128+
if (
129+
expirationDate.isAfter(
130+
moment().add(options.maxExpirationInHours, "hours"),
131+
)
132+
) {
133+
form.setFieldError(
134+
"expiration_num",
135+
t("upload.modal.expires.error.too-long", {
136+
max: moment
137+
.duration(options.maxExpirationInHours, "hours")
138+
.humanize(),
139+
}),
140+
);
141+
return;
142+
}
143+
144+
uploadCallback(
145+
{
146+
id: values.link,
147+
expiration: expirationString,
148+
recipients: values.recipients,
149+
description: values.description,
150+
security: {
151+
password: values.password,
152+
maxViews: values.maxViews,
153+
},
154+
},
155+
files,
156+
);
157+
modals.closeAll();
158+
}
159+
});
160+
108161
return (
109162
<>
110163
{showNotSignedInAlert && !options.isUserSignedIn && (
@@ -118,33 +171,9 @@ const CreateUploadModalBody = ({
118171
<FormattedMessage id="upload.modal.not-signed-in-description" />
119172
</Alert>
120173
)}
121-
<form
122-
onSubmit={form.onSubmit(async (values) => {
123-
if (!(await shareService.isShareIdAvailable(values.link))) {
124-
form.setFieldError("link", t("upload.modal.link.error.taken"));
125-
} else {
126-
const expiration = form.values.never_expires
127-
? "never"
128-
: form.values.expiration_num + form.values.expiration_unit;
129-
uploadCallback(
130-
{
131-
id: values.link,
132-
expiration: expiration,
133-
recipients: values.recipients,
134-
description: values.description,
135-
security: {
136-
password: values.password,
137-
maxViews: values.maxViews,
138-
},
139-
},
140-
files,
141-
);
142-
modals.closeAll();
143-
}
144-
})}
145-
>
174+
<form onSubmit={onSubmit}>
146175
<Stack align="stretch">
147-
<Group align="end">
176+
<Group align={form.errors.link ? "center" : "flex-end"}>
148177
<TextInput
149178
style={{ flex: "1" }}
150179
variant="filled"
@@ -179,7 +208,7 @@ const CreateUploadModalBody = ({
179208
</Text>
180209
{!options.isReverseShare && (
181210
<>
182-
<Grid align={form.errors.link ? "center" : "flex-end"}>
211+
<Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
183212
<Col xs={6}>
184213
<NumberInput
185214
min={1}

frontend/src/i18n/translations/en-US.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ export default {
288288

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

292293
"upload.modal.link.label": "Link",
293294
"upload.modal.expires.label": "Expiration",
@@ -413,6 +414,9 @@ export default {
413414
"Allow unauthenticated shares",
414415
"admin.config.share.allow-unauthenticated-shares.description":
415416
"Whether unauthenticated users can create shares",
417+
"admin.config.share.max-expiration": "Max expiration",
418+
"admin.config.share.max-expiration.description":
419+
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
416420
"admin.config.share.max-size": "Max size",
417421
"admin.config.share.max-size.description": "Maximum share size in bytes",
418422
"admin.config.share.zip-compression-level": "Zip compression level",

frontend/src/pages/account/reverseShares.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const MyShares = () => {
7777
showCreateReverseShareModal(
7878
modals,
7979
config.get("smtp.enabled"),
80+
config.get("share.maxExpiration"),
8081
getReverseShares,
8182
)
8283
}

frontend/src/pages/upload/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@ const Upload = ({
4242

4343
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
4444
setisUploading(true);
45-
createdShare = await shareService.create(share);
45+
46+
try {
47+
createdShare = await shareService.create(share);
48+
} catch (e) {
49+
toast.axiosError(e);
50+
setisUploading(false);
51+
return;
52+
}
4653

4754
const fileUploadPromises = files.map(async (file, fileIndex) =>
4855
// Limit the number of concurrent uploads to 3
@@ -132,6 +139,7 @@ const Upload = ({
132139
"share.allowUnauthenticatedShares",
133140
),
134141
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
142+
maxExpirationInHours: config.get("share.maxExpiration"),
135143
},
136144
files,
137145
uploadFiles,

0 commit comments

Comments
 (0)