diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts
index 4cf3647..78b945d 100644
--- a/backend/prisma/seed/config.seed.ts
+++ b/backend/prisma/seed/config.seed.ts
@@ -349,6 +349,33 @@ const configVariables: ConfigVariables = {
defaultValue: "",
obscured: true,
},
+ },
+ legal: {
+ enabled: {
+ type: "boolean",
+ defaultValue: "false",
+ secret: false,
+ },
+ imprintText: {
+ type: "text",
+ defaultValue: "",
+ secret: false,
+ },
+ imprintUrl: {
+ type: "string",
+ defaultValue: "",
+ secret: false,
+ },
+ privacyPolicyText: {
+ type: "text",
+ defaultValue: "",
+ secret: false,
+ },
+ privacyPolicyUrl: {
+ type: "string",
+ defaultValue: "",
+ secret: false,
+ },
}
};
diff --git a/backend/src/config/config.service.ts b/backend/src/config/config.service.ts
index 29e2e51..5e70be6 100644
--- a/backend/src/config/config.service.ts
+++ b/backend/src/config/config.service.ts
@@ -32,8 +32,7 @@ export class ConfigService extends EventEmitter {
if (configVariable.type == "number" || configVariable.type == "filesize")
return parseInt(value);
- if (configVariable.type == "boolean")
- return value == "true";
+ if (configVariable.type == "boolean") return value == "true";
if (configVariable.type == "string" || configVariable.type == "text")
return value;
}
diff --git a/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx b/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx
index 0621906..369d726 100644
--- a/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx
+++ b/frontend/src/components/admin/configuration/ConfigurationNavBar.tsx
@@ -19,6 +19,7 @@ import {
TbBucket,
TbBinaryTree,
TbSettings,
+ TbScale,
} from "react-icons/tb";
import { FormattedMessage } from "react-intl";
@@ -30,6 +31,7 @@ const categories = [
{ name: "OAuth", icon: },
{ name: "LDAP", icon: },
{ name: "S3", icon: },
+ { name: "Legal", icon: },
];
const useStyles = createStyles((theme) => ({
diff --git a/frontend/src/components/core/FileSizeInput.tsx b/frontend/src/components/core/FileSizeInput.tsx
index edc2915..929e0da 100644
--- a/frontend/src/components/core/FileSizeInput.tsx
+++ b/frontend/src/components/core/FileSizeInput.tsx
@@ -11,12 +11,16 @@ const multipliers = {
GiB: 1024 ** 3,
TB: 1000 ** 4,
TiB: 1024 ** 4,
-}
+};
-const units = (["B", "KB", "KiB", "MB", "MiB", "GB", "GiB", "TB", "TiB"] as const).map(unit => ({ label: unit, value: unit }));
+const units = (
+ ["B", "KB", "KiB", "MB", "MiB", "GB", "GiB", "TB", "TiB"] as const
+).map((unit) => ({ label: unit, value: unit }));
function getLargestApplicableUnit(value: number) {
- return units.findLast(unit => value % multipliers[unit.value] === 0) || units[0];
+ return (
+ units.findLast((unit) => value % multipliers[unit.value] === 0) || units[0]
+ );
}
const FileSizeInput = ({
@@ -46,8 +50,9 @@ const FileSizeInput = ({
marginRight: -2,
},
}}
- onChange={event => {
- const unit = event.currentTarget.value as typeof units[number]["value"];
+ onChange={(event) => {
+ const unit = event.currentTarget
+ .value as (typeof units)[number]["value"];
setUnit(unit);
onChange(multipliers[unit] * inputValue);
}}
@@ -63,7 +68,7 @@ const FileSizeInput = ({
precision={0}
rightSection={unitSelect}
rightSectionWidth={76}
- onChange={value => {
+ onChange={(value) => {
const inputVal = value || 0;
setInputValue(inputVal);
onChange(multipliers[unit] * inputVal);
diff --git a/frontend/src/components/footer/Footer.tsx b/frontend/src/components/footer/Footer.tsx
new file mode 100644
index 0000000..db13622
--- /dev/null
+++ b/frontend/src/components/footer/Footer.tsx
@@ -0,0 +1,62 @@
+import { Anchor, Footer as MFooter, SimpleGrid, Text } from "@mantine/core";
+import { useMediaQuery } from "@mantine/hooks";
+import useConfig from "../../hooks/config.hook";
+import useTranslate from "../../hooks/useTranslate.hook";
+
+const Footer = () => {
+ const t = useTranslate();
+ const config = useConfig();
+ const hasImprint = !!(
+ config.get("legal.imprintUrl") || config.get("legal.imprintText")
+ );
+ const hasPrivacy = !!(
+ config.get("legal.privacyPolicyUrl") ||
+ config.get("legal.privacyPolicyText")
+ );
+ const imprintUrl =
+ (!config.get("legal.imprintText") && config.get("legal.imprintUrl")) ||
+ "/imprint";
+ const privacyUrl =
+ (!config.get("legal.privacyPolicyText") &&
+ config.get("legal.privacyPolicyUrl")) ||
+ "/privacy";
+
+ const isMobile = useMediaQuery("(max-width: 700px)");
+
+ return (
+
+
+ {!isMobile && }
+
+ Powered by{" "}
+
+ Pingvin Share
+
+
+
+ {config.get("legal.enabled") && (
+
+ {hasImprint && (
+
+ {t("imprint.title")}
+
+ )}
+ {hasImprint && hasPrivacy && " • "}
+ {hasPrivacy && (
+
+ {t("privacy.title")}
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default Footer;
diff --git a/frontend/src/i18n/translations/de-DE.ts b/frontend/src/i18n/translations/de-DE.ts
index c51bd63..db0e62a 100644
--- a/frontend/src/i18n/translations/de-DE.ts
+++ b/frontend/src/i18n/translations/de-DE.ts
@@ -295,6 +295,12 @@ export default {
"share.edit.notify.generic-error": "Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
"share.edit.notify.save-success": "Freigabe erfolgreich aktualisiert",
// END /share/[id]/edit
+ // /imprint
+ "imprint.title": "Imprint",
+ // END /imprint
+ // /privacy
+ "privacy.title": "Privacy Policy",
+ // END /privacy
// /admin/config
"admin.config.title": "Einstellungen",
"admin.config.category.general": "Allgemein",
@@ -457,6 +463,17 @@ export default {
"admin.config.s3.key.description": "Der Schlüssel, der den Zugriff auf den S3-Bucket ermöglicht.",
"admin.config.s3.secret": "Geheimnis",
"admin.config.s3.secret.description": "Das Geheimnis, das den Zugriff auf den S3-Bucket ermöglicht.",
+ "admin.config.category.legal": "Rechtliches",
+ "admin.config.legal.enabled": "Impressum und Datenschutz aktivieren",
+ "admin.config.legal.enabled.description": "Gibt an, ob die Links zum Impressum und zur Datenschutzerklärung im Footer angezeigt werden sollen.",
+ "admin.config.legal.imprint-text": "Impressum-Text",
+ "admin.config.legal.imprint-text.description": "Der Text, der im Impressum angezeigt wird. Unterstützt Markdown. Leer lassen, um auf eine externe Impressumsseite zu verlinken.",
+ "admin.config.legal.imprint-url": "Impressum-URL",
+ "admin.config.legal.imprint-url.description": "Wenn bereits eine Impressumsseite vorhanden ist, kann sie hier verlinkt werden, anstatt den Text einzugeben.",
+ "admin.config.legal.privacy-policy-text": "Datenschutzerklärungstext",
+ "admin.config.legal.privacy-policy-text.description": "Der Text, der in der Datenschutzerklärung angezeigt wird. Unterstützt Markdown. Leer lassen, um auf eine externe Datenschutzerklärungsseite zu verlinken.",
+ "admin.config.legal.privacy-policy-url": "Datenschutzerklärungs-URL",
+ "admin.config.legal.privacy-policy-url.description": "Wenn bereits eine Datenschutzerklärungsseite vorhanden ist, kann sie hier verlinkt werden, anstatt den Text einzugeben.",
// 404
"404.description": "Ups, diese Seite existiert nicht.",
"404.button.home": "Zurück zur Startseite",
diff --git a/frontend/src/i18n/translations/en-US.ts b/frontend/src/i18n/translations/en-US.ts
index 04b2ac2..991441f 100644
--- a/frontend/src/i18n/translations/en-US.ts
+++ b/frontend/src/i18n/translations/en-US.ts
@@ -405,6 +405,14 @@ export default {
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
+ // /imprint
+ "imprint.title": "Imprint",
+ // END /imprint
+
+ // /privacy
+ "privacy.title": "Privacy Policy",
+ // END /privacy
+
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
@@ -645,6 +653,18 @@ export default {
"admin.config.s3.secret": "Secret",
"admin.config.s3.secret.description": "The secret which allows you to access the S3 bucket.",
+ "admin.config.category.legal": "Legal",
+ "admin.config.legal.enabled": "Enable legal notices",
+ "admin.config.legal.enabled.description": "Whether to show a link to imprint and privacy policy in the footer.",
+ "admin.config.legal.imprint-text": "Imprint text",
+ "admin.config.legal.imprint-text.description": "The text which should be shown in the imprint. Supports Markdown. Leave blank to link to an external imprint page.",
+ "admin.config.legal.imprint-url": "Imprint URL",
+ "admin.config.legal.imprint-url.description": "If you already have an imprint page you can link it here instead of using the text field.",
+ "admin.config.legal.privacy-policy-text": "Privacy policy text",
+ "admin.config.legal.privacy-policy-text.description": "The text which should be shown in the privacy policy. Supports Markdown. Leave blank to link to an external privacy policy page.",
+ "admin.config.legal.privacy-policy-url": "Privacy policy URL",
+ "admin.config.legal.privacy-policy-url.description": "If you already have a privacy policy page you can link it here instead of using the text field.",
+
// 404
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Bring me back home",
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
index e34827d..bdb6f0b 100644
--- a/frontend/src/middleware.ts
+++ b/frontend/src/middleware.ts
@@ -14,7 +14,14 @@ export const config = {
export async function middleware(request: NextRequest) {
const routes = {
unauthenticated: new Routes(["/auth/*", "/"]),
- public: new Routes(["/share/*", "/s/*", "/upload/*", "/error"]),
+ public: new Routes([
+ "/share/*",
+ "/s/*",
+ "/upload/*",
+ "/error",
+ "/imprint",
+ "/privacy",
+ ]),
admin: new Routes(["/admin/*"]),
account: new Routes(["/account*"]),
disabled: new Routes([]),
@@ -55,6 +62,20 @@ export async function middleware(request: NextRequest) {
routes.disabled.routes.push("/auth/resetPassword*");
}
+ if (!getConfig("legal.enabled")) {
+ routes.disabled.routes.push("/imprint", "/privacy");
+ } else {
+ if (!getConfig("legal.imprintText") && !getConfig("legal.imprintUrl")) {
+ routes.disabled.routes.push("/imprint");
+ }
+ if (
+ !getConfig("legal.privacyPolicyText") &&
+ !getConfig("legal.privacyPolicyUrl")
+ ) {
+ routes.disabled.routes.push("/privacy");
+ }
+ }
+
// prettier-ignore
const rules = [
// Disabled routes
@@ -86,6 +107,16 @@ export async function middleware(request: NextRequest) {
condition: (!getConfig("general.showHomePage") || user) && route == "/",
path: "/upload",
},
+ // Imprint redirect
+ {
+ condition: route == "/imprint" && !getConfig("legal.imprintText") && getConfig("legal.imprintUrl"),
+ path: getConfig("legal.imprintUrl"),
+ },
+ // Privacy redirect
+ {
+ condition: route == "/privacy" && !getConfig("legal.privacyPolicyText") && getConfig("legal.privacyPolicyUrl"),
+ path: getConfig("legal.privacyPolicyUrl"),
+ },
];
for (const rule of rules) {
if (rule.condition) {
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index e7674eb..a1c1ee7 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -3,6 +3,7 @@ import {
ColorSchemeProvider,
Container,
MantineProvider,
+ Stack,
} from "@mantine/core";
import { useColorScheme } from "@mantine/hooks";
import { ModalsProvider } from "@mantine/modals";
@@ -30,6 +31,7 @@ import Config from "../types/config.type";
import { CurrentUser } from "../types/user.type";
import i18nUtil from "../utils/i18n.util";
import userPreferences from "../utils/userPreferences.util";
+import Footer from "../components/footer/Footer";
const excludeDefaultLayoutRoutes = ["/admin/config/[category]"];
@@ -134,10 +136,18 @@ function App({ Component, pageProps }: AppProps) {
) : (
<>
-
-
-
-
+
+
+
+
+
+
+
+
+
>
)}
diff --git a/frontend/src/pages/imprint/index.tsx b/frontend/src/pages/imprint/index.tsx
new file mode 100644
index 0000000..906ba82
--- /dev/null
+++ b/frontend/src/pages/imprint/index.tsx
@@ -0,0 +1,55 @@
+import { Anchor, Title, useMantineTheme } from "@mantine/core";
+import Meta from "../../components/Meta";
+import useTranslate from "../../hooks/useTranslate.hook";
+import { FormattedMessage } from "react-intl";
+import useConfig from "../../hooks/config.hook";
+import Markdown from "markdown-to-jsx";
+
+const Imprint = () => {
+ const t = useTranslate();
+ const { colorScheme } = useMantineTheme();
+ const config = useConfig();
+ return (
+ <>
+
+
+
+
+
+ {config.get("legal.imprintText")}
+
+ >
+ );
+};
+
+export default Imprint;
diff --git a/frontend/src/pages/privacy/index.tsx b/frontend/src/pages/privacy/index.tsx
new file mode 100644
index 0000000..c2cb714
--- /dev/null
+++ b/frontend/src/pages/privacy/index.tsx
@@ -0,0 +1,55 @@
+import { Anchor, Title, useMantineTheme } from "@mantine/core";
+import Meta from "../../components/Meta";
+import useTranslate from "../../hooks/useTranslate.hook";
+import { FormattedMessage } from "react-intl";
+import useConfig from "../../hooks/config.hook";
+import Markdown from "markdown-to-jsx";
+
+const PrivacyPolicy = () => {
+ const t = useTranslate();
+ const { colorScheme } = useMantineTheme();
+ const config = useConfig();
+ return (
+ <>
+
+
+
+
+
+ {config.get("legal.privacyPolicyText")}
+
+ >
+ );
+};
+
+export default PrivacyPolicy;
diff --git a/frontend/src/services/config.service.ts b/frontend/src/services/config.service.ts
index f7905d3..a518604 100644
--- a/frontend/src/services/config.service.ts
+++ b/frontend/src/services/config.service.ts
@@ -27,8 +27,7 @@ const get = (key: string, configVariables: Config[]): any => {
if (configVariable.type == "number" || configVariable.type == "filesize")
return parseInt(value);
- if (configVariable.type == "boolean")
- return value == "true";
+ if (configVariable.type == "boolean") return value == "true";
if (configVariable.type == "string" || configVariable.type == "text")
return value;
};