From 84d29dff68d0ea9d76d9a35f9fb7dff95d3dda1b Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 18 Oct 2022 14:27:14 +0200 Subject: [PATCH] feat: allow unauthenticated uploads --- .env.example | 1 + backend/.env.example | 1 + backend/prisma/schema.prisma | 4 +- backend/src/auth/guard/jwt.guard.ts | 7 + backend/src/auth/strategy/jwt.strategy.ts | 1 + backend/src/share/guard/shareOwner.guard.ts | 2 + backend/src/share/share.service.ts | 6 +- frontend/.env.example | 1 + frontend/next.config.js | 1 + .../share/CreateUploadModalBody.tsx | 219 --------------- .../components/upload/ExpirationPreview.tsx | 19 ++ .../{ => modals}/showCompletedUploadModal.tsx | 4 +- .../upload/modals/showCreateUploadModal.tsx | 249 ++++++++++++++++++ .../showNotAuthenticatedWarningModal.tsx | 42 +++ .../upload/showCreateUploadModal.tsx | 20 -- frontend/src/pages/index.tsx | 3 +- frontend/src/pages/upload.tsx | 9 +- 17 files changed, 340 insertions(+), 249 deletions(-) delete mode 100644 frontend/src/components/share/CreateUploadModalBody.tsx create mode 100644 frontend/src/components/upload/ExpirationPreview.tsx rename frontend/src/components/upload/{ => modals}/showCompletedUploadModal.tsx (95%) create mode 100644 frontend/src/components/upload/modals/showCreateUploadModal.tsx create mode 100644 frontend/src/components/upload/modals/showNotAuthenticatedWarningModal.tsx delete mode 100644 frontend/src/components/upload/showCreateUploadModal.tsx diff --git a/.env.example b/.env.example index 46b8886..e57308a 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ APP_URL=http://localhost:3000 SHOW_HOME_PAGE=true ALLOW_REGISTRATION=true MAX_FILE_SIZE=1000000000 +ALLOW_UNAUTHENTICATED_SHARES=false # SECURITY JWT_SECRET=long-random-string diff --git a/backend/.env.example b/backend/.env.example index f170e44..4b2229f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,6 +2,7 @@ APP_URL=http://localhost:3000 ALLOW_REGISTRATION=true MAX_FILE_SIZE=5000000000 +ALLOW_UNAUTHENTICATED_SHARES=false # SECURITY JWT_SECRET=random-string diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 883832b..3677059 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -40,8 +40,8 @@ model Share { views Int @default(0) expiration DateTime - creatorId String - creator User @relation(fields: [creatorId], references: [id]) + creatorId String? + creator User? @relation(fields: [creatorId], references: [id]) security ShareSecurity? files File[] } diff --git a/backend/src/auth/guard/jwt.guard.ts b/backend/src/auth/guard/jwt.guard.ts index bbcc8be..fea4ec2 100644 --- a/backend/src/auth/guard/jwt.guard.ts +++ b/backend/src/auth/guard/jwt.guard.ts @@ -1,7 +1,14 @@ +import { ExecutionContext } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; +import { Observable } from "rxjs"; export class JwtGuard extends AuthGuard("jwt") { constructor() { super(); } + canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { + return process.env.ALLOW_UNAUTHENTICATED_SHARES == "true" ? true : super.canActivate(context); + } } diff --git a/backend/src/auth/strategy/jwt.strategy.ts b/backend/src/auth/strategy/jwt.strategy.ts index 4401420..9cf9d55 100644 --- a/backend/src/auth/strategy/jwt.strategy.ts +++ b/backend/src/auth/strategy/jwt.strategy.ts @@ -11,6 +11,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: config.get("JWT_SECRET"), + }); } diff --git a/backend/src/share/guard/shareOwner.guard.ts b/backend/src/share/guard/shareOwner.guard.ts index 7b99736..03d0cce 100644 --- a/backend/src/share/guard/shareOwner.guard.ts +++ b/backend/src/share/guard/shareOwner.guard.ts @@ -28,6 +28,8 @@ export class ShareOwnerGuard implements CanActivate { if (!share) throw new NotFoundException("Share not found"); + if(!share.creatorId) return true; + return share.creatorId == (request.user as User).id; } } diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index 8343233..9a13630 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -24,7 +24,7 @@ export class ShareService { private jwtService: JwtService ) {} - async create(share: CreateShareDTO, user: User) { + async create(share: CreateShareDTO, user?: User) { if (!(await this.isShareIdAvailable(share.id)).isAvailable) throw new BadRequestException("Share id already in use"); @@ -58,7 +58,7 @@ export class ShareService { data: { ...share, expiration: expirationDate, - creator: { connect: { id: user.id } }, + creator: { connect: user ? { id: user.id } : undefined }, security: { create: share.security }, }, }); @@ -154,6 +154,8 @@ export class ShareService { }); if (!share) throw new NotFoundException("Share not found"); + if (!share.creatorId) + throw new ForbiddenException("Anonymous shares can't be deleted"); await this.fileService.deleteAllFiles(shareId); await this.prisma.share.delete({ where: { id: shareId } }); diff --git a/frontend/.env.example b/frontend/.env.example index dbda902..65ef8ee 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,3 +1,4 @@ SHOW_HOME_PAGE=true ALLOW_REGISTRATION=true MAX_FILE_SIZE=1000000000 +ALLOW_UNAUTHENTICATED_SHARES=false \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js index 16030c9..9940531 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -5,6 +5,7 @@ const nextConfig = { ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION, SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE, MAX_FILE_SIZE: process.env.MAX_FILE_SIZE, + ALLOW_UNAUTHENTICATED_SHARES: process.env.ALLOW_UNAUTHENTICATED_SHARES } } diff --git a/frontend/src/components/share/CreateUploadModalBody.tsx b/frontend/src/components/share/CreateUploadModalBody.tsx deleted file mode 100644 index f4dd7d4..0000000 --- a/frontend/src/components/share/CreateUploadModalBody.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { - Accordion, - Button, - Checkbox, - Col, - Grid, - NumberInput, - PasswordInput, - Select, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useForm, yupResolver } from "@mantine/form"; -import { useModals } from "@mantine/modals"; -import moment from "moment"; -import * as yup from "yup"; -import shareService from "../../services/share.service"; -import { ShareSecurity } from "../../types/share.type"; - -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(); - - return `This share will expire on ${moment(expirationDate).format("LLL")}`; -}; - -const CreateUploadModalBody = ({ - uploadCallback, -}: { - uploadCallback: ( - id: string, - expiration: string, - security: ShareSecurity - ) => void; -}) => { - const modals = useModals(); - const validationSchema = yup.object().shape({ - link: yup - .string() - .required() - .min(3) - .max(50) - .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_num: 1, - expiration_unit: "-days", - never_expires: false, - }, - validate: yupResolver(validationSchema), - }); - - return ( -
{ - if (!(await shareService.isShareIdAvailable(values.link))) { - form.setFieldError("link", "This link is already in use"); - } else { - 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, - }); - modals.closeAll(); - } - })} - > - - - - - - - - - - - ({ - color: theme.colors.gray[6], - })} - > - {window.location.origin}/share/ - {form.values.link == "" ? "myAwesomeShare" : form.values.link} - - - - - - - + + + + + {/* Preview expiration date text */} + ({ + color: theme.colors.gray[6], + })} + > + {ExpirationPreview({ form })} + + + + + Security options + + + + + + + + + + +
+ + ); +}; + +export default showCreateUploadModal; diff --git a/frontend/src/components/upload/modals/showNotAuthenticatedWarningModal.tsx b/frontend/src/components/upload/modals/showNotAuthenticatedWarningModal.tsx new file mode 100644 index 0000000..bb50378 --- /dev/null +++ b/frontend/src/components/upload/modals/showNotAuthenticatedWarningModal.tsx @@ -0,0 +1,42 @@ +import { Alert, Button, Stack } from "@mantine/core"; +import { useModals } from "@mantine/modals"; +import { ModalsContextProps } from "@mantine/modals/lib/context"; +import { useRouter } from "next/router"; +import { TbAlertCircle } from "react-icons/tb"; + +const showNotAuthenticatedWarningModal = ( + modals: ModalsContextProps, + onConfirm: (...any: any) => any +) => { + return modals.openConfirmModal({ + closeOnClickOutside: false, + withCloseButton: false, + closeOnEscape: false, + labels: { confirm: "Continue", cancel: "Sign in" }, + onConfirm: onConfirm, + onCancel: () => {}, + + children: , + }); +}; + +const Body = () => { + const modals = useModals(); + const router = useRouter(); + return ( + <> + + } + title="You're not signed in" + color="yellow" + > + You will be unable to delete your share manually and view the visitor + count if you're not signed in. + + + + ); +}; + +export default showNotAuthenticatedWarningModal; diff --git a/frontend/src/components/upload/showCreateUploadModal.tsx b/frontend/src/components/upload/showCreateUploadModal.tsx deleted file mode 100644 index e345ce1..0000000 --- a/frontend/src/components/upload/showCreateUploadModal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Title } from "@mantine/core"; -import { ModalsContextProps } from "@mantine/modals/lib/context"; -import { ShareSecurity } from "../../types/share.type"; -import CreateUploadModalBody from "../share/CreateUploadModalBody"; - -const showCreateUploadModal = ( - modals: ModalsContextProps, - uploadCallback: ( - id: string, - expiration: string, - security: ShareSecurity - ) => void -) => { - return modals.openModal({ - title: Share, - children: , - }); -}; - -export default showCreateUploadModal; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index cfe7c55..0ffc666 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -15,6 +15,7 @@ import { useRouter } from "next/router"; import { TbCheck } from "react-icons/tb"; import Meta from "../components/Meta"; import useUser from "../hooks/user.hook"; + const { publicRuntimeConfig } = getConfig(); const useStyles = createStyles((theme) => ({ @@ -74,7 +75,7 @@ export default function Home() { const { classes } = useStyles(); const router = useRouter(); - if (user) { + if (user || publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true") { router.replace("/upload"); } else if (publicRuntimeConfig.SHOW_HOME_PAGE == "false") { router.replace("/auth/signIn"); diff --git a/frontend/src/pages/upload.tsx b/frontend/src/pages/upload.tsx index fdec657..5e5993b 100644 --- a/frontend/src/pages/upload.tsx +++ b/frontend/src/pages/upload.tsx @@ -1,19 +1,22 @@ import { Button, Group } from "@mantine/core"; import { useModals } from "@mantine/modals"; import axios from "axios"; +import getConfig from "next/config"; import { useRouter } from "next/router"; import { useState } from "react"; import Meta from "../components/Meta"; import Dropzone from "../components/upload/Dropzone"; import FileList from "../components/upload/FileList"; -import showCompletedUploadModal from "../components/upload/showCompletedUploadModal"; -import showCreateUploadModal from "../components/upload/showCreateUploadModal"; +import showCompletedUploadModal from "../components/upload/modals/showCompletedUploadModal"; +import showCreateUploadModal from "../components/upload/modals/showCreateUploadModal"; import useUser from "../hooks/user.hook"; import shareService from "../services/share.service"; import { FileUpload } from "../types/File.type"; import { ShareSecurity } from "../types/share.type"; import toast from "../utils/toast.util"; +const { publicRuntimeConfig } = getConfig(); + const Upload = () => { const router = useRouter(); const modals = useModals(); @@ -86,7 +89,7 @@ const Upload = () => { setisUploading(false); } }; - if (!user) { + if (!user && publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "false") { router.replace("/"); } else { return (