feat(whatWeKnowAboutYou): finished whatWeKnowAboutYou Seite
Build and Push Docker image / build-and-push (push) Successful in 5m53s Details

This commit is contained in:
Valentin Kolb 2024-11-04 23:10:19 +01:00
parent 29c52d2638
commit d605d17d06
5 changed files with 357 additions and 238 deletions

View File

@ -33,7 +33,7 @@ export default function Analytics() {
<div className={"group"}>
<Button
component={Link}
to={"/legal/privacy-policy"}
to={"/util/whatWeKnowAboutYou"}
size={"xs"}
color={"gray"}
>

View File

@ -0,0 +1,235 @@
import {Alert, Anchor, Button, Center, Collapse, SimpleGrid, Text, TextInput, Title} from "@mantine/core";
import {IconBug, IconEraser, IconGlobe, IconId, IconSlash, IconWorldWww} from "@tabler/icons-react";
import {
BrowserIcon,
DeviceTypeIcon,
OsIcon
} from "@/pages/admin/AnalyticsDashboard/AnalyticsSessions/AnalyticsSessionRow.tsx";
import DataItem from "@/pages/util/whatWeKnowAboutYou/DataItem.tsx";
import {useLocation} from "react-router-dom";
import {useCookies} from "react-cookie";
import {ANALYTICS_COOKIE_NAME, ANALYTICS_IP_API} from "../../../../config.ts";
import {UAParser} from "ua-parser-js";
import {usePreferredLanguage} from "@uidotdev/usehooks";
import {useMutation, useQuery} from "@tanstack/react-query";
import {ofetch} from "ofetch";
import {AnalyticsIpApiResult} from "@/models/AnalyticsTypes.ts";
import {usePB} from "@/lib/pocketbase.tsx";
import {pprintDate} from "@/lib/datetime.ts";
import {useDisclosure} from "@mantine/hooks";
import {hasLength, useForm} from "@mantine/form";
import {showErrorNotification, showSuccessNotification} from "@/components/util.tsx";
const DeleteDataRequest = () => {
const [cookies, , removeCookie] = useCookies([ANALYTICS_COOKIE_NAME])
const [showDialog, handler] = useDisclosure(false)
const {pb} = usePB()
const formValues = useForm({
initialValues: {
visitorId: ""
},
validate: {
visitorId: hasLength({min: 15, max: 15}, "Bitte gib eine gültige Analyse-ID ein")
}
})
const deleteDataMutation = useMutation({
mutationFn: async (visitorId: string) => {
await pb.collection("analyticsVisitors").delete(visitorId, {
credentials: "omit"
})
return visitorId
},
onSuccess: (visitorId) => {
if (cookies[ANALYTICS_COOKIE_NAME] === visitorId) {
removeCookie(ANALYTICS_COOKIE_NAME, {path: "/"})
}
formValues.reset()
showSuccessNotification(`Die Analysedaten wurden gelöscht.`)
},
onError: () => {
showErrorNotification(`Die Analysedaten konnten nicht gelöscht werden.`)
}
})
return <>
<Center className={"stack"}>
<Anchor
variant={"outline"}
component={"button"}
size={"sm"}
onClick={() => {
handler.toggle()
formValues.reset()
}}
>
Löschung von Daten beantragen
</Anchor>
<Collapse in={showDialog}>
<form className={"group center"} onSubmit={formValues.onSubmit((val) => {
deleteDataMutation.mutate(val.visitorId)
})}>
<TextInput
placeholder={"Analyse-ID"}
leftSection={<IconId/>}
{...formValues.getInputProps("visitorId")}
/>
<Button
type={"submit"}
color={"red"}
size={"compact-md"}
leftSection={<IconEraser/>}
>
Daten löschen
</Button>
</form>
</Collapse>
</Center>
</>
}
export default function CollectedAnalyticsData() {
const {pb} = usePB()
const location = useLocation()
const [cookies] = useCookies([ANALYTICS_COOKIE_NAME])
const cookieValue = cookies[ANALYTICS_COOKIE_NAME]
const parserResult = new UAParser().getResult()
const preferredLanguage = usePreferredLanguage()
const ipQuery = useQuery({
queryKey: ['ip'],
queryFn: async () => {
return await ofetch<AnalyticsIpApiResult>(ANALYTICS_IP_API, {ignoreResponseError: true})
}
})
const visitorIdQuery = useQuery({
queryKey: ['visitorId', cookieValue],
queryFn: async () => {
const res = await pb.collection("analyticsVisitors").getOne(cookieValue)
return {
activeSince: res?.created,
}
},
enabled: !!cookieValue
})
return (
<div className={"section-transparent stack"}>
<Center mt={"xl"}>
<Title order={2}>Anonyme Analysedaten</Title>
</Center>
<Center>
<Text ta={"center"} c={"dimmed"}>
Alle Analysedaten, die wir sammeln, sind anonymisiert und dienen dazu, diesen Dienst zu
verbessern
und zu personalisieren.
<br/>
Wir geben keine Daten an Dritte weiter. Die Daten liegen auf unseren eigenen Servern in
Deutschland.
<br/>
Nur Systemadministratoren haben Zugriff auf die Daten. Wir sammeln diese Daten erst, nachdem
Cookies
akzeptiert wurden.
</Text>
</Center>
<DeleteDataRequest/>
{!cookieValue && (
<Alert ta={"center"} fw={"600"} color={"green"}>
Solange du keine Cookies akzeptierst, sammeln wir keine Analysedaten.
</Alert>
)}
<SimpleGrid cols={{base: 1, sm: 2, md: 3}}>
{
[
{
icon: IconId,
title: "Analyse-ID",
data: cookieValue ?? "-",
description: <>
Du bist mit dieser ID aktiv
seit {pprintDate(visitorIdQuery.data?.activeSince ?? "")}.
Seit diesem Datum sammeln wir Analysedaten verknüpft mit dieser ID.
<br/>
Auf Basis dieser ID können wir die gespeicherten Analysedaten aggregieren und
analysieren.
</>
},
{
icon: IconSlash,
title: "Aktuelle Seite",
data: location.pathname,
description: "Der Pfad der aktuellen Seite, die du besuchst. Wir speichern diese Pfade um zu analysieren, welche Seiten am häufigsten besucht werden und wie Nutzende navigieren."
},
{
icon: IconBug,
title: "Fehler",
data: "-",
description: "Falls ein Fehler auftritt speichern wir die Fehlermeldung und den Zeitpunkt und dem entsprechenden Pfad, um die Stabilität der Seite zu verbessern."
},
{
icon: IconWorldWww,
title: "IP Adresse",
data: ipQuery.data?.ip || "-",
description: <>
Deine IP Adresse wird genutzt, um das Land zu bestimmen, aus dem du die Seite
besuchst.
<br/>
Folgende Daten können darüber hinaus anhand deiner IP Adresse bestimmt werden, <b>werden
aber nicht
gespeichert</b>:
Bundesland, Stadt, ungefährer Standort, Internetanbieter.
</>
},
{
icon: IconGlobe,
title: "Land",
data: ipQuery.data?.country_code || "-",
description: "Das Land, aus dem du die Seite besuchst. Diese Information wird genutzt, um die Seite zu personalisieren und um zu verstehen, woher die Nutzenden kommen."
},
{
icon: () => <BrowserIcon browser={parserResult.browser.name ?? ""}/>,
title: "Browser",
data: `${parserResult.browser.name} ${parserResult.browser.version}`,
description: "Dein Browser wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
},
{
icon: () => <OsIcon os={parserResult.os.name ?? ""}/>,
title: "Betriebssystem",
data: `${parserResult.os.name} ${parserResult.os.version}`,
description: "Dein Betriebssystem wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
},
{
icon: IconWorldWww,
title: "Bevorzugte Sprache",
data: preferredLanguage || "-",
description: "Deine bevorzugte Sprache wird genutzt, um zu verstehen, welche Sprachen Nutzende sprechen."
},
{
icon: () => <DeviceTypeIcon deviceType={parserResult.device.type ?? "desktop"}/>,
title: "Gerätetyp",
data: parserResult.device.type || "desktop",
description: "Dein Gerätetyp wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
}
].map((feature, index) => (
<DataItem key={index} {...feature}/>
))
}
</SimpleGrid>
</div>
)
}

View File

@ -0,0 +1,84 @@
import {usePB} from "@/lib/pocketbase.tsx";
import {Center, SimpleGrid, Text, Title} from "@mantine/core";
import {IconCalendarClock, IconGlobe, IconPassword, IconSignature, IconUser, IconUsers} from "@tabler/icons-react";
import {getUserName} from "@/components/users/modals/util.tsx";
import {LdapGroupModel} from "@/models/AuthTypes.ts";
import DataItem from "@/pages/util/whatWeKnowAboutYou/DataItem.tsx";
export default function CollectedUserData() {
const {user} = usePB()
if (!user) return null
return (
<div className={"section-transparent stack"}>
<Center mt={"xl"}>
<Title order={2}>Accountdaten</Title>
</Center>
<Center>
<Text ta={"center"} c={"dimmed"}>
Wenn du einen Account hast speichern wir zusätzlich zu den anonymen Analysedaten auch deine
Accountdaten.
</Text>
</Center>
<SimpleGrid
cols={{base: 1, sm: 2, md: 3}}
>
{
[
{
icon: IconUser,
title: "Anmeldename und Realm",
data: `${user.username} (${user.REALM})`,
description: "Hiermit kannst du dich anmelden und deine Daten verwalten. Deine Realm wird genutzt, um zu bestimmen ob du einen StuVe-IT oder Gast-Account hast."
},
{
icon: IconPassword,
title: "Passwort",
data: '***********',
description: "Dein Passwort wird verschlüsselt auf unseren Servern gespeichert und kann nicht eingesehen werden."
},
{
icon: IconGlobe,
title: "E-Mail",
data: user.email,
description: "Deine E-Mail Adresse wird genutzt, um dich zu kontaktieren. Andere Nutzende können deine E-Mail Adresse nicht sehen."
},
{
icon: IconSignature,
title: "Name",
data: getUserName(user),
description: "Dein Name wird genutzt, um dich zu identifizieren. Andere Nutzende können deinen Namen sehen."
},
{
icon: IconCalendarClock,
title: "Account Ablaufdatum",
data: user?.accountExpires ? (
new Date(user?.accountExpires).getTime() > Date.now() ? (
"Account ist aktiv und läuft am " + new Date(user?.accountExpires).toLocaleDateString() + " ab"
) : (
"Account ist abgelaufen"
)
) : (
"Dein Account läuft nicht ab"
),
description: "Dein Account-Ablaufdatum wird genutzt, um zu bestimmen, ob dein Account noch aktiv ist."
},
{
icon: IconUsers,
title: "Gruppen",
data: user?.expand?.memberOf.map((group: LdapGroupModel) => group.cn).join(", ") || "Keine Gruppen",
description: "Dein Account-Ablaufdatum wird genutzt, um zu bestimmen, ob dein Account noch aktiv ist."
},
].map((feature, index) => (
<DataItem key={index} {...feature}/>
))
}
</SimpleGrid>
</div>
)
}

View File

@ -0,0 +1,31 @@
import {FC, ReactNode} from "react";
import {Code, rem, Text, ThemeIcon} from "@mantine/core";
interface FeatureProps {
// eslint-disable-next-line
icon: FC<any>;
data: ReactNode;
title: ReactNode;
description: ReactNode;
}
export default function DataItem({icon: Icon, data, title, description}: FeatureProps) {
return (
<div className={"paper"}>
<ThemeIcon variant="light" size={40} radius={40}>
<Icon style={{width: rem(18), height: rem(18)}} stroke={1.5}/>
</ThemeIcon>
<Text mt="sm" mb={7}>
{title}
</Text>
<Text mt="sm" mb={7}>
<Code>
{data}
</Code>
</Text>
<Text size="sm" c="dimmed" lh={1.6}>
{description}
</Text>
</div>
)
}

View File

@ -1,81 +1,11 @@
import {Alert, Anchor, Center, Code, rem, SimpleGrid, Text, ThemeIcon, Title} from "@mantine/core";
import {usePB} from "@/lib/pocketbase.tsx";
import {FC, ReactNode} from "react";
import {
IconBug,
IconCalendarClock,
IconGlobe,
IconId,
IconPassword,
IconSignature,
IconSlash,
IconUser,
IconUsers,
IconWorldWww
} from "@tabler/icons-react";
import {useCookies} from "react-cookie";
import {ANALYTICS_COOKIE_NAME, ANALYTICS_IP_API} from "../../../../config.ts";
import {Link, useLocation} from "react-router-dom";
import {ofetch} from "ofetch";
import {AnalyticsIpApiResult} from "@/models/AnalyticsTypes.ts";
import {useQuery} from "@tanstack/react-query";
import {UAParser} from "ua-parser-js";
import {
BrowserIcon,
DeviceTypeIcon,
OsIcon
} from "@/pages/admin/AnalyticsDashboard/AnalyticsSessions/AnalyticsSessionRow.tsx";
import {usePreferredLanguage} from "@uidotdev/usehooks";
import {getUserName} from "@/components/users/modals/util.tsx";
import {LdapGroupModel} from "@/models/AuthTypes.ts";
import {Anchor, Center, Title} from "@mantine/core";
import {Link} from "react-router-dom";
import CollectedAnalyticsData from "@/pages/util/whatWeKnowAboutYou/CollectedAnalyticsData.tsx";
import CollectedUserData from "@/pages/util/whatWeKnowAboutYou/CollectedUserData.tsx";
interface FeatureProps {
icon: FC<any>;
data: ReactNode;
title: ReactNode;
description: ReactNode;
}
function DataItem({icon: Icon, data, title, description}: FeatureProps) {
return (
<div className={"paper"}>
<ThemeIcon variant="light" size={40} radius={40}>
<Icon style={{width: rem(18), height: rem(18)}} stroke={1.5}/>
</ThemeIcon>
<Text mt="sm" mb={7}>
{title}
</Text>
<Text mt="sm" mb={7}>
<Code>
{data}
</Code>
</Text>
<Text size="sm" c="dimmed" lh={1.6}>
{description}
</Text>
</div>
)
}
export default function WhatWeKnowAboutYou() {
const {user} = usePB()
const location = useLocation()
const [cookies] = useCookies([ANALYTICS_COOKIE_NAME])
const cookieValue = cookies[ANALYTICS_COOKIE_NAME]
const parserResult = new UAParser().getResult()
const preferredLanguage = usePreferredLanguage()
const ipQuery = useQuery({
queryKey: ['ip'],
queryFn: async () => {
return await ofetch<AnalyticsIpApiResult>(ANALYTICS_IP_API, {ignoreResponseError: true})
}
})
return <>
<div className={"section-transparent stack"}>
<Center mt={"xl"} className={"stack"}>
@ -92,169 +22,8 @@ export default function WhatWeKnowAboutYou() {
</Center>
</div>
<div className={"section-transparent stack"}>
<CollectedAnalyticsData/>
<Center mt={"xl"}>
<Title order={2}>Anonyme Analysedaten</Title>
</Center>
<Center>
<Text ta={"center"} c={"dimmed"}>
Alle Analysedaten, die wir sammeln, sind anonymisiert und dienen dazu, diesen Dienst zu verbessern
und zu personalisieren.
<br/>
Wir geben keine Daten an Dritte weiter. Die Daten liegen auf unseren eigenen Servern in Deutschland.
<br/>
Nur Systemadministratoren haben Zugriff auf die Daten. Wir sammeln diese Daten erst, nachdem Cookies
akzeptiert wurden.
</Text>
</Center>
{!cookieValue && (
<Alert ta={"center"} fw={"600"} color={"green"}>
Solange du keine Cookies akzeptierst, sammeln wir keine Analysedaten.
</Alert>
)}
<SimpleGrid
cols={{base: 1, sm: 2, md: 3}}
>
{
[
{
icon: IconId,
title: "Analyse-ID",
data: cookieValue || "-",
description: "Diese ID wird genutzt, um nachzuvollziehen, wie du über die Zeit verteilt die StuVe IT Tools nutzt."
},
{
icon: IconSlash,
title: "Aktuelle Seite",
data: location.pathname,
description: "Der Pfad der aktuellen Seite, die du besuchst. Wir speichern diese Pfade um zu analysieren, welche Seiten am häufigsten besucht werden und wie Nutzende navigieren."
},
{
icon: IconBug,
title: "Fehler",
data: "-",
description: "Falls ein Fehler auftritt speichern wir die Fehlermeldung und den Zeitpunkt und dem entsprechenden Pfad, um die Stabilität der Seite zu verbessern."
},
{
icon: IconWorldWww,
title: "IP Adresse",
data: ipQuery.data?.ip || "-",
description: "Deine IP Adresse wird genutzt, um das Land zu bestimmen, aus dem du die Seite besuchst."
},
{
icon: IconGlobe,
title: "Land",
data: ipQuery.data?.country_code || "-",
description: "Das Land, aus dem du die Seite besuchst. Diese Information wird genutzt, um die Seite zu personalisieren und um zu verstehen, woher die Nutzenden kommen."
},
{
icon: () => <BrowserIcon browser={parserResult.browser.name ?? ""}/>,
title: "Browser",
data: `${parserResult.browser.name} ${parserResult.browser.version}`,
description: "Dein Browser wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
},
{
icon: () => <OsIcon os={parserResult.os.name ?? ""}/>,
title: "Betriebssystem",
data: `${parserResult.os.name} ${parserResult.os.version}`,
description: "Dein Betriebssystem wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
},
{
icon: IconWorldWww,
title: "Bevorzugte Sprache",
data: preferredLanguage || "-",
description: "Deine bevorzugte Sprache wird genutzt, um zu verstehen, welche Sprachen Nutzende sprechen."
},
{
icon: () => <DeviceTypeIcon deviceType={parserResult.device.type ?? "desktop"}/>,
title: "Gerätetyp",
data: parserResult.device.type || "desktop",
description: "Dein Gerätetyp wird genutzt, um die Seite anzuzeigen. Diese Information wird genutzt, um die Seite zu optimieren und um zu verstehen, welche Technologien Nutzende verwenden."
}
].map((feature, index) => (
<DataItem key={index} {...feature}/>
))
}
</SimpleGrid>
</div>
{
user &&
<div className={"section-transparent stack"}>
<Center mt={"xl"}>
<Title order={2}>Accountdaten</Title>
</Center>
<Center>
<Text ta={"center"} c={"dimmed"}>
Wenn du einen Account hast speichern wir zusätzlich zu den anonymen Analysedaten auch deine
Accountdaten.
</Text>
</Center>
<SimpleGrid
cols={{base: 1, sm: 2, md: 3}}
>
{
[
{
icon: IconUser,
title: "Anmeldename und Realm",
data: `${user.username} (${user.REALM})`,
description: "Hiermit kannst du dich anmelden und deine Daten verwalten. Deine Realm wird genutzt, um zu bestimmen ob du einen StuVe-IT oder Gast-Account hast."
},
{
icon: IconPassword,
title: "Passwort",
data: '***********',
description: "Dein Passwort wird verschlüsselt auf unseren Servern gespeichert und kann nicht eingesehen werden."
},
{
icon: IconGlobe,
title: "E-Mail",
data: user.email,
description: "Deine E-Mail Adresse wird genutzt, um dich zu kontaktieren. Andere Nutzende können deine E-Mail Adresse nicht sehen."
},
{
icon: IconSignature,
title: "Name",
data: getUserName(user),
description: "Dein Name wird genutzt, um dich zu identifizieren. Andere Nutzende können deinen Namen sehen."
},
{
icon: IconCalendarClock,
title: "Account Ablaufdatum",
data: user?.accountExpires ? (
new Date(user?.accountExpires).getTime() > Date.now() ? (
"Account ist aktiv und läuft am " + new Date(user?.accountExpires).toLocaleDateString() + " ab"
) : (
"Account ist abgelaufen"
)
) : (
"Dein Account läuft nicht ab"
),
description: "Dein Account-Ablaufdatum wird genutzt, um zu bestimmen, ob dein Account noch aktiv ist."
},
{
icon: IconUsers,
title: "Gruppen",
data: user?.expand?.memberOf.map((group: LdapGroupModel) => group.cn).join(", ") || "Keine Gruppen",
description: "Dein Account-Ablaufdatum wird genutzt, um zu bestimmen, ob dein Account noch aktiv ist."
},
].map((feature, index) => (
<DataItem key={index} {...feature}/>
))
}
</SimpleGrid>
</div>
}
<CollectedUserData/>
</>
}