mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-11-04 23:10:13 +01:00
Merge branch 'main' into development
This commit is contained in:
commit
13d6f05b98
3
.gitignore
vendored
3
.gitignore
vendored
@ -37,3 +37,6 @@ yarn-error.log*
|
||||
# project specific
|
||||
/backend/data/
|
||||
/data/
|
||||
|
||||
# Jetbrains specific (webstorm)
|
||||
.idea/**/**
|
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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) {
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -1,3 +1,4 @@
|
||||
SHOW_HOME_PAGE=true
|
||||
ALLOW_REGISTRATION=true
|
||||
MAX_FILE_SIZE=1000000000
|
||||
TWELVE_HOUR_TIME=false
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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 = () => {
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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} />,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user