feat: direct file link

This commit is contained in:
Elias Schneider
2023-01-31 15:22:08 +01:00
parent cd9d828686
commit 008df06b5c
11 changed files with 144 additions and 25 deletions

View File

@@ -14,8 +14,8 @@ import * as contentDisposition from "content-disposition";
import { Response } from "express";
import { CreateShareGuard } from "src/share/guard/createShare.guard";
import { ShareOwnerGuard } from "src/share/guard/shareOwner.guard";
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
import { FileService } from "./file.service";
import { FileSecurityGuard } from "./guard/fileSecurity.guard";
@Controller("shares/:shareId/files")
export class FileController {
@@ -43,7 +43,7 @@ export class FileController {
}
@Get("zip")
@UseGuards(ShareSecurityGuard)
@UseGuards(FileSecurityGuard)
async getZip(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string
@@ -58,7 +58,7 @@ export class FileController {
}
@Get(":fileId")
@UseGuards(ShareSecurityGuard)
@UseGuards(FileSecurityGuard)
async getFile(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,

View File

@@ -135,6 +135,4 @@ export class FileService {
getZip(shareId: string) {
return fs.createReadStream(`./data/uploads/shares/${shareId}/archive.zip`);
}
}

View File

@@ -0,0 +1,65 @@
import {
ExecutionContext,
ForbiddenException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { Request } from "express";
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";
@Injectable()
export class FileSecurityGuard extends ShareSecurityGuard {
constructor(
private _shareService: ShareService,
private _prisma: PrismaService
) {
super(_shareService, _prisma);
}
async canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest();
const shareId = Object.prototype.hasOwnProperty.call(
request.params,
"shareId"
)
? request.params.shareId
: request.params.id;
const shareToken = request.cookies[`share_${shareId}_token`];
const share = await this._prisma.share.findUnique({
where: { id: shareId },
include: { security: true },
});
// If there is no share token the user requests a file directly
if (!shareToken) {
if (
!share ||
(moment().isAfter(share.expiration) &&
!moment(share.expiration).isSame(0))
) {
throw new NotFoundException("File not found");
}
if (share.security?.password)
throw new ForbiddenException("This share is password protected");
if (share.security?.maxViews && share.security.maxViews <= share.views) {
throw new ForbiddenException(
"Maximum views exceeded",
"share_max_views_exceeded"
);
}
await this._shareService.increaseViewCount(share);
return true;
} else {
return super.canActivate(context);
}
}
}

View File

@@ -10,8 +10,11 @@ export class ReverseShareTokenWithShare extends OmitType(ReverseShareDTO, [
shareExpiration: Date;
@Expose()
@Type(() => OmitType(MyShareDTO, ["recipients"] as const))
share: Omit<MyShareDTO, "recipients" | "files" | "from" | "fromList">;
@Type(() => OmitType(MyShareDTO, ["recipients", "hasPassword"] as const))
share: Omit<
MyShareDTO,
"recipients" | "files" | "from" | "fromList" | "hasPassword"
>;
fromList(partial: Partial<ReverseShareTokenWithShare>[]) {
return partial.map((part) =>

View File

@@ -20,6 +20,9 @@ export class ShareDTO {
@Expose()
description: string;
@Expose()
hasPassword: boolean;
from(partial: Partial<ShareDTO>) {
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
}

View File

@@ -34,10 +34,12 @@ export class ShareSecurityGuard implements CanActivate {
include: { security: true },
});
const isExpired =
moment().isAfter(share.expiration) && !moment(share.expiration).isSame(0);
if (!share || isExpired) throw new NotFoundException("Share not found");
if (
!share ||
(moment().isAfter(share.expiration) &&
!moment(share.expiration).isSame(0))
)
throw new NotFoundException("Share not found");
if (share.security?.password && !shareToken)
throw new ForbiddenException(

View File

@@ -26,10 +26,12 @@ export class ShareTokenSecurity implements CanActivate {
include: { security: true },
});
const isExpired =
moment().isAfter(share.expiration) && !moment(share.expiration).isSame(0);
if (!share || isExpired) throw new NotFoundException("Share not found");
if (
!share ||
(moment().isAfter(share.expiration) &&
!moment(share.expiration).isSame(0))
)
throw new NotFoundException("Share not found");
return true;
}

View File

@@ -204,12 +204,13 @@ export class ShareService {
return sharesWithEmailRecipients;
}
async get(id: string) {
async get(id: string): Promise<any> {
const share = await this.prisma.share.findUnique({
where: { id },
include: {
files: true,
creator: true,
security: true,
},
});
@@ -218,8 +219,10 @@ export class ShareService {
if (!share || !share.uploadLocked)
throw new NotFoundException("Share not found");
return share as any;
return {
...share,
hasPassword: share.security?.password ? true : false,
};
}
async getMetaData(id: string) {