feat: custom branding (#112)

* add first concept

* remove setup status

* split config page in multiple components

* add custom branding docs

* add test email button

* fix invalid email from header

* add migration

* mount images to host

* update docs

* remove unused endpoint

* run formatter
This commit is contained in:
Elias Schneider
2023-03-04 23:29:00 +01:00
committed by GitHub
parent f9840505b8
commit fddad3ef70
66 changed files with 908 additions and 623 deletions

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 944 B

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 943.11 911.62"><ellipse cx="471.56" cy="454.28" rx="471.56" ry="454.28" fill="#46509e"/><ellipse cx="471.56" cy="390.28" rx="233.66" ry="207" fill="#37474f"/><path d="M705.22,849c-36.69,21.14-123.09,64.32-240.64,62.57A469.81,469.81,0,0,1,237.89,849V394.76H705.22Z" fill="#37474f"/><path d="M658.81,397.7V873.49a478.12,478.12,0,0,1-374.19,0V397.7c0-95.55,83.78-173,187.1-173S658.81,302.15,658.81,397.7Z" fill="#fff"/><polygon points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68" fill="#46509e"/><ellipse cx="378.09" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><ellipse cx="565.02" cy="369.58" rx="23.37" ry="20.7" fill="#37474f"/><path d="M658.49,400.63c0-40-36.6-72.45-81.79-72.45s-81.78,32.41-81.78,72.45a64.79,64.79,0,0,0,7.9,31.05H440.29a64.79,64.79,0,0,0,7.9-31.05c0-40-36.59-72.45-81.78-72.45s-81.79,32.41-81.79,72.45l-46.73-10.35c0-114.31,104.64-207,233.67-207s233.66,92.69,233.66,207Z" fill="#37474f"/></svg>

Before

Width:  |  Height:  |  Size: 1018 B

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -8,55 +8,55 @@
"start_url": "/",
"icons": [
{
"src": "icons/icon-72x72.png",
"src": "img/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-96x96.png",
"src": "img/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-96x96.png",
"src": "img/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-128x128.png",
"src": "img/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-144x144.png",
"src": "img/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-152x152.png",
"src": "img/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-192x192.png",
"src": "img/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-384x384.png",
"src": "img/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-512x512.png",
"src": "img/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"

View File

@@ -1,34 +1,6 @@
import Image from "next/image";
const Logo = ({ height, width }: { height: number; width: number }) => {
return (
<svg
id="Layer_1"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 943.11 911.62"
height={height}
width={width}
>
<ellipse cx="471.56" cy="454.28" rx="471.56" ry="454.28" fill="#46509e" />
<ellipse cx="471.56" cy="390.28" rx="233.66" ry="207" fill="#37474f" />
<path
d="M705.22,849c-36.69,21.14-123.09,64.32-240.64,62.57A469.81,469.81,0,0,1,237.89,849V394.76H705.22Z"
fill="#37474f"
/>
<path
d="M658.81,397.7V873.49a478.12,478.12,0,0,1-374.19,0V397.7c0-95.55,83.78-173,187.1-173S658.81,302.15,658.81,397.7Z"
fill="#fff"
/>
<polygon
points="565.02 431.68 471.56 514.49 378.09 431.68 565.02 431.68"
fill="#46509e"
/>
<ellipse cx="378.09" cy="369.58" rx="23.37" ry="20.7" fill="#37474f" />
<ellipse cx="565.02" cy="369.58" rx="23.37" ry="20.7" fill="#37474f" />
<path
d="M658.49,400.63c0-40-36.6-72.45-81.79-72.45s-81.78,32.41-81.78,72.45a64.79,64.79,0,0,0,7.9,31.05H440.29a64.79,64.79,0,0,0,7.9-31.05c0-40-36.59-72.45-81.78-72.45s-81.79,32.41-81.79,72.45l-46.73-10.35c0-114.31,104.64-207,233.67-207s233.66,92.69,233.66,207Z"
fill="#37474f"
/>
</svg>
);
return <Image src="/img/logo.png" alt="logo" height={height} width={width} />;
};
export default Logo;

View File

@@ -1,4 +1,5 @@
import Head from "next/head";
import useConfig from "../hooks/config.hook";
const Meta = ({
title,
@@ -7,7 +8,9 @@ const Meta = ({
title: string;
description?: string;
}) => {
const metaTitle = `${title} - Pingvin Share`;
const config = useConfig();
const metaTitle = `${title} - ${config.get("general.appName")}`;
return (
<Head>
@@ -19,7 +22,6 @@ const Meta = ({
description ?? "An open-source and self-hosted sharing platform."
}
/>
<meta property="og:image" content="/img/opengraph-default.png" />
<meta name="twitter:title" content={metaTitle} />
<meta name="twitter:description" content={description} />
</Head>

View File

@@ -1,153 +0,0 @@
import {
Box,
Button,
Group,
Paper,
Space,
Stack,
Text,
Title,
} from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import useConfig from "../../../hooks/config.hook";
import configService from "../../../services/config.service";
import {
AdminConfigGroupedByCategory,
UpdateConfig,
} from "../../../types/config.type";
import {
capitalizeFirstLetter,
configVariableToFriendlyName,
} from "../../../utils/string.util";
import toast from "../../../utils/toast.util";
import AdminConfigInput from "./AdminConfigInput";
import TestEmailButton from "./TestEmailButton";
const AdminConfigTable = () => {
const config = useConfig();
const router = useRouter();
const isMobile = useMediaQuery("(max-width: 560px)");
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
UpdateConfig[]
>([]);
useEffect(() => {
if (config.get("SETUP_STATUS") != "FINISHED") {
config.refresh();
}
}, []);
const updateConfigVariable = (configVariable: UpdateConfig) => {
const index = updatedConfigVariables.findIndex(
(item) => item.key === configVariable.key
);
if (index > -1) {
updatedConfigVariables[index] = configVariable;
} else {
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
}
};
const [configVariablesByCategory, setCofigVariablesByCategory] =
useState<AdminConfigGroupedByCategory>({});
const getConfigVariables = async () => {
await configService.listForAdmin().then((configVariables) => {
const configVariablesByCategory = configVariables.reduce(
(categories: any, item) => {
const category = categories[item.category] || [];
category.push(item);
categories[item.category] = category;
return categories;
},
{}
);
setCofigVariablesByCategory(configVariablesByCategory);
});
};
const saveConfigVariables = async () => {
if (config.get("SETUP_STATUS") == "REGISTERED") {
await configService
.updateMany(updatedConfigVariables)
.then(async () => {
await configService.finishSetup();
router.reload();
})
.catch(toast.axiosError);
} else {
await configService
.updateMany(updatedConfigVariables)
.then(() => {
setUpdatedConfigVariables([]);
toast.success("Configurations updated successfully");
})
.catch(toast.axiosError);
}
config.refresh();
};
useEffect(() => {
getConfigVariables();
}, []);
return (
<Box mb="lg">
{Object.entries(configVariablesByCategory).map(
([category, configVariables]) => {
return (
<Paper key={category} withBorder p="lg" mb="xl">
<Title mb="xs" order={3}>
{capitalizeFirstLetter(category)}
</Title>
{configVariables.map((configVariable) => (
<>
<Group position="apart">
<Stack
style={{ maxWidth: isMobile ? "100%" : "40%" }}
spacing={0}
>
<Title order={6}>
{configVariableToFriendlyName(configVariable.key)}
</Title>
<Text color="dimmed" size="sm" mb="xs">
{configVariable.description}
</Text>
</Stack>
<Stack></Stack>
<Box style={{ width: isMobile ? "100%" : "50%" }}>
<AdminConfigInput
key={configVariable.key}
updateConfigVariable={updateConfigVariable}
configVariable={configVariable}
/>
</Box>
</Group>
<Space h="lg" />
</>
))}
{category == "smtp" && (
<Group position="right">
<TestEmailButton
configVariablesChanged={updatedConfigVariables.length != 0}
saveConfigVariables={saveConfigVariables}
/>
</Group>
)}
</Paper>
);
}
)}
<Group position="right">
<Button onClick={saveConfigVariables}>Save</Button>
</Group>
</Box>
);
};
export default AdminConfigTable;

View File

@@ -0,0 +1,54 @@
import {
Burger,
Button,
Group,
Header,
MediaQuery,
Text,
useMantineTheme,
} from "@mantine/core";
import Link from "next/link";
import { Dispatch, SetStateAction } from "react";
import useConfig from "../../../hooks/config.hook";
import Logo from "../../Logo";
const ConfigurationHeader = ({
isMobileNavBarOpened,
setIsMobileNavBarOpened,
}: {
isMobileNavBarOpened: boolean;
setIsMobileNavBarOpened: Dispatch<SetStateAction<boolean>>;
}) => {
const config = useConfig();
const theme = useMantineTheme();
return (
<Header height={60} p="md">
<div style={{ display: "flex", alignItems: "center", height: "100%" }}>
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
<Burger
opened={isMobileNavBarOpened}
onClick={() => setIsMobileNavBarOpened((o) => !o)}
size="sm"
color={theme.colors.gray[6]}
mr="xl"
/>
</MediaQuery>
<Group position="apart" w="100%">
<Link href="/" passHref>
<Group>
<Logo height={35} width={35} />
<Text weight={600}>{config.get("general.appName")}</Text>
</Group>
</Link>
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
<Button variant="light" component={Link} href="/admin">
Go back
</Button>
</MediaQuery>
</Group>
</div>
</Header>
);
};
export default ConfigurationHeader;

View File

@@ -0,0 +1,97 @@
import {
Box,
Button,
createStyles,
Group,
MediaQuery,
Navbar,
Stack,
Text,
ThemeIcon,
} from "@mantine/core";
import Link from "next/link";
import { Dispatch, SetStateAction } from "react";
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
const categories = [
{ name: "General", icon: <TbSquare /> },
{ name: "Email", icon: <TbMail /> },
{ name: "Share", icon: <TbShare /> },
{ name: "SMTP", icon: <TbAt /> },
];
const useStyles = createStyles((theme) => ({
activeLink: {
backgroundColor: theme.fn.variant({
variant: "light",
color: theme.primaryColor,
}).background,
color: theme.fn.variant({ variant: "light", color: theme.primaryColor })
.color,
borderRadius: theme.radius.sm,
fontWeight: 600,
},
}));
const ConfigurationNavBar = ({
categoryId,
isMobileNavBarOpened,
setIsMobileNavBarOpened,
}: {
categoryId: string;
isMobileNavBarOpened: boolean;
setIsMobileNavBarOpened: Dispatch<SetStateAction<boolean>>;
}) => {
const { classes } = useStyles();
return (
<Navbar
p="md"
hiddenBreakpoint="sm"
hidden={!isMobileNavBarOpened}
width={{ sm: 200, lg: 300 }}
>
<Navbar.Section>
<Text size="xs" color="dimmed" mb="sm">
Configuration
</Text>
<Stack spacing="xs">
{categories.map((category) => (
<Box
p="xs"
component={Link}
onClick={() => setIsMobileNavBarOpened(false)}
className={
categoryId == category.name.toLowerCase()
? classes.activeLink
: undefined
}
key={category.name}
href={`/admin/config/${category.name.toLowerCase()}`}
>
<Group>
<ThemeIcon
variant={
categoryId == category.name.toLowerCase()
? "filled"
: "light"
}
>
{category.icon}
</ThemeIcon>
<Text size="sm">{category.name}</Text>
</Group>
</Box>
))}
</Stack>
</Navbar.Section>
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
<Button mt="xl" variant="light" component={Link} href="/admin">
Go back
</Button>
</MediaQuery>
</Navbar>
);
};
export default ConfigurationNavBar;

View File

@@ -79,12 +79,13 @@ const Body = ({
})}
/>
)}
{form.values.setPasswordManually || !smtpEnabled && (
<PasswordInput
label="Password"
{...form.getInputProps("password")}
/>
)}
{form.values.setPasswordManually ||
(!smtpEnabled && (
<PasswordInput
label="Password"
{...form.getInputProps("password")}
/>
))}
<Switch
styles={{
body: {

View File

@@ -95,7 +95,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
<Title order={2} align="center" weight={900}>
Welcome back
</Title>
{config.get("ALLOW_REGISTRATION") && (
{config.get("share.allowRegistration") && (
<Text color="dimmed" size="sm" align="center" mt={5}>
You don't have an account yet?{" "}
<Anchor component={Link} href={"signUp"} size="sm">
@@ -131,7 +131,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
{...form.getInputProps("totp")}
/>
)}
{config.get("SMTP_ENABLED") && (
{config.get("smtp.enabled") && (
<Group position="right" mt="xs">
<Anchor component={Link} href="/auth/resetPassword" size="xs">
Forgot password?

View File

@@ -41,8 +41,12 @@ const SignUpForm = () => {
await authService
.signUp(email, username, password)
.then(async () => {
await refreshUser();
router.replace("/upload");
const user = await refreshUser();
if (user?.isAdmin) {
router.replace("/admin/intro");
} else {
router.replace("/upload");
}
})
.catch(toast.axiosError);
};
@@ -52,7 +56,7 @@ const SignUpForm = () => {
<Title order={2} align="center" weight={900}>
Sign up
</Title>
{config.get("ALLOW_REGISTRATION") && (
{config.get("share.allowRegistration") && (
<Text color="dimmed" size="sm" align="center" mt={5}>
You have an account already?{" "}
<Anchor component={Link} href={"signIn"} size="sm">

View File

@@ -4,7 +4,7 @@ import {
Container,
createStyles,
Group,
Header,
Header as MantineHeader,
Paper,
Stack,
Text,
@@ -108,7 +108,7 @@ const useStyles = createStyles((theme) => ({
},
}));
const NavBar = () => {
const Header = () => {
const { user } = useUser();
const router = useRouter();
const config = useConfig();
@@ -141,20 +141,20 @@ const NavBar = () => {
},
];
if (config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
if (config.get("share.allowUnauthenticatedShares")) {
unauthenticatedLinks.unshift({
link: "/upload",
label: "Upload",
});
}
if (config.get("SHOW_HOME_PAGE"))
if (config.get("general.showHomePage"))
unauthenticatedLinks.unshift({
link: "/",
label: "Home",
});
if (config.get("ALLOW_REGISTRATION"))
if (config.get("share.allowRegistration"))
unauthenticatedLinks.push({
link: "/auth/signUp",
label: "Sign up",
@@ -187,12 +187,12 @@ const NavBar = () => {
</>
);
return (
<Header height={HEADER_HEIGHT} mb={40} className={classes.root}>
<MantineHeader height={HEADER_HEIGHT} mb={40} className={classes.root}>
<Container className={classes.header}>
<Link href="/" passHref>
<Group>
<Logo height={35} width={35} />
<Text weight={600}>Pingvin Share</Text>
<Text weight={600}>{config.get("general.appName")}</Text>
</Group>
</Link>
<Group spacing={5} className={classes.links}>
@@ -212,8 +212,8 @@ const NavBar = () => {
)}
</Transition>
</Container>
</Header>
</MantineHeader>
);
};
export default NavBar;
export default Header;

View File

@@ -33,9 +33,9 @@ const FileList = ({
const modals = useModals();
const copyFileLink = (file: FileMetaData) => {
const link = `${config.get("APP_URL")}/api/shares/${share.id}/files/${
file.id
}`;
const link = `${config.get("general.appUrl")}/api/shares/${
share.id
}/files/${file.id}`;
if (window.isSecureContext) {
clipboard.copy(link);

View File

@@ -15,9 +15,8 @@ export async function middleware(request: NextRequest) {
const routes = {
unauthenticated: new Routes(["/auth/*", "/"]),
public: new Routes(["/share/*", "/upload/*"]),
setupStatusRegistered: new Routes(["/auth/*", "/admin/setup"]),
admin: new Routes(["/admin/*"]),
account: new Routes(["/account/*"]),
account: new Routes(["/account*"]),
disabled: new Routes([]),
};
@@ -45,41 +44,28 @@ export async function middleware(request: NextRequest) {
user = null;
}
if (!getConfig("ALLOW_REGISTRATION")) {
if (!getConfig("share.allowRegistration")) {
routes.disabled.routes.push("/auth/signUp");
}
if (getConfig("ALLOW_UNAUTHENTICATED_SHARES")) {
if (getConfig("share.allowUnauthenticatedShares")) {
routes.public.routes = ["*"];
}
if (!getConfig("SMTP_ENABLED")) {
if (!getConfig("smtp.enabled")) {
routes.disabled.routes.push("/auth/resetPassword*");
}
if (getConfig("SETUP_STATUS") == "FINISHED") {
routes.disabled.routes.push("/admin/setup");
}
// prettier-ignore
const rules = [
// Disabled routes
{
condition: routes.disabled.contains(route),
path: "/",
},
// Setup status
{
condition: getConfig("SETUP_STATUS") == "STARTED" && route != "/auth/signUp",
path: "/auth/signUp",
},
{
condition: getConfig("SETUP_STATUS") == "REGISTERED" && !routes.setupStatusRegistered.contains(route) && user?.isAdmin,
path: "/admin/setup",
},
// Authenticated state
{
condition: user && routes.unauthenticated.contains(route) && !getConfig("ALLOW_UNAUTHENTICATED_SHARES"),
condition: user && routes.unauthenticated.contains(route) && !getConfig("share.allowUnauthenticatedShares"),
path: "/upload",
},
// Unauthenticated state
@@ -98,7 +84,7 @@ export async function middleware(request: NextRequest) {
},
// Home page
{
condition: (!getConfig("SHOW_HOME_PAGE") || user) && route == "/",
condition: (!getConfig("general.showHomePage") || user) && route == "/",
path: "/upload",
},
];

View File

@@ -11,8 +11,9 @@ import axios from "axios";
import { getCookie, setCookie } from "cookies-next";
import { GetServerSidePropsContext } from "next";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import Header from "../components/navBar/NavBar";
import Header from "../components/header/Header";
import { ConfigContext } from "../hooks/config.hook";
import usePreferences from "../hooks/usePreferences";
import { UserContext } from "../hooks/user.hook";
@@ -24,17 +25,26 @@ import globalStyle from "../styles/mantine.style";
import Config from "../types/config.type";
import { CurrentUser } from "../types/user.type";
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
function App({ Component, pageProps }: AppProps) {
const systemTheme = useColorScheme(pageProps.colorScheme);
const router = useRouter();
const [colorScheme, setColorScheme] = useState<ColorScheme>(systemTheme);
const preferences = usePreferences();
const [user, setUser] = useState<CurrentUser | null>(pageProps.user);
const [route, setRoute] = useState<string>(pageProps.route);
const [configVariables, setConfigVariables] = useState<Config[]>(
pageProps.configVariables
);
useEffect(() => {
setRoute(router.pathname);
}, [router.pathname]);
useEffect(() => {
setInterval(async () => await authService.refreshAccessToken(), 30 * 1000);
}, []);
@@ -86,10 +96,16 @@ function App({ Component, pageProps }: AppProps) {
},
}}
>
<Header />
<Container>
{excludeDefaultLayoutRoutes.includes(route) ? (
<Component {...pageProps} />
</Container>
) : (
<>
<Header />
<Container>
<Component {...pageProps} />
</Container>
</>
)}
</UserContext.Provider>
</ConfigContext.Provider>
</ModalsProvider>
@@ -105,12 +121,13 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
let pageProps: {
user?: CurrentUser;
configVariables?: Config[];
route?: string;
colorScheme: ColorScheme;
} = {
route: ctx.resolvedUrl,
colorScheme:
(getCookie("mantine-color-scheme", ctx) as ColorScheme) ?? "light",
};
if (ctx.req) {
const cookieHeader = ctx.req.headers.cookie;
@@ -123,6 +140,8 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
pageProps.configVariables = (
await axios(`http://localhost:8080/api/configs`)
).data;
pageProps.route = ctx.req.url;
}
return { pageProps };

View File

@@ -11,11 +11,15 @@ export default class _Document extends Document {
<Html>
<Head>
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icons/icon-white-128x128.png" />
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
<link
rel="apple-touch-icon"
href="/img/icons/icon-white-128x128.png"
/>
<meta property="og:image" content="/img/opengraph-default.png" />
<meta property="og:image" content="/img/opengraph.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="/img/opengraph-default.png" />
<meta name="twitter:image" content="/img/opengraph.png" />
<meta name="robots" content="noindex" />
<meta name="theme-color" content="#46509e" />
</Head>

View File

@@ -67,7 +67,7 @@ const MyShares = () => {
onClick={() =>
showCreateReverseShareModal(
modals,
config.get("SMTP_ENABLED"),
config.get("smtp.enabled"),
getReverseShares
)
}
@@ -129,9 +129,9 @@ const MyShares = () => {
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get("APP_URL")}/share/${
share.id
}`
`${config.get(
"general.appUrl"
)}/share/${share.id}`
);
toast.success(
"The share link was copied to the keyboard."
@@ -140,7 +140,7 @@ const MyShares = () => {
showShareLinkModal(
modals,
share.id,
config.get("APP_URL")
config.get("general.appUrl")
);
}
}}

View File

@@ -84,7 +84,9 @@ const MyShares = () => {
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get("APP_URL")}/share/${share.id}`
`${config.get("general.appUrl")}/share/${
share.id
}`
);
toast.success(
"Your link was copied to the keyboard."
@@ -93,7 +95,7 @@ const MyShares = () => {
showShareLinkModal(
modals,
share.id,
config.get("APP_URL")
config.get("general.appUrl")
);
}
}}

View File

@@ -1,18 +0,0 @@
import { Space, Title } from "@mantine/core";
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
import Meta from "../../components/Meta";
const AdminConfig = () => {
return (
<>
<Meta title="Configuration" />
<Title mb={30} order={3}>
Configuration
</Title>
<AdminConfigTable />
<Space h="xl" />
</>
);
};
export default AdminConfig;

View File

@@ -0,0 +1,148 @@
import {
AppShell,
Box,
Button,
Container,
Group,
Stack,
Text,
Title,
useMantineTheme,
} from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import AdminConfigInput from "../../../components/admin/configuration/AdminConfigInput";
import ConfigurationHeader from "../../../components/admin/configuration/ConfigurationHeader";
import ConfigurationNavBar from "../../../components/admin/configuration/ConfigurationNavBar";
import TestEmailButton from "../../../components/admin/configuration/TestEmailButton";
import CenterLoader from "../../../components/core/CenterLoader";
import Meta from "../../../components/Meta";
import useConfig from "../../../hooks/config.hook";
import configService from "../../../services/config.service";
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
import {
capitalizeFirstLetter,
configVariableToFriendlyName,
} from "../../../utils/string.util";
import toast from "../../../utils/toast.util";
export default function AppShellDemo() {
const theme = useMantineTheme();
const router = useRouter();
const [isMobileNavBarOpened, setIsMobileNavBarOpened] = useState(false);
const isMobile = useMediaQuery("(max-width: 560px)");
const config = useConfig();
const categoryId = router.query.category as string;
const [configVariables, setConfigVariables] = useState<AdminConfig[]>();
const [updatedConfigVariables, setUpdatedConfigVariables] = useState<
UpdateConfig[]
>([]);
const saveConfigVariables = async () => {
await configService
.updateMany(updatedConfigVariables)
.then(() => {
setUpdatedConfigVariables([]);
toast.success("Configurations updated successfully");
})
.catch(toast.axiosError);
config.refresh();
};
const updateConfigVariable = (configVariable: UpdateConfig) => {
const index = updatedConfigVariables.findIndex(
(item) => item.key === configVariable.key
);
if (index > -1) {
updatedConfigVariables[index] = configVariable;
} else {
setUpdatedConfigVariables([...updatedConfigVariables, configVariable]);
}
};
useEffect(() => {
configService.getByCategory(categoryId).then((configVariables) => {
setConfigVariables(configVariables);
});
}, [categoryId]);
return (
<>
<Meta title="Configuration" />
<AppShell
styles={{
main: {
background:
theme.colorScheme === "dark"
? theme.colors.dark[8]
: theme.colors.gray[0],
},
}}
navbar={
<ConfigurationNavBar
categoryId={categoryId}
isMobileNavBarOpened={isMobileNavBarOpened}
setIsMobileNavBarOpened={setIsMobileNavBarOpened}
/>
}
header={
<ConfigurationHeader
isMobileNavBarOpened={isMobileNavBarOpened}
setIsMobileNavBarOpened={setIsMobileNavBarOpened}
/>
}
>
<Container size="lg">
{!configVariables ? (
<CenterLoader />
) : (
<>
<Stack>
<Title mb="md" order={3}>
{capitalizeFirstLetter(categoryId)}
</Title>
{configVariables.map((configVariable) => (
<Group key={configVariable.key} position="apart">
<Stack
style={{ maxWidth: isMobile ? "100%" : "40%" }}
spacing={0}
>
<Title order={6}>
{configVariableToFriendlyName(configVariable.name)}
</Title>
<Text color="dimmed" size="sm" mb="xs">
{configVariable.description}
</Text>
</Stack>
<Stack></Stack>
<Box style={{ width: isMobile ? "100%" : "50%" }}>
<AdminConfigInput
key={configVariable.key}
configVariable={configVariable}
updateConfigVariable={updateConfigVariable}
/>
</Box>
</Group>
))}
</Stack>
<Group mt="lg" position="right">
{categoryId == "smtp" && (
<TestEmailButton
configVariablesChanged={updatedConfigVariables.length != 0}
saveConfigVariables={saveConfigVariables}
/>
)}
<Button onClick={saveConfigVariables}>Save</Button>
</Group>
</>
)}
</Container>
</AppShell>
</>
);
}

View File

@@ -0,0 +1,15 @@
export function getServerSideProps() {
return {
redirect: {
permanent: false,
destination: "/admin/config/general",
},
props: {},
};
}
const Config = () => {
return null;
};
export default Config;

View File

@@ -0,0 +1,59 @@
import {
Anchor,
Button,
Center,
Container,
Stack,
Text,
Title,
} from "@mantine/core";
import Link from "next/link";
import Logo from "../../components/Logo";
import Meta from "../../components/Meta";
const Intro = () => {
return (
<>
<Meta title="Intro" />
<Container size="xs">
<Stack>
<Center>
<Logo height={80} width={80} />
</Center>
<Center>
<Title order={2}>Welcome to Pingvin Share</Title>
</Center>
<Text>
If you enjoy Pingvin Share please it on{" "}
<Anchor
target="_blank"
href="https://github.com/stonith404/pingvin-share"
>
GitHub
</Anchor>{" "}
or{" "}
<Anchor
target="_blank"
href="https://github.com/sponsors/stonith404"
>
buy me a coffee
</Anchor>{" "}
if you want to support my work.
</Text>
<Text>Enough talked, have fun with Pingvin Share!</Text>
<Text mt="lg">How to you want to continue?</Text>
<Stack>
<Button href="/admin/config" component={Link}>
Customize configuration
</Button>
<Button href="/" component={Link} variant="light">
Explore Pingvin Share
</Button>
</Stack>
</Stack>
</Container>
</>
);
};
export default Intro;

View File

@@ -1,23 +0,0 @@
import { Box, Stack, Text, Title } from "@mantine/core";
import AdminConfigTable from "../../components/admin/configuration/AdminConfigTable";
import Logo from "../../components/Logo";
import Meta from "../../components/Meta";
const Setup = () => {
return (
<>
<Meta title="Setup" />
<Stack align="center">
<Logo height={80} width={80} />
<Title order={2}>Welcome to Pingvin Share</Title>
<Text>Let's customize Pingvin Share for you! </Text>
<Box style={{ width: "100%" }}>
<AdminConfigTable />
</Box>
</Stack>
</>
);
};
export default Setup;

View File

@@ -58,7 +58,7 @@ const Users = () => {
</Title>
<Button
onClick={() =>
showCreateUserModal(modals, config.get("SMTP_ENABLED"), getUsers)
showCreateUserModal(modals, config.get("smtp.enabled"), getUsers)
}
leftIcon={<TbPlus size={20} />}
>

View File

@@ -11,7 +11,7 @@ export const config = {
export default (req: NextApiRequest, res: NextApiResponse) => {
return httpProxyMiddleware(req, res, {
headers: {
"X-Forwarded-For": req.socket.remoteAddress ?? "",
"X-Forwarded-For": req.socket?.remoteAddress ?? "",
},
target: "http://localhost:8080",
});

View File

@@ -51,7 +51,6 @@ const ResetPassword = () => {
<Paper withBorder shadow="md" p={30} radius="md" mt="xl">
<form
onSubmit={form.onSubmit((values) => {
console.log(resetPasswordToken);
authService
.resetPassword(resetPasswordToken, values.password)
.then(() => {

View File

@@ -8,11 +8,11 @@ import {
ThemeIcon,
Title,
} from "@mantine/core";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { TbCheck } from "react-icons/tb";
import Logo from "../components/Logo";
import Meta from "../components/Meta";
import useUser from "../hooks/user.hook";
@@ -150,12 +150,7 @@ export default function Home() {
</Group>
</div>
<Group className={classes.image} align="center">
<Image
src="/img/logo.svg"
alt="Pingvin Share Logo"
width={200}
height={200}
/>
<Logo width={200} height={200} />
</Group>
</div>
</Container>

View File

@@ -35,7 +35,7 @@ const Upload = ({
const [files, setFiles] = useState<FileUpload[]>([]);
const [isUploading, setisUploading] = useState(false);
maxShareSize ??= parseInt(config.get("MAX_SHARE_SIZE"));
maxShareSize ??= parseInt(config.get("share.maxSize"));
const uploadFiles = async (share: CreateShare) => {
setisUploading(true);
@@ -146,7 +146,7 @@ const Upload = ({
.completeShare(createdShare.id)
.then((share) => {
setisUploading(false);
showCompletedUploadModal(modals, share, config.get("APP_URL"));
showCompletedUploadModal(modals, share, config.get("general.appUrl"));
setFiles([]);
})
.catch(() =>
@@ -168,9 +168,9 @@ const Upload = ({
{
isUserSignedIn: user ? true : false,
isReverseShare,
appUrl: config.get("APP_URL"),
appUrl: config.get("general.appUrl"),
allowUnauthenticatedShares: config.get(
"ALLOW_UNAUTHENTICATED_SHARES"
"share.allowUnauthenticatedShares"
),
enableEmailRecepients: config.get(
"ENABLE_SHARE_EMAIL_RECIPIENTS"

View File

@@ -6,8 +6,8 @@ const list = async (): Promise<Config[]> => {
return (await api.get("/configs")).data;
};
const listForAdmin = async (): Promise<AdminConfig[]> => {
return (await api.get("/configs/admin")).data;
const getByCategory = async (category: string): Promise<AdminConfig[]> => {
return (await api.get(`/configs/admin/${category}`)).data;
};
const updateMany = async (data: UpdateConfig[]): Promise<AdminConfig[]> => {
@@ -48,7 +48,7 @@ const isNewReleaseAvailable = async () => {
export default {
list,
listForAdmin,
getByCategory,
updateMany,
get,
finishSetup,

View File

@@ -10,11 +10,11 @@ export type UpdateConfig = {
};
export type AdminConfig = Config & {
name: string;
updatedAt: Date;
secret: boolean;
description: string;
obscured: boolean;
category: string;
};
export type AdminConfigGroupedByCategory = {
@@ -29,6 +29,11 @@ export type AdminConfigGroupedByCategory = {
];
};
export type ConfigVariablesCategory = {
category: string;
count: number;
};
export type ConfigHook = {
configVariables: Config[];
refresh: () => void;

View File

@@ -1,8 +1,6 @@
export const configVariableToFriendlyName = (variable: string) => {
return variable
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
const splitted = variable.split(/(?=[A-Z])/).join(" ");
return splitted.charAt(0).toUpperCase() + splitted.slice(1);
};
export const capitalizeFirstLetter = (string: string) => {