mirror of
https://github.com/stonith404/pingvin-share.git
synced 2025-01-29 01:28:59 -05:00
Merge pull request #18 from stonith404/feat/allow-unauthenticated-shares
feat: allow unauthenticated shares
This commit is contained in:
commit
e4019612f8
18 changed files with 366 additions and 256 deletions
|
@ -4,6 +4,7 @@
|
||||||
APP_URL=http://localhost:3000
|
APP_URL=http://localhost:3000
|
||||||
SHOW_HOME_PAGE=true
|
SHOW_HOME_PAGE=true
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES=false
|
||||||
MAX_FILE_SIZE=1000000000
|
MAX_FILE_SIZE=1000000000
|
||||||
|
|
||||||
# SECURITY
|
# SECURITY
|
||||||
|
|
15
README.md
15
README.md
|
@ -28,13 +28,14 @@ The website is now listening available on `http://localhost:3000`, have fun with
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Variable | Description | Possible values |
|
| Variable | Description | Possible values |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------- | --------------- |
|
| ------------------------------ | ------------------------------------------------------------------------------------------- | --------------- |
|
||||||
| `APP_URL` | On which URL Pingvin Share is available. E.g http://localhost or https://pingvin-share.com. | URL |
|
| `APP_URL` | On which URL Pingvin Share is available. E.g http://localhost or https://pingvin-share.com. | URL |
|
||||||
| `SHOW_HOME_PAGE` | Whether the Pingvin Share home page should be shown. | true/false |
|
| `SHOW_HOME_PAGE` | Whether the Pingvin Share home page should be shown. | true/false |
|
||||||
| `ALLOW_REGISTRATION` | Whether a new user can create a new account. | true/false |
|
| `ALLOW_REGISTRATION` | Whether a new user can create a new account. | true/false |
|
||||||
| `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
|
| `ALLOW_UNAUTHENTICATED_SHARES` | Whether a user can create a share without being signed in. | true/false |
|
||||||
| `JWT_SECRET` | Long random string to sign the JWT's. | Random string |
|
| `MAX_FILE_SIZE` | Maximum allowed size per file in bytes. | Number |
|
||||||
|
| `JWT_SECRET` | Long random string to sign the JWT's. | Random string |
|
||||||
|
|
||||||
### Upgrade to a new version
|
### Upgrade to a new version
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
APP_URL=http://localhost:3000
|
APP_URL=http://localhost:3000
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
MAX_FILE_SIZE=5000000000
|
MAX_FILE_SIZE=5000000000
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES=false
|
||||||
|
|
||||||
# SECURITY
|
# SECURITY
|
||||||
JWT_SECRET=random-string
|
JWT_SECRET=random-string
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Share" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"uploadLocked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isZipReady" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"views" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"expiration" DATETIME NOT NULL,
|
||||||
|
"creatorId" TEXT,
|
||||||
|
CONSTRAINT "Share_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Share" ("createdAt", "creatorId", "expiration", "id", "isZipReady", "uploadLocked", "views") SELECT "createdAt", "creatorId", "expiration", "id", "isZipReady", "uploadLocked", "views" FROM "Share";
|
||||||
|
DROP TABLE "Share";
|
||||||
|
ALTER TABLE "new_Share" RENAME TO "Share";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -40,8 +40,8 @@ model Share {
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
expiration DateTime
|
expiration DateTime
|
||||||
|
|
||||||
creatorId String
|
creatorId String?
|
||||||
creator User @relation(fields: [creatorId], references: [id])
|
creator User? @relation(fields: [creatorId], references: [id])
|
||||||
security ShareSecurity?
|
security ShareSecurity?
|
||||||
files File[]
|
files File[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
import { AuthGuard } from "@nestjs/passport";
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
export class JwtGuard extends AuthGuard("jwt") {
|
export class JwtGuard extends AuthGuard("jwt") {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
return process.env.ALLOW_UNAUTHENTICATED_SHARES == "true"
|
||||||
|
? true
|
||||||
|
: super.canActivate(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ export class ShareOwnerGuard implements CanActivate {
|
||||||
|
|
||||||
if (!share) throw new NotFoundException("Share not found");
|
if (!share) throw new NotFoundException("Share not found");
|
||||||
|
|
||||||
|
if (!share.creatorId) return true;
|
||||||
|
|
||||||
return share.creatorId == (request.user as User).id;
|
return share.creatorId == (request.user as User).id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class ShareService {
|
||||||
private jwtService: JwtService
|
private jwtService: JwtService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(share: CreateShareDTO, user: User) {
|
async create(share: CreateShareDTO, user?: User) {
|
||||||
if (!(await this.isShareIdAvailable(share.id)).isAvailable)
|
if (!(await this.isShareIdAvailable(share.id)).isAvailable)
|
||||||
throw new BadRequestException("Share id already in use");
|
throw new BadRequestException("Share id already in use");
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export class ShareService {
|
||||||
data: {
|
data: {
|
||||||
...share,
|
...share,
|
||||||
expiration: expirationDate,
|
expiration: expirationDate,
|
||||||
creator: { connect: { id: user.id } },
|
creator: { connect: user ? { id: user.id } : undefined },
|
||||||
security: { create: share.security },
|
security: { create: share.security },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -154,6 +154,8 @@ export class ShareService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!share) throw new NotFoundException("Share not found");
|
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.fileService.deleteAllFiles(shareId);
|
||||||
await this.prisma.share.delete({ where: { id: shareId } });
|
await this.prisma.share.delete({ where: { id: shareId } });
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
SHOW_HOME_PAGE=true
|
SHOW_HOME_PAGE=true
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
MAX_FILE_SIZE=1000000000
|
MAX_FILE_SIZE=1000000000
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES=false
|
|
@ -5,6 +5,7 @@ const nextConfig = {
|
||||||
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
|
ALLOW_REGISTRATION: process.env.ALLOW_REGISTRATION,
|
||||||
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
|
SHOW_HOME_PAGE: process.env.SHOW_HOME_PAGE,
|
||||||
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
|
MAX_FILE_SIZE: process.env.MAX_FILE_SIZE,
|
||||||
|
ALLOW_UNAUTHENTICATED_SHARES: process.env.ALLOW_UNAUTHENTICATED_SHARES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
|
||||||
<form
|
|
||||||
onSubmit={form.onSubmit(async (values) => {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Stack align="stretch">
|
|
||||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
|
||||||
<Col xs={9}>
|
|
||||||
<TextInput
|
|
||||||
variant="filled"
|
|
||||||
label="Link"
|
|
||||||
placeholder="myAwesomeShare"
|
|
||||||
{...form.getInputProps("link")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={3}>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
form.setFieldValue(
|
|
||||||
"link",
|
|
||||||
Buffer.from(Math.random().toString(), "utf8")
|
|
||||||
.toString("base64")
|
|
||||||
.substr(10, 7)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Generate
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
italic
|
|
||||||
size="xs"
|
|
||||||
sx={(theme) => ({
|
|
||||||
color: theme.colors.gray[6],
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{window.location.origin}/share/
|
|
||||||
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
|
||||||
</Text>
|
|
||||||
<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>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Stack align="stretch">
|
|
||||||
<PasswordInput
|
|
||||||
variant="filled"
|
|
||||||
placeholder="No password"
|
|
||||||
label="Password protection"
|
|
||||||
{...form.getInputProps("password")}
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
min={1}
|
|
||||||
type="number"
|
|
||||||
variant="filled"
|
|
||||||
placeholder="No limit"
|
|
||||||
label="Maximal views"
|
|
||||||
{...form.getInputProps("maxViews")}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion>
|
|
||||||
<Button type="submit">Share</Button>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateUploadModalBody;
|
|
19
frontend/src/components/upload/ExpirationPreview.tsx
Normal file
19
frontend/src/components/upload/ExpirationPreview.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
const ExpirationPreview = ({ 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")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpirationPreview;
|
|
@ -13,8 +13,8 @@ import moment from "moment";
|
||||||
import getConfig from "next/config";
|
import getConfig from "next/config";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TbCopy } from "react-icons/tb";
|
import { TbCopy } from "react-icons/tb";
|
||||||
import { Share } from "../../types/share.type";
|
import { Share } from "../../../types/share.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
249
frontend/src/components/upload/modals/showCreateUploadModal.tsx
Normal file
249
frontend/src/components/upload/modals/showCreateUploadModal.tsx
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Col,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
NumberInput,
|
||||||
|
PasswordInput,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
|
import { useModals } from "@mantine/modals";
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import getConfig from "next/config";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TbAlertCircle } from "react-icons/tb";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import shareService from "../../../services/share.service";
|
||||||
|
import { ShareSecurity } from "../../../types/share.type";
|
||||||
|
import ExpirationPreview from "../ExpirationPreview";
|
||||||
|
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
|
const showCreateUploadModal = (
|
||||||
|
modals: ModalsContextProps,
|
||||||
|
uploadCallback: (
|
||||||
|
id: string,
|
||||||
|
expiration: string,
|
||||||
|
security: ShareSecurity
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
return modals.openModal({
|
||||||
|
title: <Title order={4}>Share</Title>,
|
||||||
|
children: <CreateUploadModalBody uploadCallback={uploadCallback} />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateUploadModalBody = ({
|
||||||
|
uploadCallback,
|
||||||
|
}: {
|
||||||
|
uploadCallback: (
|
||||||
|
id: string,
|
||||||
|
expiration: string,
|
||||||
|
security: ShareSecurity
|
||||||
|
) => void;
|
||||||
|
}) => {
|
||||||
|
const modals = useModals();
|
||||||
|
|
||||||
|
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(
|
||||||
|
publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true"
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Group>
|
||||||
|
{showNotSignedInAlert && (
|
||||||
|
<Alert
|
||||||
|
withCloseButton
|
||||||
|
onClose={() => setShowNotSignedInAlert(false)}
|
||||||
|
icon={<TbAlertCircle size={16} />}
|
||||||
|
title="You're not signed in"
|
||||||
|
color="yellow"
|
||||||
|
>
|
||||||
|
You will be unable to delete your share manually and view the visitor
|
||||||
|
count.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack align="stretch">
|
||||||
|
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||||
|
<Col xs={9}>
|
||||||
|
<TextInput
|
||||||
|
variant="filled"
|
||||||
|
label="Link"
|
||||||
|
placeholder="myAwesomeShare"
|
||||||
|
{...form.getInputProps("link")}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={3}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
form.setFieldValue(
|
||||||
|
"link",
|
||||||
|
Buffer.from(Math.random().toString(), "utf8")
|
||||||
|
.toString("base64")
|
||||||
|
.substr(10, 7)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
italic
|
||||||
|
size="xs"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.colors.gray[6],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{window.location.origin}/share/
|
||||||
|
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
||||||
|
</Text>
|
||||||
|
<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],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{ExpirationPreview({ form })}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Accordion>
|
||||||
|
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
||||||
|
<Accordion.Control>Security options</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Stack align="stretch">
|
||||||
|
<PasswordInput
|
||||||
|
variant="filled"
|
||||||
|
placeholder="No password"
|
||||||
|
label="Password protection"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
min={1}
|
||||||
|
type="number"
|
||||||
|
variant="filled"
|
||||||
|
placeholder="No limit"
|
||||||
|
label="Maximal views"
|
||||||
|
{...form.getInputProps("maxViews")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
<Button type="submit">Share</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showCreateUploadModal;
|
|
@ -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: <Body />,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Body = () => {
|
||||||
|
const modals = useModals();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack align="stretch">
|
||||||
|
<Alert
|
||||||
|
icon={<TbAlertCircle size={16} />}
|
||||||
|
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.
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showNotAuthenticatedWarningModal;
|
|
@ -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: <Title order={4}>Share</Title>,
|
|
||||||
children: <CreateUploadModalBody uploadCallback={uploadCallback} />,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default showCreateUploadModal;
|
|
|
@ -15,6 +15,7 @@ import { useRouter } from "next/router";
|
||||||
import { TbCheck } from "react-icons/tb";
|
import { TbCheck } from "react-icons/tb";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig();
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
|
@ -74,7 +75,7 @@ export default function Home() {
|
||||||
|
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
if (user) {
|
if (user || publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "true") {
|
||||||
router.replace("/upload");
|
router.replace("/upload");
|
||||||
} else if (publicRuntimeConfig.SHOW_HOME_PAGE == "false") {
|
} else if (publicRuntimeConfig.SHOW_HOME_PAGE == "false") {
|
||||||
router.replace("/auth/signIn");
|
router.replace("/auth/signIn");
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import { Button, Group } from "@mantine/core";
|
import { Button, Group } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import getConfig from "next/config";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Meta from "../components/Meta";
|
import Meta from "../components/Meta";
|
||||||
import Dropzone from "../components/upload/Dropzone";
|
import Dropzone from "../components/upload/Dropzone";
|
||||||
import FileList from "../components/upload/FileList";
|
import FileList from "../components/upload/FileList";
|
||||||
import showCompletedUploadModal from "../components/upload/showCompletedUploadModal";
|
import showCompletedUploadModal from "../components/upload/modals/showCompletedUploadModal";
|
||||||
import showCreateUploadModal from "../components/upload/showCreateUploadModal";
|
import showCreateUploadModal from "../components/upload/modals/showCreateUploadModal";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
import shareService from "../services/share.service";
|
import shareService from "../services/share.service";
|
||||||
import { FileUpload } from "../types/File.type";
|
import { FileUpload } from "../types/File.type";
|
||||||
import { ShareSecurity } from "../types/share.type";
|
import { ShareSecurity } from "../types/share.type";
|
||||||
import toast from "../utils/toast.util";
|
import toast from "../utils/toast.util";
|
||||||
|
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
const Upload = () => {
|
const Upload = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
@ -86,7 +89,7 @@ const Upload = () => {
|
||||||
setisUploading(false);
|
setisUploading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (!user) {
|
if (!user && publicRuntimeConfig.ALLOW_UNAUTHENTICATED_SHARES == "false") {
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Add table
Reference in a new issue