1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-07-02 07:20:38 +02:00

Merge pull request #15 from stautonico/main

New Feature: Granular Control of Expiration Date + 12/24 Hour Modes
This commit is contained in:
Elias Schneider 2022-10-16 10:18:51 +02:00 committed by GitHub
commit 8435eaed77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 193 additions and 55 deletions

3
.gitignore vendored
View File

@ -37,3 +37,6 @@ yarn-error.log*
# project specific
/backend/data/
/data/
# Jetbrains specific (webstorm)
.idea/**/**

View File

@ -29,7 +29,9 @@
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"rimraf": "^3.0.2"
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.7"
},
"devDependencies": {
"@nestjs/cli": "^9.1.4",

View File

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

View File

@ -2,6 +2,7 @@ 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 {
@ -13,7 +14,13 @@ export class JobsService {
@Cron("0 * * * *")
async deleteExpiredShares() {
const expiredShares = await this.prisma.share.findMany({
where: { expiration: { lt: new Date() } },
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) {

View File

@ -34,7 +34,11 @@ 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?.password && !shareToken)

View File

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

View File

@ -35,16 +35,24 @@ export class ShareService {
share.security.password = await argon.hash(share.security.password);
}
const expirationDate = moment()
.add(
share.expiration.split("-")[0],
share.expiration.split("-")[1] as moment.unitOfTime.DurationConstructor
)
.toDate();
// 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();
// Throw error if expiration date is now
if (expirationDate.setMilliseconds(0) == new Date().setMilliseconds(0))
throw new BadRequestException("Invalid expiration date");
// 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();
}
return await this.prisma.share.create({
data: {
@ -101,8 +109,12 @@ export class ShareService {
return await this.prisma.share.findMany({
where: {
creator: { id: userId },
expiration: { gt: new Date() },
uploadLocked: true,
// 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() } },
],
},
orderBy: {
expiration: "desc",
@ -186,7 +198,6 @@ export class ShareService {
const { expiration } = await this.prisma.share.findUnique({
where: { id: shareId },
});
console.log(moment(expiration).diff(new Date(), "seconds"));
return this.jwtService.sign(
{
shareId,
@ -198,10 +209,16 @@ export class ShareService {
);
}
verifyShareToken(shareId: string, token: string) {
async verifyShareToken(shareId: string, token: string) {
const { expiration } = await this.prisma.share.findUnique({
where: { id: shareId },
});
try {
const claims = this.jwtService.verify(token, {
secret: this.config.get("JWT_SECRET"),
// Ignore expiration if expiration is 0
ignoreExpiration: moment(expiration).isSame(0),
});
return claims.shareId == shareId;

View File

@ -1,3 +1,4 @@
SHOW_HOME_PAGE=true
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=1000000000
TWELVE_HOUR_TIME=false

View File

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

View File

@ -6,9 +6,11 @@
"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",
"@emotion/server": "^11.10.0",
"@mantine/core": "^5.5.2",
"@mantine/dropzone": "^5.5.2",
"@mantine/form": "^5.5.2",

View File

@ -5,7 +5,10 @@ const Footer = () => {
<MFooter height="auto" p={10}>
<Center>
<Text size="xs" color="dimmed">
Made with 🖤 by <Anchor size="xs" href="https://eliasschneider.com" target="_blank">Elias Schneider</Anchor>
Made with 🖤 by{" "}
<Anchor size="xs" href="https://eliasschneider.com" target="_blank">
Elias Schneider
</Anchor>
</Text>
</Center>
</MFooter>

View File

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

View File

@ -2,6 +2,7 @@ import {
Accordion,
Button,
Col,
Checkbox,
Grid,
NumberInput,
PasswordInput,
@ -15,6 +16,33 @@ import { useModals } from "@mantine/modals";
import * as yup from "yup";
import shareService from "../../services/share.service";
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 +72,9 @@ const CreateUploadModalBody = ({
password: undefined,
maxViews: undefined,
expiration: "1-day",
expiration_num: 1,
expiration_unit: "-days",
never_expires: false,
},
validate: yupResolver(validationSchema),
});
@ -55,7 +85,10 @@ 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,
});
@ -91,6 +124,7 @@ const CreateUploadModalBody = ({
</Grid>
<Text
italic
size="xs"
sx={(theme) => ({
color: theme.colors.gray[6],
@ -99,20 +133,70 @@ const CreateUploadModalBody = ({
{window.location.origin}/share/
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
</Text>
<Select
label="Expiration"
{...form.getInputProps("expiration")}
data={[
{
value: "10-minutes",
label: "10 Minutes",
},
{ value: "1-hour", label: "1 Hour" },
{ value: "1-day", label: "1 Day" },
{ value: "1-week".toString(), label: "1 Week" },
{ value: "1-month", label: "1 Month" },
]}
<Grid align={form.errors.link ? "center" : "flex-end"}>
<Col xs={6}>
<NumberInput
min={1}
max={99999}
precision={0}
variant="filled"
label="Expiration"
placeholder="n"
disabled={form.values.never_expires}
{...form.getInputProps("expiration_num")}
/>
</Col>
<Col xs={6}>
<Select
disabled={form.values.never_expires}
{...form.getInputProps("expiration_unit")}
data={[
// Set the label to singular if the number is 1, else plural
{
value: "-minutes",
label:
"Minute" + (form.values.expiration_num == 1 ? "" : "s"),
},
{
value: "-hours",
label: "Hour" + (form.values.expiration_num == 1 ? "" : "s"),
},
{
value: "-days",
label: "Day" + (form.values.expiration_num == 1 ? "" : "s"),
},
{
value: "-weeks",
label: "Week" + (form.values.expiration_num == 1 ? "" : "s"),
},
{
value: "-months",
label: "Month" + (form.values.expiration_num == 1 ? "" : "s"),
},
{
value: "-years",
label: "Year" + (form.values.expiration_num == 1 ? "" : "s"),
},
]}
/>
</Col>
</Grid>
<Checkbox
label="Never Expires"
{...form.getInputProps("never_expires")}
/>
{/* Preview expiration date text */}
<Text
italic
size="xs"
sx={(theme) => ({
color: theme.colors.gray[6],
})}
>
{PreviewExpiration({ form })}
</Text>
<Accordion>
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
<Accordion.Control>Security options</Accordion.Control>

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Button, Center, createStyles, Group, Text } from "@mantine/core";
import { Dropzone as MantineDropzone } from "@mantine/dropzone";
import getConfig from "next/config";
import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
import { TbCloudUpload, TbUpload } from "react-icons/tb";;;
import { TbCloudUpload, TbUpload } from "react-icons/tb";
import { FileUpload } from "../../types/File.type";
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
import toast from "../../utils/toast.util";

View File

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

View File

@ -10,10 +10,14 @@ import { useClipboard } from "@mantine/hooks";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import moment from "moment";
import getConfig from "next/config";
import { useRouter } from "next/router";
import { TbCopy } from "react-icons/tb";
import { Share } from "../../types/share.type";
import toast from "../../utils/toast.util";
const { publicRuntimeConfig } = getConfig();
const showCompletedUploadModal = (modals: ModalsContextProps, share: Share) => {
return modals.openModal({
closeOnClickOutside: false,
@ -55,7 +59,14 @@ const Body = ({ share }: { share: Share }) => {
color: theme.colors.gray[6],
})}
>
Your share expires at {moment(share.expiration).format("LLL")}
{/* 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 ${
publicRuntimeConfig.TWELVE_HOUR_TIME === "true"
? moment(share.expiration).format("MMMM Do YYYY, h:mm a")
: moment(share.expiration).format("MMMM DD YYYY, HH:mm")
}`}
</Text>
<Button

View File

@ -8,14 +8,12 @@ const showCreateUploadModal = (
uploadCallback: (
id: string,
expiration: string,
security: ShareSecurity,
security: ShareSecurity
) => void
) => {
return modals.openModal({
title: <Title order={4}>Share</Title>,
children: (
<CreateUploadModalBody uploadCallback={uploadCallback} />
),
children: <CreateUploadModalBody uploadCallback={uploadCallback} />,
});
};

View File

@ -16,12 +16,15 @@ import { NextLink } from "@mantine/next";
import moment from "moment";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { TbLink, TbTrash } from "react-icons/tb";;
import { TbLink, TbTrash } from "react-icons/tb";
import Meta from "../../components/Meta";
import useUser from "../../hooks/user.hook";
import shareService from "../../services/share.service";
import { MyShare } from "../../types/share.type";
import toast from "../../utils/toast.util";
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
const MyShares = () => {
const modals = useModals();
@ -72,7 +75,11 @@ const MyShares = () => {
<td>{share.id}</td>
<td>{share.views}</td>
<td>
{moment(share.expiration).format("MMMM DD YYYY, HH:mm")}
{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")}
</td>
<td>
<Group position="right">