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
sendEmailNotification Boolean
remainingUses Int
simplified Boolean @default(false)
publicAccess Boolean @default(true)
creatorId String
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 { 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) {

View File

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

View File

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

View File

@@ -49,6 +49,8 @@ export class ReverseShareService {
remainingUses: data.maxUseCount,
maxShareSize: data.maxShareSize,
sendEmailNotification: data.sendEmailNotification,
simplified: data.simplified,
publicAccess: data.publicAccess,
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 {
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;
}
}

View File

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

View File

@@ -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) {