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:
Ivan Li
2024-07-30 14:26:56 +08:00
committed by GitHub
parent 3563715f57
commit fe735f9704
22 changed files with 355 additions and 28 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -103,6 +103,8 @@ model ReverseShare {
maxShareSize String maxShareSize String
sendEmailNotification Boolean sendEmailNotification Boolean
remainingUses Int remainingUses Int
simplified Boolean @default(false)
publicAccess Boolean @default(true)
creatorId String creatorId String
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade) creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)

View File

@@ -9,14 +9,16 @@ import * as moment from "moment";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard"; import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
import { ShareService } from "src/share/share.service"; import { ShareService } from "src/share/share.service";
import { ConfigService } from "src/config/config.service";
@Injectable() @Injectable()
export class FileSecurityGuard extends ShareSecurityGuard { export class FileSecurityGuard extends ShareSecurityGuard {
constructor( constructor(
private _shareService: ShareService, private _shareService: ShareService,
private _prisma: PrismaService, private _prisma: PrismaService,
_config: ConfigService,
) { ) {
super(_shareService, _prisma); super(_shareService, _prisma, _config);
} }
async canActivate(context: ExecutionContext) { async canActivate(context: ExecutionContext) {

View File

@@ -13,4 +13,10 @@ export class CreateReverseShareDTO {
@Min(1) @Min(1)
@Max(1000) @Max(1000)
maxUseCount: number; maxUseCount: number;
@IsBoolean()
simplified: boolean;
@IsBoolean()
publicAccess: boolean;
} }

View File

@@ -13,6 +13,9 @@ export class ReverseShareDTO {
@Expose() @Expose()
token: string; token: string;
@Expose()
simplified: boolean;
from(partial: Partial<ReverseShareDTO>) { from(partial: Partial<ReverseShareDTO>) {
return plainToClass(ReverseShareDTO, partial, { return plainToClass(ReverseShareDTO, partial, {
excludeExtraneousValues: true, excludeExtraneousValues: true,

View File

@@ -49,6 +49,8 @@ export class ReverseShareService {
remainingUses: data.maxUseCount, remainingUses: data.maxUseCount,
maxShareSize: data.maxShareSize, maxShareSize: data.maxShareSize,
sendEmailNotification: data.sendEmailNotification, sendEmailNotification: data.sendEmailNotification,
simplified: data.simplified,
publicAccess: data.publicAccess,
creatorId, creatorId,
}, },
}); });

View 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 }),
);
}
}

View File

@@ -1,5 +1,4 @@
import { import {
CanActivate,
ExecutionContext, ExecutionContext,
ForbiddenException, ForbiddenException,
Injectable, Injectable,
@@ -9,13 +8,19 @@ import { Request } from "express";
import * as moment from "moment"; import * as moment from "moment";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ShareService } from "src/share/share.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() @Injectable()
export class ShareSecurityGuard implements CanActivate { export class ShareSecurityGuard extends JwtGuard {
constructor( constructor(
private shareService: ShareService, private shareService: ShareService,
private prisma: PrismaService, private prisma: PrismaService,
) {} configService: ConfigService,
) {
super(configService);
}
async canActivate(context: ExecutionContext) { async canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest(); const request: Request = context.switchToHttp().getRequest();
@@ -31,7 +36,7 @@ export class ShareSecurityGuard implements CanActivate {
const share = await this.prisma.share.findUnique({ const share = await this.prisma.share.findUnique({
where: { id: shareId }, where: { id: shareId },
include: { security: true }, include: { security: true, reverseShare: true },
}); });
if ( if (
@@ -53,6 +58,19 @@ export class ShareSecurityGuard implements CanActivate {
"share_token_required", "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; return true;
} }
} }

View File

@@ -29,6 +29,7 @@ import { ShareOwnerGuard } from "./guard/shareOwner.guard";
import { ShareSecurityGuard } from "./guard/shareSecurity.guard"; import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard"; import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard";
import { ShareService } from "./share.service"; import { ShareService } from "./share.service";
import { CompletedShareDTO } from "./dto/shareComplete.dto";
@Controller("shares") @Controller("shares")
export class ShareController { export class ShareController {
constructor( constructor(
@@ -86,7 +87,7 @@ export class ShareController {
@UseGuards(CreateShareGuard, ShareOwnerGuard) @UseGuards(CreateShareGuard, ShareOwnerGuard)
async complete(@Param("id") id: string, @Req() request: Request) { async complete(@Param("id") id: string, @Req() request: Request) {
const { reverse_share_token } = request.cookies; const { reverse_share_token } = request.cookies;
return new ShareDTO().from( return new CompletedShareDTO().from(
await this.shareService.complete(id, reverse_share_token), await this.shareService.complete(id, reverse_share_token),
); );
} }

View File

@@ -159,11 +159,12 @@ export class ShareService {
); );
} }
if ( const notifyReverseShareCreator = share.reverseShare
share.reverseShare && ? this.config.get("smtp.enabled") &&
this.config.get("smtp.enabled") && share.reverseShare.sendEmailNotification
share.reverseShare.sendEmailNotification : undefined;
) {
if (notifyReverseShareCreator) {
await this.emailService.sendMailToReverseShareCreator( await this.emailService.sendMailToReverseShareCreator(
share.reverseShare.creator.email, share.reverseShare.creator.email,
share.id, share.id,
@@ -180,10 +181,15 @@ export class ShareService {
}); });
} }
return this.prisma.share.update({ const updatedShare = await this.prisma.share.update({
where: { id }, where: { id },
data: { uploadLocked: true }, data: { uploadLocked: true },
}); });
return {
...updatedShare,
notifyReverseShareCreator,
};
} }
async revertComplete(id: string) { async revertComplete(id: string) {

View File

@@ -22,6 +22,7 @@ import { getExpirationPreview } from "../../../utils/date.util";
import toast from "../../../utils/toast.util"; import toast from "../../../utils/toast.util";
import FileSizeInput from "../FileSizeInput"; import FileSizeInput from "../FileSizeInput";
import showCompletedReverseShareModal from "./showCompletedReverseShareModal"; import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
import { getCookie, setCookie } from "cookies-next";
const showCreateReverseShareModal = ( const showCreateReverseShareModal = (
modals: ModalsContextProps, modals: ModalsContextProps,
@@ -61,10 +62,16 @@ const Body = ({
sendEmailNotification: false, sendEmailNotification: false,
expiration_num: 1, expiration_num: 1,
expiration_unit: "-days", expiration_unit: "-days",
simplified: !!(getCookie("reverse-share.simplified") ?? false),
publicAccess: !!(getCookie("reverse-share.public-access") ?? true),
}, },
}); });
const onSubmit = form.onSubmit(async (values) => { 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( const expirationDate = moment().add(
form.values.expiration_num, form.values.expiration_num,
form.values.expiration_unit.replace( form.values.expiration_unit.replace(
@@ -91,6 +98,8 @@ const Body = ({
values.maxShareSize, values.maxShareSize,
values.maxUseCount, values.maxUseCount,
values.sendEmailNotification, values.sendEmailNotification,
values.simplified,
values.publicAccess,
) )
.then(({ link }) => { .then(({ link }) => {
modals.closeAll(); 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"> <Button mt="md" type="submit">
<FormattedMessage id="common.button.create" /> <FormattedMessage id="common.button.create" />
</Button> </Button>

View File

@@ -7,12 +7,12 @@ import { FormattedMessage } from "react-intl";
import useTranslate, { import useTranslate, {
translateOutsideContext, translateOutsideContext,
} from "../../../hooks/useTranslate.hook"; } from "../../../hooks/useTranslate.hook";
import { Share } from "../../../types/share.type"; import { CompletedShare } from "../../../types/share.type";
import CopyTextField from "../CopyTextField"; import CopyTextField from "../CopyTextField";
const showCompletedUploadModal = ( const showCompletedUploadModal = (
modals: ModalsContextProps, modals: ModalsContextProps,
share: Share, share: CompletedShare,
appUrl: string, appUrl: string,
) => { ) => {
const t = translateOutsideContext(); 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 modals = useModals();
const router = useRouter(); const router = useRouter();
const t = useTranslate(); const t = useTranslate();
@@ -35,6 +35,19 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
return ( return (
<Stack align="stretch"> <Stack align="stretch">
<CopyTextField link={link} /> <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 <Text
size="xs" size="xs"
sx={(theme) => ({ sx={(theme) => ({

View File

@@ -31,6 +31,7 @@ import { FileUpload } from "../../../types/File.type";
import { CreateShare } from "../../../types/share.type"; import { CreateShare } from "../../../types/share.type";
import { getExpirationPreview } from "../../../utils/date.util"; import { getExpirationPreview } from "../../../utils/date.util";
import React from "react"; import React from "react";
import toast from "../../../utils/toast.util";
const showCreateUploadModal = ( const showCreateUploadModal = (
modals: ModalsContextProps, modals: ModalsContextProps,
@@ -41,12 +42,26 @@ const showCreateUploadModal = (
allowUnauthenticatedShares: boolean; allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean; enableEmailRecepients: boolean;
maxExpirationInHours: number; maxExpirationInHours: number;
simplified: boolean;
}, },
files: FileUpload[], files: FileUpload[],
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void, uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
) => { ) => {
const t = translateOutsideContext(); 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({ return modals.openModal({
title: t("upload.modal.title"), title: t("upload.modal.title"),
children: ( 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 = ({ const CreateUploadModalBody = ({
uploadCallback, uploadCallback,
files, files,
@@ -78,9 +110,7 @@ const CreateUploadModalBody = ({
const modals = useModals(); const modals = useModals();
const t = useTranslate(); const t = useTranslate();
const generatedLink = Buffer.from(Math.random().toString(), "utf8") const generatedLink = generateLink();
.toString("base64")
.substr(10, 7);
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true); const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
@@ -202,14 +232,7 @@ const CreateUploadModalBody = ({
<Button <Button
style={{ flex: "0 0 auto" }} style={{ flex: "0 0 auto" }}
variant="outline" variant="outline"
onClick={() => onClick={() => form.setFieldValue("link", generateLink())}
form.setFieldValue(
"link",
Buffer.from(Math.random().toString(), "utf8")
.toString("base64")
.substr(10, 7),
)
}
> >
<FormattedMessage id="common.button.generate" /> <FormattedMessage id="common.button.generate" />
</Button> </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; export default showCreateUploadModal;

View File

@@ -199,6 +199,14 @@ export default {
"account.reverseShares.modal.send-email.description": "account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.", "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.label": "Max uses",
"account.reverseShares.modal.max-use.description": "account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.", "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": "upload.modal.completed.expires-on":
"This share will expire on {expiration}.", "This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready", "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 // END /upload
@@ -355,6 +364,8 @@ export default {
"share.error.not-found.title": "Share not found", "share.error.not-found.title": "Share not found",
"share.error.not-found.description": "share.error.not-found.description":
"The share you're looking for doesn't exist.", "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.title": "Password required",
"share.modal.password.description": "share.modal.password.description":

View File

@@ -152,6 +152,12 @@ export default {
"account.reverseShares.modal.max-size.label": "共享文件上限", "account.reverseShares.modal.max-size.label": "共享文件上限",
"account.reverseShares.modal.send-email": "发送邮件提醒", "account.reverseShares.modal.send-email": "发送邮件提醒",
"account.reverseShares.modal.send-email.description": "当这个预留共享链接被用于共享时,发送邮件提醒", "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.label": "最大使用次数",
"account.reverseShares.modal.max-use.description": "这个预留共享链接可被用于创建共享的最大使用次数", "account.reverseShares.modal.max-use.description": "这个预留共享链接可被用于创建共享的最大使用次数",
"account.reverseShare.never-expires": "这个预留共享永不过期", "account.reverseShare.never-expires": "这个预留共享永不过期",
@@ -255,6 +261,7 @@ export default {
"upload.modal.completed.never-expires": "这个共享永不过期", "upload.modal.completed.never-expires": "这个共享永不过期",
"upload.modal.completed.expires-on": "这个共享将过期于 {expiration}.", "upload.modal.completed.expires-on": "这个共享将过期于 {expiration}.",
"upload.modal.completed.share-ready": "共享创建完毕", "upload.modal.completed.share-ready": "共享创建完毕",
"upload.modal.completed.notified-reverse-share-creator": "我们已经通知预留共享的创建者。您也可以通过其他方式将该链接手动分享给他们。",
// END /upload // END /upload
// /share/[id] // /share/[id]
"share.title": "共享 {shareId}", "share.title": "共享 {shareId}",
@@ -264,6 +271,8 @@ export default {
"share.error.removed.title": "共享已删除", "share.error.removed.title": "共享已删除",
"share.error.not-found.title": "共享未找到", "share.error.not-found.title": "共享未找到",
"share.error.not-found.description": "共享文件走丢了", "share.error.not-found.description": "共享文件走丢了",
"share.error.access-denied.title": "私有共享",
"share.error.access-denied.description": "当前账户没有权限访问此共享",
"share.modal.password.title": "需要密码", "share.modal.password.title": "需要密码",
"share.modal.password.description": "请输入密码来访问此共享", "share.modal.password.description": "请输入密码来访问此共享",
"share.modal.password": "密码", "share.modal.password": "密码",

View File

@@ -43,6 +43,12 @@ const Share = ({ shareId }: { shareId: string }) => {
t("share.error.not-found.description"), 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 { } else {
showErrorModal(modals, t("common.error"), t("common.error.unknown")); showErrorModal(modals, t("common.error"), t("common.error.unknown"));
} }

View File

@@ -69,6 +69,12 @@ const Share = ({ shareId }: { shareId: string }) => {
"go-home", "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") { } else if (error == "share_password_required") {
showEnterPasswordModal(modals, getShareToken); showEnterPasswordModal(modals, getShareToken);
} else if (error == "share_token_required") { } else if (error == "share_token_required") {

View File

@@ -17,12 +17,14 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [maxShareSize, setMaxShareSize] = useState(0); const [maxShareSize, setMaxShareSize] = useState(0);
const [simplified, setSimplified] = useState(false);
useEffect(() => { useEffect(() => {
shareService shareService
.setReverseShare(reverseShareToken) .setReverseShare(reverseShareToken)
.then((reverseShareTokenData) => { .then((reverseShareTokenData) => {
setMaxShareSize(parseInt(reverseShareTokenData.maxShareSize)); setMaxShareSize(parseInt(reverseShareTokenData.maxShareSize));
setSimplified(reverseShareTokenData.simplified);
setIsLoading(false); setIsLoading(false);
}) })
.catch(() => { .catch(() => {
@@ -38,7 +40,13 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
if (isLoading) return <LoadingOverlay visible />; if (isLoading) return <LoadingOverlay visible />;
return <Upload isReverseShare maxShareSize={maxShareSize} />; return (
<Upload
isReverseShare
maxShareSize={maxShareSize}
simplified={simplified}
/>
);
}; };
export default Share; export default Share;

View File

@@ -25,9 +25,11 @@ let createdShare: Share;
const Upload = ({ const Upload = ({
maxShareSize, maxShareSize,
isReverseShare = false, isReverseShare = false,
simplified,
}: { }: {
maxShareSize?: number; maxShareSize?: number;
isReverseShare: boolean; isReverseShare: boolean;
simplified: boolean;
}) => { }) => {
const modals = useModals(); const modals = useModals();
const t = useTranslate(); const t = useTranslate();
@@ -133,6 +135,7 @@ const Upload = ({
), ),
enableEmailRecepients: config.get("email.enableShareEmailRecipients"), enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
maxExpirationInHours: config.get("share.maxExpiration"), maxExpirationInHours: config.get("share.maxExpiration"),
simplified,
}, },
files, files,
uploadFiles, uploadFiles,

View File

@@ -109,6 +109,8 @@ const createReverseShare = async (
maxShareSize: number, maxShareSize: number,
maxUseCount: number, maxUseCount: number,
sendEmailNotification: boolean, sendEmailNotification: boolean,
simplified: boolean,
publicAccess: boolean,
) => { ) => {
return ( return (
await api.post("reverseShares", { await api.post("reverseShares", {
@@ -116,6 +118,8 @@ const createReverseShare = async (
maxShareSize: maxShareSize.toString(), maxShareSize: maxShareSize.toString(),
maxUseCount, maxUseCount,
sendEmailNotification, sendEmailNotification,
simplified,
publicAccess,
}) })
).data; ).data;
}; };

View File

@@ -11,6 +11,15 @@ export type Share = {
hasPassword: boolean; 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 = { export type CreateShare = {
id: string; id: string;
name?: string; name?: string;