From df1ffaa2bcc047668cdc207cf8f86d821778cf44 Mon Sep 17 00:00:00 2001 From: Aaron <42084688+aarondoet@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:29:01 +0100 Subject: [PATCH] feat: add legal page with configuration options (#724) * Addconfig entries for legal notice * Add legal route handling to middleware * Make legal notice public * Add legal category to config sidebar * Add legal notice page * Add German translations for legal notice and configuration options * Replace legal page with separate imprint and privacy pages * Update middleware * Add footer component * Update legal text descriptions to indicate Markdown support again * Refactor footer layout * Add zIndex to footer component * improve mobile layout * run formatter --------- Co-authored-by: Elias Schneider --- backend/prisma/seed/config.seed.ts | 27 ++++++++ backend/src/config/config.service.ts | 3 +- .../configuration/ConfigurationNavBar.tsx | 2 + .../src/components/core/FileSizeInput.tsx | 17 +++-- frontend/src/components/footer/Footer.tsx | 62 +++++++++++++++++++ frontend/src/i18n/translations/de-DE.ts | 17 +++++ frontend/src/i18n/translations/en-US.ts | 20 ++++++ frontend/src/middleware.ts | 33 +++++++++- frontend/src/pages/_app.tsx | 18 ++++-- frontend/src/pages/imprint/index.tsx | 55 ++++++++++++++++ frontend/src/pages/privacy/index.tsx | 55 ++++++++++++++++ frontend/src/services/config.service.ts | 3 +- 12 files changed, 297 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/footer/Footer.tsx create mode 100644 frontend/src/pages/imprint/index.tsx create mode 100644 frontend/src/pages/privacy/index.tsx 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) { ) : ( <> -
- - - + +
+
+ + + +
+