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
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
BIN
frontend/public/img/logo.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
148
frontend/src/pages/admin/config/[category].tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
15
frontend/src/pages/admin/config/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export function getServerSideProps() {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/admin/config/general",
|
||||
},
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
|
||||
const Config = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Config;
|
||||
59
frontend/src/pages/admin/intro.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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} />}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||