0
Fork 0
mirror of https://github.com/stonith404/pingvin-share.git synced 2025-02-19 01:55:48 -05:00

refactor: handle authentication state in middleware

This commit is contained in:
Elias Schneider 2023-02-04 18:12:49 +01:00
parent 064ef38d78
commit 4e840ecd29
No known key found for this signature in database
GPG key ID: 07E623B294202B6C
17 changed files with 511 additions and 474 deletions

View file

@ -120,7 +120,7 @@ export class AuthController {
const accessToken = await this.authService.refreshAccessToken( const accessToken = await this.authService.refreshAccessToken(
request.cookies.refresh_token request.cookies.refresh_token
); );
response.cookie("access_token", accessToken); response = this.addTokensToResponse(response, undefined, accessToken);
return new TokenDTO().from({ accessToken }); return new TokenDTO().from({ accessToken });
} }
@ -162,11 +162,13 @@ export class AuthController {
refreshToken?: string, refreshToken?: string,
accessToken?: string accessToken?: string
) { ) {
if (accessToken) response.cookie("access_token", accessToken); if (accessToken)
response.cookie("access_token", accessToken, { sameSite: "lax" });
if (refreshToken) if (refreshToken)
response.cookie("refresh_token", refreshToken, { response.cookie("refresh_token", refreshToken, {
path: "/api/auth/token", path: "/api/auth/token",
httpOnly: true, httpOnly: true,
sameSite: "strict",
maxAge: 1000 * 60 * 60 * 24 * 30 * 3, maxAge: 1000 * 60 * 60 * 24 * 30 * 3,
}); });

View file

@ -110,26 +110,30 @@ export class AuthService {
{ {
sub: user.id, sub: user.id,
email: user.email, email: user.email,
isAdmin: user.isAdmin,
refreshTokenId, refreshTokenId,
}, },
{ {
expiresIn: "15min", expiresIn: "10s",
secret: this.config.get("JWT_SECRET"), secret: this.config.get("JWT_SECRET"),
} }
); );
} }
async signOut(accessToken: string) { async signOut(accessToken: string) {
const { refreshTokenId } = this.jwtService.decode(accessToken) as { const { refreshTokenId } =
refreshTokenId: string; (this.jwtService.decode(accessToken) as {
}; refreshTokenId: string;
}) || {};
await this.prisma.refreshToken if (refreshTokenId) {
.delete({ where: { id: refreshTokenId } }) await this.prisma.refreshToken
.catch((e) => { .delete({ where: { id: refreshTokenId } })
// Ignore error if refresh token doesn't exist .catch((e) => {
if (e.code != "P2025") throw e; // Ignore error if refresh token doesn't exist
}); if (e.code != "P2025") throw e;
});
}
} }
async refreshAccessToken(refreshToken: string) { async refreshAccessToken(refreshToken: string) {

View file

@ -1,4 +1,5 @@
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common"; import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
import { SkipThrottle } from "@nestjs/throttler";
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard"; import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
import { JwtGuard } from "src/auth/guard/jwt.guard"; import { JwtGuard } from "src/auth/guard/jwt.guard";
import { EmailService } from "src/email/email.service"; import { EmailService } from "src/email/email.service";
@ -16,6 +17,7 @@ export class ConfigController {
) {} ) {}
@Get() @Get()
@SkipThrottle()
async list() { async list() {
return new ConfigDTO().fromList(await this.configService.list()); return new ConfigDTO().fromList(await this.configService.list());
} }

View file

@ -21,6 +21,7 @@
"cookies-next": "^2.1.1", "cookies-next": "^2.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jose": "^4.11.2", "jose": "^4.11.2",
"jwt-decode": "^3.1.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.1.2", "next": "^13.1.2",
@ -5610,6 +5611,11 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"node_modules/klona": { "node_modules/klona": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
@ -12122,6 +12128,11 @@
"object.assign": "^4.1.2" "object.assign": "^4.1.2"
} }
}, },
"jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"klona": { "klona": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",

View file

@ -22,6 +22,7 @@
"cookies-next": "^2.1.1", "cookies-next": "^2.1.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jose": "^4.11.2", "jose": "^4.11.2",
"jwt-decode": "^3.1.2",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.1.2", "next": "^13.1.2",

View file

@ -14,7 +14,6 @@ import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context"; import { ModalsContextProps } from "@mantine/modals/lib/context";
import * as yup from "yup"; import * as yup from "yup";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service"; import authService from "../../services/auth.service";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";

View file

@ -9,6 +9,7 @@ import {
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useConfig from "../../../hooks/config.hook"; import useConfig from "../../../hooks/config.hook";
import configService from "../../../services/config.service"; import configService from "../../../services/config.service";
@ -27,6 +28,7 @@ import TestEmailButton from "./TestEmailButton";
const AdminConfigTable = () => { const AdminConfigTable = () => {
const config = useConfig(); const config = useConfig();
const router = useRouter();
const isMobile = useMediaQuery("(max-width: 560px)"); const isMobile = useMediaQuery("(max-width: 560px)");
const [updatedConfigVariables, setUpdatedConfigVariables] = useState< const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
@ -68,7 +70,7 @@ const AdminConfigTable = () => {
.updateMany(updatedConfigVariables) .updateMany(updatedConfigVariables)
.then(async () => { .then(async () => {
await configService.finishSetup(); await configService.finishSetup();
window.location.reload(); router.replace("/upload");
}) })
.catch(toast.axiosError); .catch(toast.axiosError);
} else { } else {

111
frontend/src/middleware.ts Normal file
View file

@ -0,0 +1,111 @@
import jwtDecode from "jwt-decode";
import { NextRequest, NextResponse } from "next/server";
import configService from "./services/config.service";
// This middleware redirects based on different conditions:
// - Authentication state
// - Setup status
// - Admin privileges
export const config = {
matcher: "/((?!api|static|.*\\..*|_next).*)",
};
export async function middleware(request: NextRequest) {
// Get config from backend
const config = await (
await fetch("http://localhost:8080/api/configs")
).json();
const getConfig = (key: string) => {
return configService.get(key, config);
};
const containsRoute = (routes: string[], url: string) => {
for (const route of routes) {
if (new RegExp("^" + route.replace(/\*/g, ".*") + "$").test(url))
return true;
}
return false;
};
const route = request.nextUrl.pathname;
let user: { isAdmin: boolean } | null = null;
const accessToken = request.cookies.get("access_token")?.value;
try {
const claims = jwtDecode<{ exp: number; isAdmin: boolean }>(
accessToken as string
);
if (claims.exp * 1000 > Date.now()) {
user = claims;
}
} catch {
user = null;
}
const unauthenticatedRoutes = ["/auth/signIn", "/"];
let publicRoutes = ["/share/*", "/upload/*"];
const setupStatusRegisteredRoutes = ["/auth/*", "/admin/setup"];
const adminRoutes = ["/admin/*"];
const accountRoutes = ["/account/*"];
if (getConfig("ALLOW_REGISTRATION")) {
unauthenticatedRoutes.push("/auth/signUp");
}
if (getConfig("ALLOW_UNAUTHENTICATED_SHARES")) {
publicRoutes = ["*"];
}
const isPublicRoute = containsRoute(publicRoutes, route);
const isUnauthenticatedRoute = containsRoute(unauthenticatedRoutes, route);
const isAdminRoute = containsRoute(adminRoutes, route);
const isAccountRoute = containsRoute(accountRoutes, route);
const isSetupStatusRegisteredRoute = containsRoute(
setupStatusRegisteredRoutes,
route
);
// prettier-ignore
const rules = [
// Setup status
{
condition: getConfig("SETUP_STATUS") == "STARTED" && route != "/auth/signUp",
path: "/auth/signUp",
},
{
condition: getConfig("SETUP_STATUS") == "REGISTERED" && !isSetupStatusRegisteredRoute,
path: user ? "/admin/setup" : "/auth/signIn",
},
// Authenticated state
{
condition: user && isUnauthenticatedRoute,
path: "/upload",
},
// Unauthenticated state
{
condition: !user && !isPublicRoute && !isUnauthenticatedRoute,
path: "/auth/signIn",
},
{
condition: !user && isAccountRoute,
path: "/upload",
},
// Admin privileges
{
condition: isAdminRoute && !user?.isAdmin,
path: "/upload",
},
// Home page
{
condition: (!getConfig("SHOW_HOME_PAGE") || user) && route == "/",
path: "/upload",
},
];
for (const rule of rules) {
if (rule.condition)
return NextResponse.redirect(new URL(rule.path, request.url));
}
}

View file

@ -9,7 +9,6 @@ import { useColorScheme } from "@mantine/hooks";
import { ModalsProvider } from "@mantine/modals"; import { ModalsProvider } from "@mantine/modals";
import { NotificationsProvider } from "@mantine/notifications"; import { NotificationsProvider } from "@mantine/notifications";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Header from "../components/navBar/NavBar"; import Header from "../components/navBar/NavBar";
import { ConfigContext } from "../hooks/config.hook"; import { ConfigContext } from "../hooks/config.hook";
@ -26,7 +25,7 @@ import { GlobalLoadingContext } from "../utils/loading.util";
function App({ Component, pageProps }: AppProps) { function App({ Component, pageProps }: AppProps) {
const systemTheme = useColorScheme(); const systemTheme = useColorScheme();
const router = useRouter();
const preferences = usePreferences(); const preferences = usePreferences();
const [colorScheme, setColorScheme] = useState<ColorScheme>("light"); const [colorScheme, setColorScheme] = useState<ColorScheme>("light");
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -46,25 +45,6 @@ function App({ Component, pageProps }: AppProps) {
getInitalData(); getInitalData();
}, []); }, []);
// Redirect to setup page if setup is not completed
useEffect(() => {
if (
configVariables &&
!["/auth/signUp", "/admin/setup"].includes(router.asPath)
) {
const setupStatus = configVariables.filter(
(variable) => variable.key == "SETUP_STATUS"
)[0].value;
if (setupStatus == "STARTED") {
router.replace("/auth/signUp");
} else if (user && setupStatus == "REGISTERED") {
router.replace("/admin/setup");
} else if (setupStatus == "REGISTERED") {
router.replace("/auth/signIn");
}
}
}, [configVariables, router.asPath]);
useEffect(() => { useEffect(() => {
setColorScheme( setColorScheme(
preferences.get("colorScheme") == "system" preferences.get("colorScheme") == "system"

View file

@ -13,7 +13,6 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form"; import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { useRouter } from "next/router";
import { Tb2Fa } from "react-icons/tb"; import { Tb2Fa } from "react-icons/tb";
import * as yup from "yup"; import * as yup from "yup";
import showEnableTotpModal from "../../components/account/showEnableTotpModal"; import showEnableTotpModal from "../../components/account/showEnableTotpModal";
@ -27,7 +26,6 @@ import toast from "../../utils/toast.util";
const Account = () => { const Account = () => {
const { user, setUser } = useUser(); const { user, setUser } = useUser();
const modals = useModals(); const modals = useModals();
const router = useRouter();
const accountForm = useForm({ const accountForm = useForm({
initialValues: { initialValues: {
@ -85,11 +83,6 @@ const Account = () => {
const refreshUser = async () => setUser(await userService.getCurrentUser()); const refreshUser = async () => setUser(await userService.getCurrentUser());
if (!user) {
router.push("/");
return;
}
return ( return (
<> <>
<Meta title="My account" /> <Meta title="My account" />
@ -171,7 +164,7 @@ const Account = () => {
</Tabs.List> </Tabs.List>
<Tabs.Panel value="totp" pt="xs"> <Tabs.Panel value="totp" pt="xs">
{user.totpVerified ? ( {user!.totpVerified ? (
<> <>
<form <form
onSubmit={disableTotpForm.onSubmit((values) => { onSubmit={disableTotpForm.onSubmit((values) => {

View file

@ -13,7 +13,6 @@ import {
import { useClipboard } from "@mantine/hooks"; import { useClipboard } from "@mantine/hooks";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import moment from "moment"; import moment from "moment";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb"; import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
import showShareLinkModal from "../../components/account/showShareLinkModal"; import showShareLinkModal from "../../components/account/showShareLinkModal";
@ -21,7 +20,6 @@ import CenterLoader from "../../components/core/CenterLoader";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal"; import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
import useConfig from "../../hooks/config.hook"; import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
import { MyReverseShare } from "../../types/share.type"; import { MyReverseShare } from "../../types/share.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util"; import { byteToHumanSizeString } from "../../utils/fileSize.util";
@ -30,10 +28,8 @@ import toast from "../../utils/toast.util";
const MyShares = () => { const MyShares = () => {
const modals = useModals(); const modals = useModals();
const clipboard = useClipboard(); const clipboard = useClipboard();
const router = useRouter();
const config = useConfig();
const { user } = useUser(); const config = useConfig();
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>(); const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
@ -47,154 +43,145 @@ const MyShares = () => {
getReverseShares(); getReverseShares();
}, []); }, []);
if (!user) { if (!reverseShares) return <CenterLoader />;
router.replace("/"); return (
} else { <>
if (!reverseShares) return <CenterLoader />; <Meta title="My shares" />
return ( <Group position="apart" align="baseline" mb={20}>
<> <Group align="center" spacing={3} mb={30}>
<Meta title="My shares" /> <Title order={3}>My reverse shares</Title>
<Group position="apart" align="baseline" mb={20}> <Tooltip
<Group align="center" spacing={3} mb={30}> position="bottom"
<Title order={3}>My reverse shares</Title> multiline
<Tooltip width={220}
position="bottom" label="A reverse share allows you to generate a unique URL for a single-use share for an external user."
multiline events={{ hover: true, focus: false, touch: true }}
width={220}
label="A reverse share allows you to generate a unique URL for a single-use share for an external user."
events={{ hover: true, focus: false, touch: true }}
>
<ActionIcon>
<TbInfoCircle />
</ActionIcon>
</Tooltip>
</Group>
<Button
onClick={() =>
showCreateReverseShareModal(
modals,
config.get("SMTP_ENABLED"),
getReverseShares
)
}
leftIcon={<TbPlus size={20} />}
> >
Create <ActionIcon>
</Button> <TbInfoCircle />
</ActionIcon>
</Tooltip>
</Group> </Group>
{reverseShares.length == 0 ? ( <Button
<Center style={{ height: "70vh" }}> onClick={() =>
<Stack align="center" spacing={10}> showCreateReverseShareModal(
<Title order={3}>It's empty here 👀</Title> modals,
<Text>You don't have any reverse shares.</Text> config.get("SMTP_ENABLED"),
</Stack> getReverseShares
</Center> )
) : ( }
<Box sx={{ display: "block", overflowX: "auto" }}> leftIcon={<TbPlus size={20} />}
<Table> >
<thead> Create
<tr> </Button>
<th>Name</th> </Group>
<th>Visitors</th> {reverseShares.length == 0 ? (
<th>Max share size</th> <Center style={{ height: "70vh" }}>
<th>Expires at</th> <Stack align="center" spacing={10}>
<th></th> <Title order={3}>It's empty here 👀</Title>
</tr> <Text>You don't have any reverse shares.</Text>
</thead> </Stack>
<tbody> </Center>
{reverseShares.map((reverseShare) => ( ) : (
<tr key={reverseShare.id}> <Box sx={{ display: "block", overflowX: "auto" }}>
<td> <Table>
{reverseShare.share ? ( <thead>
reverseShare.share?.id <tr>
) : ( <th>Name</th>
<Text color="dimmed">No share created yet</Text> <th>Visitors</th>
)} <th>Max share size</th>
</td> <th>Expires at</th>
<td>{reverseShare.share?.views ?? "0"}</td> <th></th>
<td> </tr>
{byteToHumanSizeString( </thead>
parseInt(reverseShare.maxShareSize) <tbody>
)} {reverseShares.map((reverseShare) => (
</td> <tr key={reverseShare.id}>
<td> <td>
{moment(reverseShare.shareExpiration).unix() === 0 {reverseShare.share ? (
? "Never" reverseShare.share?.id
: moment(reverseShare.shareExpiration).format("LLL")} ) : (
</td> <Text color="dimmed">No share created yet</Text>
<td> )}
<Group position="right"> </td>
{reverseShare.share && ( <td>{reverseShare.share?.views ?? "0"}</td>
<ActionIcon <td>
color="victoria" {byteToHumanSizeString(parseInt(reverseShare.maxShareSize))}
variant="light" </td>
size={25} <td>
onClick={() => { {moment(reverseShare.shareExpiration).unix() === 0
if (window.isSecureContext) { ? "Never"
clipboard.copy( : moment(reverseShare.shareExpiration).format("LLL")}
`${config.get("APP_URL")}/share/${ </td>
reverseShare.share!.id <td>
}` <Group position="right">
); {reverseShare.share && (
toast.success(
"The share link was copied to the keyboard."
);
} else {
showShareLinkModal(
modals,
reverseShare.share!.id,
config.get("APP_URL")
);
}
}}
>
<TbLink />
</ActionIcon>
)}
<ActionIcon <ActionIcon
color="red" color="victoria"
variant="light" variant="light"
size={25} size={25}
onClick={() => { onClick={() => {
modals.openConfirmModal({ if (window.isSecureContext) {
title: `Delete reverse share`, clipboard.copy(
children: ( `${config.get("APP_URL")}/share/${
<Text size="sm"> reverseShare.share!.id
Do you really want to delete this reverse }`
share? If you do, the share will be deleted as );
well. toast.success(
</Text> "The share link was copied to the keyboard."
), );
confirmProps: { } else {
color: "red", showShareLinkModal(
}, modals,
labels: { confirm: "Confirm", cancel: "Cancel" }, reverseShare.share!.id,
onConfirm: () => { config.get("APP_URL")
shareService.removeReverseShare( );
reverseShare.id }
);
setReverseShares(
reverseShares.filter(
(item) => item.id !== reverseShare.id
)
);
},
});
}} }}
> >
<TbTrash /> <TbLink />
</ActionIcon> </ActionIcon>
</Group> )}
</td> <ActionIcon
</tr> color="red"
))} variant="light"
</tbody> size={25}
</Table> onClick={() => {
</Box> modals.openConfirmModal({
)} title: `Delete reverse share`,
</> children: (
); <Text size="sm">
} Do you really want to delete this reverse share?
If you do, the share will be deleted as well.
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Confirm", cancel: "Cancel" },
onConfirm: () => {
shareService.removeReverseShare(reverseShare.id);
setReverseShares(
reverseShares.filter(
(item) => item.id !== reverseShare.id
)
);
},
});
}}
>
<TbTrash />
</ActionIcon>
</Group>
</td>
</tr>
))}
</tbody>
</Table>
</Box>
)}
</>
);
}; };
export default MyShares; export default MyShares;

View file

@ -15,13 +15,11 @@ import { useClipboard } from "@mantine/hooks";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import moment from "moment"; import moment from "moment";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TbLink, TbTrash } from "react-icons/tb"; import { TbLink, TbTrash } from "react-icons/tb";
import showShareLinkModal from "../../components/account/showShareLinkModal"; import showShareLinkModal from "../../components/account/showShareLinkModal";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook"; import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
import shareService from "../../services/share.service"; import shareService from "../../services/share.service";
import { MyShare } from "../../types/share.type"; import { MyShare } from "../../types/share.type";
import toast from "../../utils/toast.util"; import toast from "../../utils/toast.util";
@ -29,122 +27,115 @@ import toast from "../../utils/toast.util";
const MyShares = () => { const MyShares = () => {
const modals = useModals(); const modals = useModals();
const clipboard = useClipboard(); const clipboard = useClipboard();
const router = useRouter();
const config = useConfig(); const config = useConfig();
const { user } = useUser();
const [shares, setShares] = useState<MyShare[]>(); const [shares, setShares] = useState<MyShare[]>();
useEffect(() => { useEffect(() => {
shareService.getMyShares().then((shares) => setShares(shares)); shareService.getMyShares().then((shares) => setShares(shares));
}, []); }, []);
if (!user) { if (!shares) return <LoadingOverlay visible />;
router.replace("/"); return (
} else { <>
if (!shares) return <LoadingOverlay visible />; <Meta title="My shares" />
return ( <Title mb={30} order={3}>
<> My shares
<Meta title="My shares" /> </Title>
<Title mb={30} order={3}> {shares.length == 0 ? (
My shares <Center style={{ height: "70vh" }}>
</Title> <Stack align="center" spacing={10}>
{shares.length == 0 ? ( <Title order={3}>It's empty here 👀</Title>
<Center style={{ height: "70vh" }}> <Text>You don't have any shares.</Text>
<Stack align="center" spacing={10}> <Space h={5} />
<Title order={3}>It's empty here 👀</Title> <Button component={Link} href="/upload" variant="light">
<Text>You don't have any shares.</Text> Create one
<Space h={5} /> </Button>
<Button component={Link} href="/upload" variant="light"> </Stack>
Create one </Center>
</Button> ) : (
</Stack> <Box sx={{ display: "block", overflowX: "auto" }}>
</Center> <Table>
) : ( <thead>
<Box sx={{ display: "block", overflowX: "auto" }}> <tr>
<Table> <th>Name</th>
<thead> <th>Visitors</th>
<tr> <th>Expires at</th>
<th>Name</th> <th></th>
<th>Visitors</th> </tr>
<th>Expires at</th> </thead>
<th></th> <tbody>
{shares.map((share) => (
<tr key={share.id}>
<td>{share.id}</td>
<td>{share.views}</td>
<td>
{moment(share.expiration).unix() === 0
? "Never"
: moment(share.expiration).format("LLL")}
</td>
<td>
<Group position="right">
<ActionIcon
color="victoria"
variant="light"
size={25}
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get("APP_URL")}/share/${share.id}`
);
toast.success(
"Your link was copied to the keyboard."
);
} else {
showShareLinkModal(
modals,
share.id,
config.get("APP_URL")
);
}
}}
>
<TbLink />
</ActionIcon>
<ActionIcon
color="red"
variant="light"
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete share ${share.id}`,
children: (
<Text size="sm">
Do you really want to delete this share?
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Confirm", cancel: "Cancel" },
onConfirm: () => {
shareService.remove(share.id);
setShares(
shares.filter((item) => item.id !== share.id)
);
},
});
}}
>
<TbTrash />
</ActionIcon>
</Group>
</td>
</tr> </tr>
</thead> ))}
<tbody> </tbody>
{shares.map((share) => ( </Table>
<tr key={share.id}> </Box>
<td>{share.id}</td> )}
<td>{share.views}</td> </>
<td> );
{moment(share.expiration).unix() === 0
? "Never"
: moment(share.expiration).format("LLL")}
</td>
<td>
<Group position="right">
<ActionIcon
color="victoria"
variant="light"
size={25}
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get("APP_URL")}/share/${share.id}`
);
toast.success(
"Your link was copied to the keyboard."
);
} else {
showShareLinkModal(
modals,
share.id,
config.get("APP_URL")
);
}
}}
>
<TbLink />
</ActionIcon>
<ActionIcon
color="red"
variant="light"
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete share ${share.id}`,
children: (
<Text size="sm">
Do you really want to delete this share?
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Confirm", cancel: "Cancel" },
onConfirm: () => {
shareService.remove(share.id);
setShares(
shares.filter((item) => item.id !== share.id)
);
},
});
}}
>
<TbTrash />
</ActionIcon>
</Group>
</td>
</tr>
))}
</tbody>
</Table>
</Box>
)}
</>
);
}
}; };
export default MyShares; export default MyShares;

View file

@ -1,25 +1,10 @@
import { Box, Stack, Text, Title } from "@mantine/core"; import { Box, Stack, Text, Title } from "@mantine/core";
import { useRouter } from "next/router";
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable"; import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
import Logo from "../../components/Logo"; import Logo from "../../components/Logo";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
const Setup = () => { const Setup = () => {
const router = useRouter();
const config = useConfig();
const { user } = useUser();
if (!user) {
router.push("/auth/signUp");
return;
} else if (config.get("SETUP_STATUS") == "FINISHED") {
router.push("/");
return;
}
return ( return (
<> <>
<Meta title="Setup" /> <Meta title="Setup" />

View file

@ -1,20 +1,25 @@
import { LoadingOverlay } from "@mantine/core";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import SignInForm from "../../components/auth/SignInForm"; import SignInForm from "../../components/auth/SignInForm";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import useUser from "../../hooks/user.hook"; import useUser from "../../hooks/user.hook";
const SignIn = () => { const SignIn = () => {
const { user } = useUser();
const router = useRouter(); const router = useRouter();
const { user } = useUser();
// If the access token is expired, the middleware redirects to this page.
// If the refresh token is still valid, the user will be redirected to the home page.
if (user) { if (user) {
router.replace("/"); router.replace("/");
} else { return <LoadingOverlay overlayOpacity={1} visible />;
return (
<>
<Meta title="Sign In" />
<SignInForm />
</>
);
} }
return (
<>
<Meta title="Sign In" />
<SignInForm />
</>
);
}; };
export default SignIn; export default SignIn;

View file

@ -1,24 +1,12 @@
import { useRouter } from "next/router";
import SignUpForm from "../../components/auth/SignUpForm"; import SignUpForm from "../../components/auth/SignUpForm";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import useConfig from "../../hooks/config.hook";
import useUser from "../../hooks/user.hook";
const SignUp = () => { const SignUp = () => {
const config = useConfig(); return (
const { user } = useUser(); <>
const router = useRouter(); <Meta title="Sign Up" />
if (user) { <SignUpForm />
router.replace("/"); </>
} else if (!config.get("ALLOW_REGISTRATION")) { );
router.replace("/auth/signIn");
} else {
return (
<>
<Meta title="Sign Up" />
<SignUpForm />
</>
);
}
}; };
export default SignUp; export default SignUp;

View file

@ -10,11 +10,8 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
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 useConfig from "../hooks/config.hook";
import useUser from "../hooks/user.hook";
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
inner: { inner: {
@ -69,94 +66,85 @@ const useStyles = createStyles((theme) => ({
})); }));
export default function Home() { export default function Home() {
const config = useConfig();
const { user } = useUser();
const { classes } = useStyles(); const { classes } = useStyles();
const router = useRouter();
if (user || config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
router.replace("/upload");
} else if (!config.get("SHOW_HOME_PAGE")) {
router.replace("/auth/signIn");
} else {
return (
<>
<Meta title="Home" />
<Container>
<div className={classes.inner}>
<div className={classes.content}>
<Title className={classes.title}>
A <span className={classes.highlight}>self-hosted</span> <br />{" "}
file sharing platform.
</Title>
<Text color="dimmed" mt="md">
Do you really want to give your personal files in the hand of
third parties like WeTransfer?
</Text>
<List return (
mt={30} <>
spacing="sm" <Meta title="Home" />
size="sm" <Container>
icon={ <div className={classes.inner}>
<ThemeIcon size={20} radius="xl"> <div className={classes.content}>
<TbCheck size={12} /> <Title className={classes.title}>
</ThemeIcon> A <span className={classes.highlight}>self-hosted</span> <br />{" "}
} file sharing platform.
</Title>
<Text color="dimmed" mt="md">
Do you really want to give your personal files in the hand of
third parties like WeTransfer?
</Text>
<List
mt={30}
spacing="sm"
size="sm"
icon={
<ThemeIcon size={20} radius="xl">
<TbCheck size={12} />
</ThemeIcon>
}
>
<List.Item>
<div>
<b>Self-Hosted</b> - Host Pingvin Share on your own machine.
</div>
</List.Item>
<List.Item>
<div>
<b>Privacy</b> - Your files are your files and should never
get into the hands of third parties.
</div>
</List.Item>
<List.Item>
<div>
<b>No annoying file size limit</b> - Upload as big files as
you want. Only your hard drive will be your limit.
</div>
</List.Item>
</List>
<Group mt={30}>
<Button
component={Link}
href="/auth/signUp"
radius="xl"
size="md"
className={classes.control}
> >
<List.Item> Get started
<div> </Button>
<b>Self-Hosted</b> - Host Pingvin Share on your own machine. <Button
</div> component={Link}
</List.Item> href="https://github.com/stonith404/pingvin-share"
<List.Item> target="_blank"
<div> variant="default"
<b>Privacy</b> - Your files are your files and should never radius="xl"
get into the hands of third parties. size="md"
</div> className={classes.control}
</List.Item> >
<List.Item> Source code
<div> </Button>
<b>No annoying file size limit</b> - Upload as big files as
you want. Only your hard drive will be your limit.
</div>
</List.Item>
</List>
<Group mt={30}>
<Button
component={Link}
href="/auth/signUp"
radius="xl"
size="md"
className={classes.control}
>
Get started
</Button>
<Button
component={Link}
href="https://github.com/stonith404/pingvin-share"
target="_blank"
variant="default"
radius="xl"
size="md"
className={classes.control}
>
Source code
</Button>
</Group>
</div>
<Group className={classes.image} align="center">
<Image
src="/img/logo.svg"
alt="Pingvin Share Logo"
width={200}
height={200}
/>
</Group> </Group>
</div> </div>
</Container> <Group className={classes.image} align="center">
</> <Image
); src="/img/logo.svg"
} alt="Pingvin Share Logo"
width={200}
height={200}
/>
</Group>
</div>
</Container>
</>
);
} }

View file

@ -2,8 +2,6 @@ import { Button, Group } from "@mantine/core";
import { useModals } from "@mantine/modals"; import { useModals } from "@mantine/modals";
import { cleanNotifications } from "@mantine/notifications"; import { cleanNotifications } from "@mantine/notifications";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { getCookie } from "cookies-next";
import { useRouter } from "next/router";
import pLimit from "p-limit"; import pLimit from "p-limit";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
@ -30,7 +28,6 @@ const Upload = ({
maxShareSize?: number; maxShareSize?: number;
isReverseShare: boolean; isReverseShare: boolean;
}) => { }) => {
const router = useRouter();
const modals = useModals(); const modals = useModals();
const { user } = useUser(); const { user } = useUser();
@ -158,51 +155,42 @@ const Upload = ({
} }
}, [files]); }, [files]);
if ( return (
!user && <>
!config.get("ALLOW_UNAUTHENTICATED_SHARES") && <Meta title="Upload" />
!getCookie("reverse_share_token") <Group position="right" mb={20}>
) { <Button
router.replace("/"); loading={isUploading}
return null; disabled={files.length <= 0}
} else { onClick={() => {
return ( showCreateUploadModal(
<> modals,
<Meta title="Upload" /> {
<Group position="right" mb={20}> isUserSignedIn: user ? true : false,
<Button isReverseShare,
loading={isUploading} appUrl: config.get("APP_URL"),
disabled={files.length <= 0} allowUnauthenticatedShares: config.get(
onClick={() => { "ALLOW_UNAUTHENTICATED_SHARES"
showCreateUploadModal( ),
modals, enableEmailRecepients: config.get(
{ "ENABLE_SHARE_EMAIL_RECIPIENTS"
isUserSignedIn: user ? true : false, ),
isReverseShare, },
appUrl: config.get("APP_URL"), uploadFiles
allowUnauthenticatedShares: config.get( );
"ALLOW_UNAUTHENTICATED_SHARES" }}
), >
enableEmailRecepients: config.get( Share
"ENABLE_SHARE_EMAIL_RECIPIENTS" </Button>
), </Group>
}, <Dropzone
uploadFiles maxShareSize={maxShareSize}
); files={files}
}} setFiles={setFiles}
> isUploading={isUploading}
Share />
</Button> {files.length > 0 && <FileList files={files} setFiles={setFiles} />}
</Group> </>
<Dropzone );
maxShareSize={maxShareSize}
files={files}
setFiles={setFiles}
isUploading={isUploading}
/>
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
</>
);
}
}; };
export default Upload; export default Upload;