From fb2dd3bd6f150a7367d5fdfcf44a00f458251e25 Mon Sep 17 00:00:00 2001 From: Steve Tautonico Date: Wed, 12 Oct 2022 16:03:27 -0400 Subject: [PATCH 1/7] chore: Fixed some missing dependencies in package.json The backend was missing "reflect-metadata" and the frontend was missing "@emotion/react" and "@emotion/server". Also added JetBrains specific stuff in .gitignore --- .gitignore | 3 +++ backend/package.json | 1 + frontend/package.json | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 21d982f..89d4739 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ yarn-error.log* # project specific /backend/data/ /data/ + +# Jetbrains specific (webstorm) +.idea/**/** \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 7ba4975..e0df3e1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,6 +27,7 @@ "passport": "^0.6.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2" }, "devDependencies": { diff --git a/frontend/package.json b/frontend/package.json index 1e4c45b..4ae636c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "format": "prettier --write \"src/**/*.ts\"" }, "dependencies": { + "@emotion/react": "^11.10.4", + "@emotion/server": "^11.10.0", "@mantine/core": "^5.5.2", "@mantine/dropzone": "^5.5.2", "@mantine/form": "^5.5.2", From 69ee88aebcc37c30ffbda69fef82db04d1e13660 Mon Sep 17 00:00:00 2001 From: Steve Tautonico Date: Wed, 12 Oct 2022 16:11:53 -0400 Subject: [PATCH 2/7] chore: Upgrade rxjs is required Without upgrading rxjs, we get `TypeError: (0 , rxjs_1.lastValueFrom) is not a function` This solution comes from https://github.com/nestjs/nest/issues/7468 --- backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index e0df3e1..5bf134e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,7 +28,8 @@ "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "reflect-metadata": "^0.1.13", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "rxjs": "^7.5.7" }, "devDependencies": { "@nestjs/cli": "^9.1.4", From 56349c6f4cc739d07bcf8ad862b0868e09342883 Mon Sep 17 00:00:00 2001 From: Steve Tautonico Date: Wed, 12 Oct 2022 16:59:04 -0400 Subject: [PATCH 3/7] feature: Added "never" expiration date --- backend/src/auth/jobs/jobs.service.ts | 57 +-- .../src/share/guard/shareSecurity.guard.ts | 2 +- backend/src/share/share.service.ts | 353 +++++++++--------- .../share/CreateUploadModalBody.tsx | 254 +++++++------ .../share/showEnterPasswordModal.tsx | 2 +- .../src/components/share/showErrorModal.tsx | 2 +- frontend/src/components/upload/Dropzone.tsx | 2 - .../upload/showCompletedUploadModal.tsx | 120 +++--- frontend/src/pages/account/shares.tsx | 4 +- 9 files changed, 406 insertions(+), 390 deletions(-) diff --git a/backend/src/auth/jobs/jobs.service.ts b/backend/src/auth/jobs/jobs.service.ts index a752afd..58334d4 100644 --- a/backend/src/auth/jobs/jobs.service.ts +++ b/backend/src/auth/jobs/jobs.service.ts @@ -2,36 +2,41 @@ import { Injectable } from "@nestjs/common"; import { Cron } from "@nestjs/schedule"; import { FileService } from "src/file/file.service"; import { PrismaService } from "src/prisma/prisma.service"; +import * as moment from "moment"; @Injectable() export class JobsService { - constructor( - private prisma: PrismaService, - private fileService: FileService - ) {} - - @Cron("0 * * * *") - async deleteExpiredShares() { - const expiredShares = await this.prisma.share.findMany({ - where: { expiration: { lt: new Date() } }, - }); - - for (const expiredShare of expiredShares) { - await this.prisma.share.delete({ - where: { id: expiredShare.id }, - }); - - await this.fileService.deleteAllFiles(expiredShare.id); + constructor( + private prisma: PrismaService, + private fileService: FileService + ) { } - console.log(`job: deleted ${expiredShares.length} expired shares`); - } + @Cron("0 * * * *") + async deleteExpiredShares() { + const expiredShares = await this.prisma.share.findMany({ + where: { + // We want to remove only shares that have an expiration date less than the current date, but not 0 + AND: [{expiration: {lt: new Date()}}, {expiration: {not: moment(0).toDate()}}] + }, + }); - @Cron("0 * * * *") - async deleteExpiredRefreshTokens() { - const expiredShares = await this.prisma.refreshToken.deleteMany({ - where: { expiresAt: { lt: new Date() } }, - }); - console.log(`job: deleted ${expiredShares.count} expired refresh tokens`); - } + for (const expiredShare of expiredShares) { + await this.prisma.share.delete({ + where: {id: expiredShare.id}, + }); + + await this.fileService.deleteAllFiles(expiredShare.id); + } + + console.log(`job: deleted ${expiredShares.length} expired shares`); + } + + @Cron("0 * * * *") + async deleteExpiredRefreshTokens() { + const expiredShares = await this.prisma.refreshToken.deleteMany({ + where: {expiresAt: {lt: new Date()}}, + }); + console.log(`job: deleted ${expiredShares.count} expired refresh tokens`); + } } diff --git a/backend/src/share/guard/shareSecurity.guard.ts b/backend/src/share/guard/shareSecurity.guard.ts index 5f9a729..57110ed 100644 --- a/backend/src/share/guard/shareSecurity.guard.ts +++ b/backend/src/share/guard/shareSecurity.guard.ts @@ -33,7 +33,7 @@ export class ShareSecurityGuard implements CanActivate { include: { security: true }, }); - if (!share || moment().isAfter(share.expiration)) + if (!share || (moment().isAfter(share.expiration) && moment(share.expiration).unix() !== 0)) throw new NotFoundException("Share not found"); if (!share.security) return true; diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index a8c4c4b..67d8c42 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -1,8 +1,8 @@ import { - BadRequestException, - ForbiddenException, - Injectable, - NotFoundException, + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; @@ -17,184 +17,195 @@ import { CreateShareDTO } from "./dto/createShare.dto"; @Injectable() export class ShareService { - constructor( - private prisma: PrismaService, - private fileService: FileService, - private config: ConfigService, - private jwtService: JwtService - ) {} - - async create(share: CreateShareDTO, user: User) { - if (!(await this.isShareIdAvailable(share.id)).isAvailable) - throw new BadRequestException("Share id already in use"); - - if (!share.security || Object.keys(share.security).length == 0) - share.security = undefined; - - if (share.security?.password) { - share.security.password = await argon.hash(share.security.password); + constructor( + private prisma: PrismaService, + private fileService: FileService, + private config: ConfigService, + private jwtService: JwtService + ) { } - const expirationDate = moment() - .add( - share.expiration.split("-")[0], - share.expiration.split("-")[1] as moment.unitOfTime.DurationConstructor - ) - .toDate(); + async create(share: CreateShareDTO, user: User) { + if (!(await this.isShareIdAvailable(share.id)).isAvailable) + throw new BadRequestException("Share id already in use"); - // Throw error if expiration date is now - if (expirationDate.setMilliseconds(0) == new Date().setMilliseconds(0)) - throw new BadRequestException("Invalid expiration date"); + if (!share.security || Object.keys(share.security).length == 0) + share.security = undefined; - return await this.prisma.share.create({ - data: { - ...share, - expiration: expirationDate, - creator: { connect: { id: user.id } }, - security: { create: share.security }, - }, - }); - } + if (share.security?.password) { + share.security.password = await argon.hash(share.security.password); + } - async createZip(shareId: string) { - const path = `./data/uploads/shares/${shareId}`; + // We have to add an exception for "never" (since moment won't like that) + let expirationDate; + if (share.expiration !== "never") { + expirationDate = moment() + .add( + share.expiration.split("-")[0], + share.expiration.split("-")[1] as moment.unitOfTime.DurationConstructor + ) + .toDate(); - const files = await this.prisma.file.findMany({ where: { shareId } }); - const archive = archiver("zip", { - zlib: { level: 9 }, - }); - const writeStream = fs.createWriteStream(`${path}/archive.zip`); + // Throw error if expiration date is now + if (expirationDate.setMilliseconds(0) == new Date().setMilliseconds(0)) + throw new BadRequestException("Invalid expiration date"); + } else { + expirationDate = moment(0).toDate(); + } - for (const file of files) { - archive.append(fs.createReadStream(`${path}/${file.id}`), { - name: file.name, - }); + return await this.prisma.share.create({ + data: { + ...share, + expiration: expirationDate, + creator: {connect: {id: user.id}}, + security: {create: share.security}, + }, + }); } - archive.pipe(writeStream); - await archive.finalize(); - } + async createZip(shareId: string) { + const path = `./data/uploads/shares/${shareId}`; - async complete(id: string) { - const moreThanOneFileInShare = - (await this.prisma.file.findMany({ where: { shareId: id } })).length != 0; + const files = await this.prisma.file.findMany({where: {shareId}}); + const archive = archiver("zip", { + zlib: {level: 9}, + }); + const writeStream = fs.createWriteStream(`${path}/archive.zip`); - if (!moreThanOneFileInShare) - throw new BadRequestException( - "You need at least on file in your share to complete it." - ); + for (const file of files) { + archive.append(fs.createReadStream(`${path}/${file.id}`), { + name: file.name, + }); + } - this.createZip(id).then(() => - this.prisma.share.update({ where: { id }, data: { isZipReady: true } }) - ); - - return await this.prisma.share.update({ - where: { id }, - data: { uploadLocked: true }, - }); - } - - async getSharesByUser(userId: string) { - return await this.prisma.share.findMany({ - where: { creator: { id: userId }, expiration: { gt: new Date() } }, - }); - } - - async get(id: string) { - let share: any = await this.prisma.share.findUnique({ - where: { id }, - include: { - files: true, - creator: true, - }, - }); - - if (!share || !share.uploadLocked) - throw new NotFoundException("Share not found"); - - share.files = share.files.map((file) => { - file["url"] = `http://localhost:8080/file/${file.id}`; - return file; - }); - - await this.increaseViewCount(share); - - return share; - } - - async getMetaData(id: string) { - const share = await this.prisma.share.findUnique({ - where: { id }, - }); - - if (!share || !share.uploadLocked) - throw new NotFoundException("Share not found"); - - return share; - } - - async remove(shareId: string) { - const share = await this.prisma.share.findUnique({ - where: { id: shareId }, - }); - - if (!share) throw new NotFoundException("Share not found"); - - await this.fileService.deleteAllFiles(shareId); - await this.prisma.share.delete({ where: { id: shareId } }); - } - - async isShareCompleted(id: string) { - return (await this.prisma.share.findUnique({ where: { id } })).uploadLocked; - } - - async isShareIdAvailable(id: string) { - const share = await this.prisma.share.findUnique({ where: { id } }); - return { isAvailable: !share }; - } - - async increaseViewCount(share: Share) { - await this.prisma.share.update({ - where: { id: share.id }, - data: { views: share.views + 1 }, - }); - } - - async exchangeSharePasswordWithToken(shareId: string, password: string) { - const sharePassword = ( - await this.prisma.shareSecurity.findFirst({ - where: { share: { id: shareId } }, - }) - ).password; - - if (!(await argon.verify(sharePassword, password))) - throw new ForbiddenException("Wrong password"); - - const token = this.generateShareToken(shareId); - return { token }; - } - - generateShareToken(shareId: string) { - return this.jwtService.sign( - { - shareId, - }, - { - expiresIn: "1h", - secret: this.config.get("JWT_SECRET"), - } - ); - } - - verifyShareToken(shareId: string, token: string) { - try { - const claims = this.jwtService.verify(token, { - secret: this.config.get("JWT_SECRET"), - }); - - return claims.shareId == shareId; - } catch { - return false; + archive.pipe(writeStream); + await archive.finalize(); + } + + async complete(id: string) { + const moreThanOneFileInShare = + (await this.prisma.file.findMany({where: {shareId: id}})).length != 0; + + if (!moreThanOneFileInShare) + throw new BadRequestException( + "You need at least on file in your share to complete it." + ); + + this.createZip(id).then(() => + this.prisma.share.update({where: {id}, data: {isZipReady: true}}) + ); + + return await this.prisma.share.update({ + where: {id}, + data: {uploadLocked: true}, + }); + } + + async getSharesByUser(userId: string) { + return await this.prisma.share.findMany({ + where: { + creator: {id: userId}, + // We want to grab any shares that are not expired or have their expiration date set to "never" (unix 0) + OR: [{expiration: {gt: new Date()}}, {expiration: {equals: moment(0).toDate()}}] + }, + }); + } + + async get(id: string) { + let share: any = await this.prisma.share.findUnique({ + where: {id}, + include: { + files: true, + creator: true, + }, + }); + + if (!share || !share.uploadLocked) + throw new NotFoundException("Share not found"); + + share.files = share.files.map((file) => { + file["url"] = `http://localhost:8080/file/${file.id}`; + return file; + }); + + await this.increaseViewCount(share); + + return share; + } + + async getMetaData(id: string) { + const share = await this.prisma.share.findUnique({ + where: {id}, + }); + + if (!share || !share.uploadLocked) + throw new NotFoundException("Share not found"); + + return share; + } + + async remove(shareId: string) { + const share = await this.prisma.share.findUnique({ + where: {id: shareId}, + }); + + if (!share) throw new NotFoundException("Share not found"); + + await this.fileService.deleteAllFiles(shareId); + await this.prisma.share.delete({where: {id: shareId}}); + } + + async isShareCompleted(id: string) { + return (await this.prisma.share.findUnique({where: {id}})).uploadLocked; + } + + async isShareIdAvailable(id: string) { + const share = await this.prisma.share.findUnique({where: {id}}); + return {isAvailable: !share}; + } + + async increaseViewCount(share: Share) { + await this.prisma.share.update({ + where: {id: share.id}, + data: {views: share.views + 1}, + }); + } + + async exchangeSharePasswordWithToken(shareId: string, password: string) { + const sharePassword = ( + await this.prisma.shareSecurity.findFirst({ + where: {share: {id: shareId}}, + }) + ).password; + + if (!(await argon.verify(sharePassword, password))) + throw new ForbiddenException("Wrong password"); + + const token = this.generateShareToken(shareId); + return {token}; + } + + generateShareToken(shareId: string) { + return this.jwtService.sign( + { + shareId, + }, + { + expiresIn: "1h", + secret: this.config.get("JWT_SECRET"), + } + ); + } + + verifyShareToken(shareId: string, token: string) { + try { + const claims = this.jwtService.verify(token, { + secret: this.config.get("JWT_SECRET"), + }); + + return claims.shareId == shareId; + } catch { + return false; + } } - } } diff --git a/frontend/src/components/share/CreateUploadModalBody.tsx b/frontend/src/components/share/CreateUploadModalBody.tsx index 659f5a8..14b28ea 100644 --- a/frontend/src/components/share/CreateUploadModalBody.tsx +++ b/frontend/src/components/share/CreateUploadModalBody.tsx @@ -1,14 +1,14 @@ import { - Accordion, - Button, - Col, - Grid, - NumberInput, - PasswordInput, - Select, - Stack, - Text, - TextInput, + Accordion, + Button, + Col, + Grid, + NumberInput, + PasswordInput, + Select, + Stack, + Text, + TextInput, } from "@mantine/core"; import { useForm, yupResolver } from "@mantine/form"; import { useModals } from "@mantine/modals"; @@ -17,129 +17,127 @@ import shareService from "../../services/share.service"; import { ShareSecurity } from "../../types/share.type"; const CreateUploadModalBody = ({ - uploadCallback, -}: { - uploadCallback: ( - id: string, - expiration: string, - security: ShareSecurity - ) => void; + uploadCallback, + }: { + uploadCallback: ( + id: string, + expiration: string, + security: ShareSecurity + ) => void; }) => { - const modals = useModals(); - const validationSchema = yup.object().shape({ - link: yup - .string() - .required() - .min(3) - .max(100) - .matches(new RegExp("^[a-zA-Z0-9_-]*$"), { - message: "Can only contain letters, numbers, underscores and hyphens", - }), - password: yup.string().min(3).max(30), - maxViews: yup.number().min(1), - }); - const form = useForm({ - initialValues: { - link: "", + const modals = useModals(); + const validationSchema = yup.object().shape({ + link: yup + .string() + .required() + .min(3) + .max(100) + .matches(new RegExp("^[a-zA-Z0-9_-]*$"), { + message: "Can only contain letters, numbers, underscores and hyphens", + }), + password: yup.string().min(3).max(30), + maxViews: yup.number().min(1), + }); + const form = useForm({ + initialValues: { + link: "", - password: undefined, - maxViews: undefined, - expiration: "1-day", - }, - validate: yupResolver(validationSchema), - }); + password: undefined, + maxViews: undefined, + expiration: "1-day", + }, + validate: yupResolver(validationSchema), + }); - return ( -
{ - if (!(await shareService.isShareIdAvailable(values.link))) { - form.setFieldError("link", "This link is already in use"); - } else { - uploadCallback(values.link, values.expiration, { - password: values.password, - maxViews: values.maxViews, - }); - modals.closeAll(); - } - })} - > - - - - - - - - - - - ({ - color: theme.colors.gray[6], - })} + return ( + { + if (!(await shareService.isShareIdAvailable(values.link))) { + form.setFieldError("link", "This link is already in use"); + } else { + uploadCallback(values.link, values.expiration, { + password: values.password, + maxViews: values.maxViews, + }); + modals.closeAll(); + } + })} > - {window.location.origin}/share/ - {form.values.link == "" ? "myAwesomeShare" : form.values.link} - - - - - - - - - -
- ); + + + Security options + + + + + + + + + + + + ); }; export default CreateUploadModalBody; diff --git a/frontend/src/components/share/showEnterPasswordModal.tsx b/frontend/src/components/share/showEnterPasswordModal.tsx index 7b910bc..e89bd89 100644 --- a/frontend/src/components/share/showEnterPasswordModal.tsx +++ b/frontend/src/components/share/showEnterPasswordModal.tsx @@ -1,4 +1,4 @@ -import { Button, Group, PasswordInput, Stack, Text, Title } from "@mantine/core"; +import { Button, PasswordInput, Stack, Text, Title } from "@mantine/core"; import { ModalsContextProps } from "@mantine/modals/lib/context"; import { useState } from "react"; diff --git a/frontend/src/components/share/showErrorModal.tsx b/frontend/src/components/share/showErrorModal.tsx index 0868ce3..c0c2783 100644 --- a/frontend/src/components/share/showErrorModal.tsx +++ b/frontend/src/components/share/showErrorModal.tsx @@ -1,4 +1,4 @@ -import { Button, Group, Stack, Text, Title } from "@mantine/core"; +import { Button, Stack, Text, Title } from "@mantine/core"; import { useModals } from "@mantine/modals"; import { ModalsContextProps } from "@mantine/modals/lib/context"; import { useRouter } from "next/router"; diff --git a/frontend/src/components/upload/Dropzone.tsx b/frontend/src/components/upload/Dropzone.tsx index e5a679c..2a7b1dd 100644 --- a/frontend/src/components/upload/Dropzone.tsx +++ b/frontend/src/components/upload/Dropzone.tsx @@ -4,7 +4,6 @@ import { createStyles, Group, Text, - useMantineTheme, } from "@mantine/core"; import { Dropzone as MantineDropzone } from "@mantine/dropzone"; import getConfig from "next/config"; @@ -46,7 +45,6 @@ const Dropzone = ({ isUploading: boolean; setFiles: Dispatch>; }) => { - const theme = useMantineTheme(); const { classes } = useStyles(); const openRef = useRef<() => void>(); return ( diff --git a/frontend/src/components/upload/showCompletedUploadModal.tsx b/frontend/src/components/upload/showCompletedUploadModal.tsx index 09c2b73..3d8374b 100644 --- a/frontend/src/components/upload/showCompletedUploadModal.tsx +++ b/frontend/src/components/upload/showCompletedUploadModal.tsx @@ -1,11 +1,10 @@ import { - ActionIcon, - Button, - Group, - Stack, - Text, - TextInput, - Title + ActionIcon, + Button, + Stack, + Text, + TextInput, + Title } from "@mantine/core"; import { useClipboard } from "@mantine/hooks"; import { useModals } from "@mantine/modals"; @@ -17,62 +16,65 @@ import { Share } from "../../types/share.type"; import toast from "../../utils/toast.util"; const showCompletedUploadModal = ( - modals: ModalsContextProps, - share: Share, + modals: ModalsContextProps, + share: Share, ) => { - return modals.openModal({ - closeOnClickOutside: false, - withCloseButton: false, - closeOnEscape: false, - title: ( - - Share ready - - ), - children: , - }); + return modals.openModal({ + closeOnClickOutside: false, + withCloseButton: false, + closeOnEscape: false, + title: ( + + Share ready + + ), + children: , + }); }; -const Body = ({ share }: { share: Share }) => { - const clipboard = useClipboard({ timeout: 500 }); - const modals = useModals(); - const router = useRouter(); - const link = `${window.location.origin}/share/${share.id}`; - return ( - - { - clipboard.copy(link); - toast.success("Your link was copied to the keyboard."); - }} - > - - - } - /> - ({ - color: theme.colors.gray[6], - })} - > - Your share expires at {moment(share.expiration).format("LLL")} - +const Body = ({share}: { share: Share }) => { + const clipboard = useClipboard({timeout: 500}); + const modals = useModals(); + const router = useRouter(); + const link = `${window.location.origin}/share/${share.id}`; + return ( + + { + clipboard.copy(link); + toast.success("Your link was copied to the keyboard."); + }} + > + + + } + /> + ({ + color: theme.colors.gray[6], + })} + > + {/* If our share.expiration is timestamp 0, show a different message */} + {moment(share.expiration).unix() === 0 + ? "This share will never expire." + : `This share will expire on ${moment(share.expiration).format("LLL")}`} + - - - ); + + + ); }; export default showCompletedUploadModal; diff --git a/frontend/src/pages/account/shares.tsx b/frontend/src/pages/account/shares.tsx index f3da20b..a9b4553 100644 --- a/frontend/src/pages/account/shares.tsx +++ b/frontend/src/pages/account/shares.tsx @@ -65,7 +65,9 @@ const MyShares = () => { {share.id} {share.views} - {moment(share.expiration).format("MMMM DD YYYY, HH:mm")} + {moment(share.expiration).unix() === 0 + ? "Never" + : moment(share.expiration).format("MMMM DD YYYY, HH:mm")} From 5988d5ffcc62421f04931ac134423498d514d8a6 Mon Sep 17 00:00:00 2001 From: Steve Tautonico Date: Fri, 14 Oct 2022 18:14:46 -0400 Subject: [PATCH 4/7] Added granular control of expiration + 12/24 hour modes --- frontend/.env.example | 1 + frontend/next.config.js | 3 +- .../share/CreateUploadModalBody.tsx | 90 +++++-- .../upload/showCompletedUploadModal.tsx | 8 +- frontend/src/pages/account/shares.tsx | 228 +++++++++--------- 5 files changed, 199 insertions(+), 131 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index dbda902..7e65299 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,3 +1,4 @@ SHOW_HOME_PAGE=true ALLOW_REGISTRATION=true MAX_FILE_SIZE=1000000000 +TWELVE_HOUR_TIME=false diff --git a/frontend/next.config.js b/frontend/next.config.js index b2b6bf5..dea14c0 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -5,7 +5,8 @@ const nextConfig = { ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION, SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE, MAX_FILE_SIZE: process.env.MAX_FILE_SIZE, - BACKEND_URL: process.env.BACKEND_URL + BACKEND_URL: process.env.BACKEND_URL, + TWELVE_HOUR_TIME: process.env.TWELVE_HOUR_TIME } } diff --git a/frontend/src/components/share/CreateUploadModalBody.tsx b/frontend/src/components/share/CreateUploadModalBody.tsx index 14b28ea..24d5b8c 100644 --- a/frontend/src/components/share/CreateUploadModalBody.tsx +++ b/frontend/src/components/share/CreateUploadModalBody.tsx @@ -2,6 +2,7 @@ import { Accordion, Button, Col, + Checkbox, Grid, NumberInput, PasswordInput, @@ -10,11 +11,32 @@ import { Text, TextInput, } from "@mantine/core"; -import { useForm, yupResolver } from "@mantine/form"; -import { useModals } from "@mantine/modals"; +import {useForm, yupResolver} from "@mantine/form"; +import {useModals} from "@mantine/modals"; import * as yup from "yup"; import shareService from "../../services/share.service"; -import { ShareSecurity } from "../../types/share.type"; +import {ShareSecurity} from "../../types/share.type"; +import moment from "moment"; +import getConfig from "next/config"; + +const {publicRuntimeConfig} = getConfig(); + +const PreviewExpiration = ({form}: { form: any }) => { + const value = form.values.never_expires ? "never" : form.values.expiration_num + form.values.expiration_unit; + if (value === "never") return "This share will never expire."; + + const expirationDate = moment() + .add( + value.split("-")[0], + value.split("-")[1] as moment.unitOfTime.DurationConstructor + ) + .toDate(); + + if (publicRuntimeConfig.TWELVE_HOUR_TIME === "true") + return `This share will expire on ${moment(expirationDate).format("MMMM Do YYYY, h:mm a")}`; + else + return `This share will expire on ${moment(expirationDate).format("MMMM DD YYYY, HH:mm")}`; +} const CreateUploadModalBody = ({ uploadCallback, @@ -44,7 +66,9 @@ const CreateUploadModalBody = ({ password: undefined, maxViews: undefined, - expiration: "1-day", + expiration_num: 1, + expiration_unit: "-days", + never_expires: false }, validate: yupResolver(validationSchema), }); @@ -55,7 +79,8 @@ const CreateUploadModalBody = ({ if (!(await shareService.isShareIdAvailable(values.link))) { form.setFieldError("link", "This link is already in use"); } else { - uploadCallback(values.link, values.expiration, { + const expiration = form.values.never_expires ? "never" : form.values.expiration_num + form.values.expiration_unit; + uploadCallback(values.link, expiration, { password: values.password, maxViews: values.maxViews, }); @@ -90,7 +115,7 @@ const CreateUploadModalBody = ({ - ({ color: theme.colors.gray[6], @@ -99,18 +124,47 @@ const CreateUploadModalBody = ({ {window.location.origin}/share/ {form.values.link == "" ? "myAwesomeShare" : form.values.link} - + + + + + {/* Preview expiration date text */} + ({ + color: theme.colors.gray[6], + })} + > + {PreviewExpiration({form})} + + Security options diff --git a/frontend/src/components/upload/showCompletedUploadModal.tsx b/frontend/src/components/upload/showCompletedUploadModal.tsx index 3d8374b..59c194d 100644 --- a/frontend/src/components/upload/showCompletedUploadModal.tsx +++ b/frontend/src/components/upload/showCompletedUploadModal.tsx @@ -14,6 +14,9 @@ import { useRouter } from "next/router"; import { Copy } from "tabler-icons-react"; import { Share } from "../../types/share.type"; import toast from "../../utils/toast.util"; +import getConfig from "next/config"; + +const {publicRuntimeConfig} = getConfig(); const showCompletedUploadModal = ( modals: ModalsContextProps, @@ -62,7 +65,10 @@ const Body = ({share}: { share: Share }) => { {/* If our share.expiration is timestamp 0, show a different message */} {moment(share.expiration).unix() === 0 ? "This share will never expire." - : `This share will expire on ${moment(share.expiration).format("LLL")}`} + : `This share will expire on ${ + (publicRuntimeConfig.TWELVE_HOUR_TIME === "true") + ? moment(share.expiration).format("MMMM Do YYYY, h:mm a") + : moment(share.expiration).format("MMMM DD YYYY, HH:mm")}`} - - - ) : ( - - - - - - - - - - - {shares.map((share) => ( - - - - - - - ))} - -
NameVisitorsExpires at
{share.id}{share.views} - {moment(share.expiration).unix() === 0 - ? "Never" - : moment(share.expiration).format("MMMM DD YYYY, HH:mm")} - - - { - clipboard.copy( - `${window.location.origin}/share/${share.id}` - ); - toast.success("Your link was copied to the keyboard."); - }} - > - - - { - modals.openConfirmModal({ - title: `Delete share ${share.id}`, - children: ( - - Do you really want to delete this share? - - ), - confirmProps: { - color: "red", - }, - labels: { confirm: "Confirm", cancel: "Cancel" }, - onConfirm: () => { - shareService.remove(share.id); - setShares( - shares.filter((item) => item.id !== share.id) - ); - }, - }); - }} - > - - - -
- )} - - ); + if (!shares) return ; + return ( + <> + + + My shares + + {shares.length == 0 ? ( +
+ + It's empty here 👀 + You don't have any shares. + + + +
+ ) : ( + + + + + + + + + + + {shares.map((share) => ( + + + + + + + ))} + +
NameVisitorsExpires at
{share.id}{share.views} + {moment(share.expiration).unix() === 0 + ? "Never" + : (publicRuntimeConfig.TWELVE_HOUR_TIME === "true") + ? moment(share.expiration).format("MMMM Do YYYY, h:mm a") + : moment(share.expiration).format("MMMM DD YYYY, HH:mm") + } + + + { + clipboard.copy( + `${window.location.origin}/share/${share.id}` + ); + toast.success("Your link was copied to the keyboard."); + }} + > + + + { + modals.openConfirmModal({ + title: `Delete share ${share.id}`, + children: ( + + Do you really want to delete this share? + + ), + confirmProps: { + color: "red", + }, + labels: {confirm: "Confirm", cancel: "Cancel"}, + onConfirm: () => { + shareService.remove(share.id); + setShares( + shares.filter((item) => item.id !== share.id) + ); + }, + }); + }} + > + + + +
+ )} + + ); }; export default MyShares; From 83cde4778aae748c26d0d438c29ab739dbe0539b Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sun, 16 Oct 2022 00:13:08 +0200 Subject: [PATCH 5/7] refactor: manual merge conflict changes that broke that application --- .../src/share/guard/shareTokenSecurity.guard.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/src/share/guard/shareTokenSecurity.guard.ts b/backend/src/share/guard/shareTokenSecurity.guard.ts index 86f0dd6..f108842 100644 --- a/backend/src/share/guard/shareTokenSecurity.guard.ts +++ b/backend/src/share/guard/shareTokenSecurity.guard.ts @@ -5,19 +5,13 @@ import { Injectable, NotFoundException, } from "@nestjs/common"; -import { Reflector } from "@nestjs/core"; import { Request } from "express"; import * as moment from "moment"; import { PrismaService } from "src/prisma/prisma.service"; -import { ShareService } from "src/share/share.service"; @Injectable() export class ShareTokenSecurity implements CanActivate { - constructor( - private reflector: Reflector, - private shareService: ShareService, - private prisma: PrismaService - ) {} + constructor(private prisma: PrismaService) {} async canActivate(context: ExecutionContext) { const request: Request = context.switchToHttp().getRequest(); @@ -33,7 +27,11 @@ export class ShareTokenSecurity implements CanActivate { include: { security: true }, }); - if (!share || moment().isAfter(share.expiration)) + if ( + !share || + (moment().isAfter(share.expiration) && + !moment(share.expiration).isSame(0)) + ) throw new NotFoundException("Share not found"); if (share.security?.maxViews && share.security.maxViews <= share.views) From c5099ce2e88f448e32c4b5a4e7e5319e91fe461e Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sun, 16 Oct 2022 00:14:02 +0200 Subject: [PATCH 6/7] refactor: run formatter --- backend/src/auth/jobs/jobs.service.ts | 62 ++++++++++--------- .../src/share/guard/shareSecurity.guard.ts | 6 +- frontend/package.json | 2 +- frontend/src/components/Footer.tsx | 5 +- .../src/components/navBar/ActionAvatar.tsx | 2 +- frontend/src/components/share/FileList.tsx | 2 +- frontend/src/components/upload/FileList.tsx | 2 +- .../upload/showCreateUploadModal.tsx | 6 +- 8 files changed, 47 insertions(+), 40 deletions(-) diff --git a/backend/src/auth/jobs/jobs.service.ts b/backend/src/auth/jobs/jobs.service.ts index 58334d4..688d2f3 100644 --- a/backend/src/auth/jobs/jobs.service.ts +++ b/backend/src/auth/jobs/jobs.service.ts @@ -6,37 +6,39 @@ import * as moment from "moment"; @Injectable() export class JobsService { - constructor( - private prisma: PrismaService, - private fileService: FileService - ) { + constructor( + private prisma: PrismaService, + private fileService: FileService + ) {} + + @Cron("0 * * * *") + async deleteExpiredShares() { + const expiredShares = await this.prisma.share.findMany({ + where: { + // We want to remove only shares that have an expiration date less than the current date, but not 0 + AND: [ + { expiration: { lt: new Date() } }, + { expiration: { not: moment(0).toDate() } }, + ], + }, + }); + + for (const expiredShare of expiredShares) { + await this.prisma.share.delete({ + where: { id: expiredShare.id }, + }); + + await this.fileService.deleteAllFiles(expiredShare.id); } - @Cron("0 * * * *") - async deleteExpiredShares() { - const expiredShares = await this.prisma.share.findMany({ - where: { - // We want to remove only shares that have an expiration date less than the current date, but not 0 - AND: [{expiration: {lt: new Date()}}, {expiration: {not: moment(0).toDate()}}] - }, - }); + console.log(`job: deleted ${expiredShares.length} expired shares`); + } - for (const expiredShare of expiredShares) { - await this.prisma.share.delete({ - where: {id: expiredShare.id}, - }); - - await this.fileService.deleteAllFiles(expiredShare.id); - } - - console.log(`job: deleted ${expiredShares.length} expired shares`); - } - - @Cron("0 * * * *") - async deleteExpiredRefreshTokens() { - const expiredShares = await this.prisma.refreshToken.deleteMany({ - where: {expiresAt: {lt: new Date()}}, - }); - console.log(`job: deleted ${expiredShares.count} expired refresh tokens`); - } + @Cron("0 * * * *") + async deleteExpiredRefreshTokens() { + const expiredShares = await this.prisma.refreshToken.deleteMany({ + where: { expiresAt: { lt: new Date() } }, + }); + console.log(`job: deleted ${expiredShares.count} expired refresh tokens`); + } } diff --git a/backend/src/share/guard/shareSecurity.guard.ts b/backend/src/share/guard/shareSecurity.guard.ts index b768452..028689c 100644 --- a/backend/src/share/guard/shareSecurity.guard.ts +++ b/backend/src/share/guard/shareSecurity.guard.ts @@ -34,7 +34,11 @@ export class ShareSecurityGuard implements CanActivate { include: { security: true }, }); - if (!share || (moment().isAfter(share.expiration) && moment(share.expiration).unix() !== 0)) + if ( + !share || + (moment().isAfter(share.expiration) && + moment(share.expiration).unix() !== 0) + ) throw new NotFoundException("Share not found"); if (share.security?.password && !shareToken) diff --git a/frontend/package.json b/frontend/package.json index 4042ea7..fe0793f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "build": "next build", "start": "dotenv next start", "lint": "next lint", - "format": "prettier --write \"src/**/*.ts\"" + "format": "prettier --write \"src/**/*.ts*\"" }, "dependencies": { "@emotion/react": "^11.10.4", diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index dc6eeb4..869e795 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -5,7 +5,10 @@ const Footer = () => {
- Made with 🖤 by Elias Schneider + Made with 🖤 by{" "} + + Elias Schneider +
diff --git a/frontend/src/components/navBar/ActionAvatar.tsx b/frontend/src/components/navBar/ActionAvatar.tsx index 867029c..2db5090 100644 --- a/frontend/src/components/navBar/ActionAvatar.tsx +++ b/frontend/src/components/navBar/ActionAvatar.tsx @@ -1,6 +1,6 @@ import { ActionIcon, Avatar, Menu } from "@mantine/core"; import { NextLink } from "@mantine/next"; -import { TbDoorExit, TbLink } from "react-icons/tb";; +import { TbDoorExit, TbLink } from "react-icons/tb"; import authService from "../../services/auth.service"; const ActionAvatar = () => { diff --git a/frontend/src/components/share/FileList.tsx b/frontend/src/components/share/FileList.tsx index 9cce244..8ffeef0 100644 --- a/frontend/src/components/share/FileList.tsx +++ b/frontend/src/components/share/FileList.tsx @@ -1,5 +1,5 @@ import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core"; -import { TbCircleCheck, TbDownload } from "react-icons/tb";; +import { TbCircleCheck, TbDownload } from "react-icons/tb"; import shareService from "../../services/share.service"; import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util"; diff --git a/frontend/src/components/upload/FileList.tsx b/frontend/src/components/upload/FileList.tsx index fb4c2a7..a1854e4 100644 --- a/frontend/src/components/upload/FileList.tsx +++ b/frontend/src/components/upload/FileList.tsx @@ -1,6 +1,6 @@ import { ActionIcon, Table } from "@mantine/core"; import { Dispatch, SetStateAction } from "react"; -import { TbTrash } from "react-icons/tb";; +import { TbTrash } from "react-icons/tb"; import { FileUpload } from "../../types/File.type"; import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util"; import UploadProgressIndicator from "./UploadProgressIndicator"; diff --git a/frontend/src/components/upload/showCreateUploadModal.tsx b/frontend/src/components/upload/showCreateUploadModal.tsx index 86dab8d..e345ce1 100644 --- a/frontend/src/components/upload/showCreateUploadModal.tsx +++ b/frontend/src/components/upload/showCreateUploadModal.tsx @@ -8,14 +8,12 @@ const showCreateUploadModal = ( uploadCallback: ( id: string, expiration: string, - security: ShareSecurity, + security: ShareSecurity ) => void ) => { return modals.openModal({ title: Share, - children: ( - - ), + children: , }); }; From 9d973c91bce813fd3a5dbac15d4e9715a12bf3bb Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Sun, 16 Oct 2022 00:25:32 +0200 Subject: [PATCH 7/7] refactor: move `jobs` folder into src root --- backend/src/app.module.ts | 2 +- backend/src/{auth => }/jobs/jobs.service.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename backend/src/{auth => }/jobs/jobs.service.ts (100%) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index cb32321..4b5a7fe 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { ScheduleModule } from "@nestjs/schedule"; import { AuthModule } from "./auth/auth.module"; -import { JobsService } from "./auth/jobs/jobs.service"; +import { JobsService } from "./jobs/jobs.service"; import { FileController } from "./file/file.controller"; import { FileModule } from "./file/file.module"; diff --git a/backend/src/auth/jobs/jobs.service.ts b/backend/src/jobs/jobs.service.ts similarity index 100% rename from backend/src/auth/jobs/jobs.service.ts rename to backend/src/jobs/jobs.service.ts