mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-05 07:47:01 +02:00
feat: add more options to reverse shares (#495)
* feat(reverse-share): optional simplified interface for reverse sharing. issue #155. * chore: Remove useless form validation. * feat: Share Ready modal adds a prompt that an email has been sent to the reverse share creator. * fix: Simplified reverse shared interface elements lack spacing when not logged in. * fix: Share Ready modal prompt contrast is too low in dark mode. * feat: add public access options to reverse share. * feat: remember reverse share simplified and publicAccess options in cookies. * style: npm run format. * chore(i18n): Improve translation. Co-authored-by: Elias Schneider <login@eliasschneider.com> Update frontend/src/i18n/translations/en-US.ts Co-authored-by: Elias Schneider <login@eliasschneider.com> Update frontend/src/i18n/translations/en-US.ts Co-authored-by: Elias Schneider <login@eliasschneider.com> chore(i18n): Improve translation. * chore: Improved variable naming. * chore(i18n): Improve translation. x2. * fix(backend/shares): Misjudged the permission of the share of the reverse share.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ReverseShare" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"token" TEXT NOT NULL,
|
||||
"shareExpiration" DATETIME NOT NULL,
|
||||
"maxShareSize" TEXT NOT NULL,
|
||||
"sendEmailNotification" BOOLEAN NOT NULL,
|
||||
"remainingUses" INTEGER NOT NULL,
|
||||
"simplified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "ReverseShare_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ReverseShare" ("createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "token") SELECT "createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "token" FROM "ReverseShare";
|
||||
DROP TABLE "ReverseShare";
|
||||
ALTER TABLE "new_ReverseShare" RENAME TO "ReverseShare";
|
||||
CREATE UNIQUE INDEX "ReverseShare_token_key" ON "ReverseShare"("token");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ReverseShare" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"token" TEXT NOT NULL,
|
||||
"shareExpiration" DATETIME NOT NULL,
|
||||
"maxShareSize" TEXT NOT NULL,
|
||||
"sendEmailNotification" BOOLEAN NOT NULL,
|
||||
"remainingUses" INTEGER NOT NULL,
|
||||
"simplified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"publicAccess" BOOLEAN NOT NULL DEFAULT true,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "ReverseShare_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ReverseShare" ("createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "simplified", "token") SELECT "createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "simplified", "token" FROM "ReverseShare";
|
||||
DROP TABLE "ReverseShare";
|
||||
ALTER TABLE "new_ReverseShare" RENAME TO "ReverseShare";
|
||||
CREATE UNIQUE INDEX "ReverseShare_token_key" ON "ReverseShare"("token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -103,6 +103,8 @@ model ReverseShare {
|
||||
maxShareSize String
|
||||
sendEmailNotification Boolean
|
||||
remainingUses Int
|
||||
simplified Boolean @default(false)
|
||||
publicAccess Boolean @default(true)
|
||||
|
||||
creatorId String
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -9,14 +9,16 @@ import * as moment from "moment";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
constructor(
|
||||
private _shareService: ShareService,
|
||||
private _prisma: PrismaService,
|
||||
_config: ConfigService,
|
||||
) {
|
||||
super(_shareService, _prisma);
|
||||
super(_shareService, _prisma, _config);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
|
||||
@@ -13,4 +13,10 @@ export class CreateReverseShareDTO {
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
maxUseCount: number;
|
||||
|
||||
@IsBoolean()
|
||||
simplified: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
publicAccess: boolean;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ export class ReverseShareDTO {
|
||||
@Expose()
|
||||
token: string;
|
||||
|
||||
@Expose()
|
||||
simplified: boolean;
|
||||
|
||||
from(partial: Partial<ReverseShareDTO>) {
|
||||
return plainToClass(ReverseShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
|
||||
@@ -49,6 +49,8 @@ export class ReverseShareService {
|
||||
remainingUses: data.maxUseCount,
|
||||
maxShareSize: data.maxShareSize,
|
||||
sendEmailNotification: data.sendEmailNotification,
|
||||
simplified: data.simplified,
|
||||
publicAccess: data.publicAccess,
|
||||
creatorId,
|
||||
},
|
||||
});
|
||||
|
||||
19
backend/src/share/dto/shareComplete.dto.ts
Normal file
19
backend/src/share/dto/shareComplete.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { ShareDTO } from "./share.dto";
|
||||
|
||||
export class CompletedShareDTO extends ShareDTO {
|
||||
@Expose()
|
||||
notifyReverseShareCreator?: boolean;
|
||||
|
||||
from(partial: Partial<CompletedShareDTO>) {
|
||||
return plainToClass(CompletedShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
}
|
||||
|
||||
fromList(partial: Partial<CompletedShareDTO>[]) {
|
||||
return partial.map((part) =>
|
||||
plainToClass(CompletedShareDTO, part, { excludeExtraneousValues: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
@@ -9,13 +8,19 @@ import { Request } from "express";
|
||||
import * as moment from "moment";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class ShareSecurityGuard implements CanActivate {
|
||||
export class ShareSecurityGuard extends JwtGuard {
|
||||
constructor(
|
||||
private shareService: ShareService,
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
@@ -31,7 +36,7 @@ export class ShareSecurityGuard implements CanActivate {
|
||||
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
include: { security: true },
|
||||
include: { security: true, reverseShare: true },
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -53,6 +58,19 @@ export class ShareSecurityGuard implements CanActivate {
|
||||
"share_token_required",
|
||||
);
|
||||
|
||||
// Run the JWTGuard to set the user
|
||||
await super.canActivate(context);
|
||||
const user = request.user as User;
|
||||
|
||||
// Only the creator and reverse share creator can access the reverse share if it's not public
|
||||
if (share.reverseShare && !share.reverseShare.publicAccess
|
||||
&& share.creatorId !== user?.id
|
||||
&& share.reverseShare.creatorId !== user?.id)
|
||||
throw new ForbiddenException(
|
||||
"Only reverse share creator can access this share",
|
||||
"private_share",
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ShareOwnerGuard } from "./guard/shareOwner.guard";
|
||||
import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
|
||||
import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard";
|
||||
import { ShareService } from "./share.service";
|
||||
import { CompletedShareDTO } from "./dto/shareComplete.dto";
|
||||
@Controller("shares")
|
||||
export class ShareController {
|
||||
constructor(
|
||||
@@ -86,7 +87,7 @@ export class ShareController {
|
||||
@UseGuards(CreateShareGuard, ShareOwnerGuard)
|
||||
async complete(@Param("id") id: string, @Req() request: Request) {
|
||||
const { reverse_share_token } = request.cookies;
|
||||
return new ShareDTO().from(
|
||||
return new CompletedShareDTO().from(
|
||||
await this.shareService.complete(id, reverse_share_token),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,11 +159,12 @@ export class ShareService {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
share.reverseShare &&
|
||||
this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
) {
|
||||
const notifyReverseShareCreator = share.reverseShare
|
||||
? this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
: undefined;
|
||||
|
||||
if (notifyReverseShareCreator) {
|
||||
await this.emailService.sendMailToReverseShareCreator(
|
||||
share.reverseShare.creator.email,
|
||||
share.id,
|
||||
@@ -180,10 +181,15 @@ export class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.share.update({
|
||||
const updatedShare = await this.prisma.share.update({
|
||||
where: { id },
|
||||
data: { uploadLocked: true },
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedShare,
|
||||
notifyReverseShareCreator,
|
||||
};
|
||||
}
|
||||
|
||||
async revertComplete(id: string) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import FileSizeInput from "../FileSizeInput";
|
||||
import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
|
||||
const showCreateReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -61,10 +62,16 @@ const Body = ({
|
||||
sendEmailNotification: false,
|
||||
expiration_num: 1,
|
||||
expiration_unit: "-days",
|
||||
simplified: !!(getCookie("reverse-share.simplified") ?? false),
|
||||
publicAccess: !!(getCookie("reverse-share.public-access") ?? true),
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
// remember simplified and publicAccess in cookies
|
||||
setCookie("reverse-share.simplified", values.simplified);
|
||||
setCookie("reverse-share.public-access", values.publicAccess);
|
||||
|
||||
const expirationDate = moment().add(
|
||||
form.values.expiration_num,
|
||||
form.values.expiration_unit.replace(
|
||||
@@ -91,6 +98,8 @@ const Body = ({
|
||||
values.maxShareSize,
|
||||
values.maxUseCount,
|
||||
values.sendEmailNotification,
|
||||
values.simplified,
|
||||
values.publicAccess,
|
||||
)
|
||||
.then(({ link }) => {
|
||||
modals.closeAll();
|
||||
@@ -210,7 +219,28 @@ const Body = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label={t("account.reverseShares.modal.simplified")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.simplified.description",
|
||||
)}
|
||||
{...form.getInputProps("simplified", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label={t("account.reverseShares.modal.public-access")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.public-access.description",
|
||||
)}
|
||||
{...form.getInputProps("publicAccess", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Button mt="md" type="submit">
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
|
||||
@@ -7,12 +7,12 @@ import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import { Share } from "../../../types/share.type";
|
||||
import { CompletedShare } from "../../../types/share.type";
|
||||
import CopyTextField from "../CopyTextField";
|
||||
|
||||
const showCompletedUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
share: Share,
|
||||
share: CompletedShare,
|
||||
appUrl: string,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
@@ -25,7 +25,7 @@ const showCompletedUploadModal = (
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
const Body = ({ share, appUrl }: { share: CompletedShare; appUrl: string }) => {
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
@@ -35,6 +35,19 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<CopyTextField link={link} />
|
||||
{share.notifyReverseShareCreator === true && (
|
||||
<Text
|
||||
size="sm"
|
||||
sx={(theme) => ({
|
||||
color:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.gray[3]
|
||||
: theme.colors.dark[4],
|
||||
})}
|
||||
>
|
||||
{t("upload.modal.completed.notified-reverse-share-creator")}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
|
||||
@@ -31,6 +31,7 @@ import { FileUpload } from "../../../types/File.type";
|
||||
import { CreateShare } from "../../../types/share.type";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import React from "react";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
const showCreateUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -41,12 +42,26 @@ const showCreateUploadModal = (
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
simplified: boolean;
|
||||
},
|
||||
files: FileUpload[],
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
|
||||
if (options.simplified) {
|
||||
return modals.openModal({
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
<SimplifiedCreateUploadModalModal
|
||||
options={options}
|
||||
files={files}
|
||||
uploadCallback={uploadCallback}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return modals.openModal({
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
@@ -59,6 +74,23 @@ const showCreateUploadModal = (
|
||||
});
|
||||
};
|
||||
|
||||
const generateLink = () =>
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substring(10, 17);
|
||||
|
||||
const generateAvailableLink = async (times = 10): Promise<string> => {
|
||||
if (times <= 0) {
|
||||
throw new Error("Could not generate available link");
|
||||
}
|
||||
const _link = generateLink();
|
||||
if (!(await shareService.isShareIdAvailable(_link))) {
|
||||
return await generateAvailableLink(times - 1);
|
||||
} else {
|
||||
return _link;
|
||||
}
|
||||
};
|
||||
|
||||
const CreateUploadModalBody = ({
|
||||
uploadCallback,
|
||||
files,
|
||||
@@ -78,9 +110,7 @@ const CreateUploadModalBody = ({
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7);
|
||||
const generatedLink = generateLink();
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
@@ -202,14 +232,7 @@ const CreateUploadModalBody = ({
|
||||
<Button
|
||||
style={{ flex: "0 0 auto" }}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7),
|
||||
)
|
||||
}
|
||||
onClick={() => form.setFieldValue("link", generateLink())}
|
||||
>
|
||||
<FormattedMessage id="common.button.generate" />
|
||||
</Button>
|
||||
@@ -429,4 +452,108 @@ const CreateUploadModalBody = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SimplifiedCreateUploadModalModal = ({
|
||||
uploadCallback,
|
||||
files,
|
||||
options,
|
||||
}: {
|
||||
files: FileUpload[];
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void;
|
||||
options: {
|
||||
isUserSignedIn: boolean;
|
||||
isReverseShare: boolean;
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
name: yup
|
||||
.string()
|
||||
.transform((value) => value || undefined)
|
||||
.min(3, t("common.error.too-short", { length: 3 }))
|
||||
.max(30, t("common.error.too-long", { length: 30 })),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
const link = await generateAvailableLink().catch(() => {
|
||||
toast.error(t("upload.modal.link.error.taken"));
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadCallback(
|
||||
{
|
||||
id: link,
|
||||
name: values.name,
|
||||
expiration: "never",
|
||||
recipients: [],
|
||||
description: values.description,
|
||||
security: {
|
||||
password: undefined,
|
||||
maxViews: undefined,
|
||||
},
|
||||
},
|
||||
files,
|
||||
);
|
||||
modals.closeAll();
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{showNotSignedInAlert && !options.isUserSignedIn && (
|
||||
<Alert
|
||||
withCloseButton
|
||||
onClose={() => setShowNotSignedInAlert(false)}
|
||||
icon={<TbAlertCircle size={16} />}
|
||||
title={t("upload.modal.not-signed-in")}
|
||||
color="yellow"
|
||||
>
|
||||
<FormattedMessage id="upload.modal.not-signed-in-description" />
|
||||
</Alert>
|
||||
)}
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack align="stretch">
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.name.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Textarea
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.description.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Stack>
|
||||
<Button type="submit" data-autofocus>
|
||||
<FormattedMessage id="common.button.share" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default showCreateUploadModal;
|
||||
|
||||
@@ -199,6 +199,14 @@ export default {
|
||||
"account.reverseShares.modal.send-email.description":
|
||||
"Send an email notification when a share is created with this reverse share link.",
|
||||
|
||||
"account.reverseShares.modal.simplified": "Simple mode",
|
||||
"account.reverseShares.modal.simplified.description":
|
||||
"Make it easy for the person uploading the file to share it with you. They will be able to customize only the name and description of the share.",
|
||||
|
||||
"account.reverseShares.modal.public-access": "Public access",
|
||||
"account.reverseShares.modal.public-access.description":
|
||||
"Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
|
||||
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||
"account.reverseShares.modal.max-use.description":
|
||||
"The maximum amount of times this URL can be used to create a share.",
|
||||
@@ -342,6 +350,7 @@ export default {
|
||||
"upload.modal.completed.expires-on":
|
||||
"This share will expire on {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Share ready",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "We have notified the creator of the reverse share. You can also manually share this link with them through other means.",
|
||||
|
||||
// END /upload
|
||||
|
||||
@@ -355,6 +364,8 @@ export default {
|
||||
"share.error.not-found.title": "Share not found",
|
||||
"share.error.not-found.description":
|
||||
"The share you're looking for doesn't exist.",
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description": "The current account does not have permission to access this share",
|
||||
|
||||
"share.modal.password.title": "Password required",
|
||||
"share.modal.password.description":
|
||||
|
||||
@@ -152,6 +152,12 @@ export default {
|
||||
"account.reverseShares.modal.max-size.label": "共享文件上限",
|
||||
"account.reverseShares.modal.send-email": "发送邮件提醒",
|
||||
"account.reverseShares.modal.send-email.description": "当这个预留共享链接被用于共享时,发送邮件提醒",
|
||||
"account.reverseShares.modal.simplified": "简单模式",
|
||||
"account.reverseShares.modal.simplified.description":
|
||||
"让上传者更轻松地与你共享文件,他们仅能自定义共享的名称和描述。",
|
||||
"account.reverseShares.modal.public-access": "公开访问",
|
||||
"account.reverseShares.modal.public-access.description":
|
||||
"让通过这个预留共享创建共享能被公开访问。如果禁用,将只有您和创建者能够访问。",
|
||||
"account.reverseShares.modal.max-use.label": "最大使用次数",
|
||||
"account.reverseShares.modal.max-use.description": "这个预留共享链接可被用于创建共享的最大使用次数",
|
||||
"account.reverseShare.never-expires": "这个预留共享永不过期",
|
||||
@@ -255,6 +261,7 @@ export default {
|
||||
"upload.modal.completed.never-expires": "这个共享永不过期",
|
||||
"upload.modal.completed.expires-on": "这个共享将过期于 {expiration}.",
|
||||
"upload.modal.completed.share-ready": "共享创建完毕",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "我们已经通知预留共享的创建者。您也可以通过其他方式将该链接手动分享给他们。",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "共享 {shareId}",
|
||||
@@ -264,6 +271,8 @@ export default {
|
||||
"share.error.removed.title": "共享已删除",
|
||||
"share.error.not-found.title": "共享未找到",
|
||||
"share.error.not-found.description": "共享文件走丢了",
|
||||
"share.error.access-denied.title": "私有共享",
|
||||
"share.error.access-denied.description": "当前账户没有权限访问此共享",
|
||||
"share.modal.password.title": "需要密码",
|
||||
"share.modal.password.description": "请输入密码来访问此共享",
|
||||
"share.modal.password": "密码",
|
||||
|
||||
@@ -43,6 +43,12 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
t("share.error.not-found.description"),
|
||||
);
|
||||
}
|
||||
} else if (e.response.status == 403 && error == "share_removed") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.access-denied.title"),
|
||||
t("share.error.access-denied.description"),
|
||||
);
|
||||
} else {
|
||||
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
"go-home",
|
||||
);
|
||||
}
|
||||
} else if (e.response.status == 403 && error == "private_share") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.access-denied.title"),
|
||||
t("share.error.access-denied.description"),
|
||||
);
|
||||
} else if (error == "share_password_required") {
|
||||
showEnterPasswordModal(modals, getShareToken);
|
||||
} else if (error == "share_token_required") {
|
||||
|
||||
@@ -17,12 +17,14 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [maxShareSize, setMaxShareSize] = useState(0);
|
||||
const [simplified, setSimplified] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
shareService
|
||||
.setReverseShare(reverseShareToken)
|
||||
.then((reverseShareTokenData) => {
|
||||
setMaxShareSize(parseInt(reverseShareTokenData.maxShareSize));
|
||||
setSimplified(reverseShareTokenData.simplified);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -38,7 +40,13 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
|
||||
|
||||
if (isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
return <Upload isReverseShare maxShareSize={maxShareSize} />;
|
||||
return (
|
||||
<Upload
|
||||
isReverseShare
|
||||
maxShareSize={maxShareSize}
|
||||
simplified={simplified}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Share;
|
||||
|
||||
@@ -25,9 +25,11 @@ let createdShare: Share;
|
||||
const Upload = ({
|
||||
maxShareSize,
|
||||
isReverseShare = false,
|
||||
simplified,
|
||||
}: {
|
||||
maxShareSize?: number;
|
||||
isReverseShare: boolean;
|
||||
simplified: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
@@ -133,6 +135,7 @@ const Upload = ({
|
||||
),
|
||||
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
|
||||
maxExpirationInHours: config.get("share.maxExpiration"),
|
||||
simplified,
|
||||
},
|
||||
files,
|
||||
uploadFiles,
|
||||
|
||||
@@ -109,6 +109,8 @@ const createReverseShare = async (
|
||||
maxShareSize: number,
|
||||
maxUseCount: number,
|
||||
sendEmailNotification: boolean,
|
||||
simplified: boolean,
|
||||
publicAccess: boolean,
|
||||
) => {
|
||||
return (
|
||||
await api.post("reverseShares", {
|
||||
@@ -116,6 +118,8 @@ const createReverseShare = async (
|
||||
maxShareSize: maxShareSize.toString(),
|
||||
maxUseCount,
|
||||
sendEmailNotification,
|
||||
simplified,
|
||||
publicAccess,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,15 @@ export type Share = {
|
||||
hasPassword: boolean;
|
||||
};
|
||||
|
||||
export type CompletedShare = Share & {
|
||||
/**
|
||||
* undefined means is not reverse share
|
||||
* true means server was send email to reverse share creator
|
||||
* false means server was not send email to reverse share creator
|
||||
* */
|
||||
notifyReverseShareCreator: boolean | undefined;
|
||||
};
|
||||
|
||||
export type CreateShare = {
|
||||
id: string;
|
||||
name?: string;
|
||||
|
||||
Reference in New Issue
Block a user