diff --git a/src/App.jsx b/src/App.jsx index de83358..8e720d1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,6 +18,11 @@ import CompanyPage from "./pages/Company/CompanyPage.jsx"; import EditCompanyPage from "./pages/Company/EditCompanyPage.jsx"; import CalenderPostPage from "./pages/Calender/CalenderPostPage.jsx"; import ProfilePage from "./pages/Auth/ProfilePage.jsx"; +import UnauthorizedPage from "./pages/Auth/UnauthorizedPage.jsx"; +import DatenschutzPage from "./pages/Legal/DatenschutzPage.jsx"; +import ImpressumPage from "./pages/Legal/ImpressumPage.jsx"; + + const theme = createTheme({ palette: { @@ -51,7 +56,10 @@ export default function App() { } /> + } /> + } /> } /> + } /> {/* ← neu */} } > - } /> + + + + } + /> + + + Impressum + + + Datenschutz + + {/* Main content */} diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index e310947..4b72fd6 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -21,6 +21,15 @@ export function AuthProvider({ children }) { .finally(() => setLoading(false)); }, []); + useEffect(() => { + const handleExpired = () => { + setIsAuthenticated(false); + setRole(null); + }; + window.addEventListener('auth:expired', handleExpired); + return () => window.removeEventListener('auth:expired', handleExpired); + }, []); + const login = async (email, password) => { const { data } = await axiosInstance.post('/auth/login', { email, password }); setRole(data.role.replace('ROLE_', '')); diff --git a/src/pages/Auth/ProfilePage.jsx b/src/pages/Auth/ProfilePage.jsx index 2967826..1d4c646 100644 --- a/src/pages/Auth/ProfilePage.jsx +++ b/src/pages/Auth/ProfilePage.jsx @@ -1,11 +1,16 @@ import { useState } from 'react'; +import { useAuth } from '../../context/AuthContext'; +import { useNavigate } from 'react-router-dom'; import { Box, Card, CardContent, TextField, Button, Typography, - Alert, CircularProgress, Divider, + Alert, CircularProgress, Divider, DialogTitle, DialogContent, Stack, DialogActions, Dialog, } from '@mui/material'; import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import axiosInstance from '../../api/axiosInstance'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from "@mui/icons-material/Add"; +import {Info} from "@mui/icons-material"; export default function ProfilePage() { @@ -21,6 +26,47 @@ export default function ProfilePage() { const [passwordError, setPasswordError] = useState(''); const [passwordSuccess, setPasswordSuccess] = useState(''); + const [open, setOpen] = useState(false); + const [deleting, setDeleting] = useState(false); + const [deletingError, setDeletingError] = useState(''); + + const { logout } = useAuth(); + const navigate = useNavigate(); + + const [deleteForm, setDeleteForm] = useState({ + password: '' + }); + + const handleFormChange = (e) => { + const { name, value } = e.target; + setDeleteForm((prev) => ({ ...prev, [name]: value })); + }; + + const handleDelete = async () => { + setDeleting(true); + setDeletingError(''); + + try { + const jsonPayload = { + password: deleteForm.password}; + + await axiosInstance.post('/users/delete/me', jsonPayload); + handleDeleteClose(); + await logout(); + navigate('/login'); + } catch (err) { + setDeletingError(err.response?.data?.error ?? 'Löschen fehlgeschlagen'); + } finally { + setDeleting(false); + } + }; + + const handleDeleteClose = () => { + setOpen(false); + setDeleteForm({ password: ''}); + setDeletingError(''); + }; + const handleNicknameSave = async (e) => { e.preventDefault(); setNicknameError(''); @@ -151,6 +197,72 @@ export default function ProfilePage() { + {/* Konto Löschen Card */} + + + + + + Konto löschen + + + + Dein Konto und alle zugehörigen Daten werden dauerhaft gelöscht. + Diese Aktion kann nicht rückgängig gemacht werden. + + + + + + {/* Konto Löschen Dialog */} + + Konto löschen + + + + {deletingError && {deletingError}} + + + Das Löschen deines Kontos kann nicht rückgängig gemacht werden. Sobald du dein Konto löscht werden alle News etc. die zu dir gehören automatisch mit gelöscht. Unternehmen/Vereine, bei welchen du als Verwalter eingetragen bist bleiben bestehen, falls diese auch gelöscht werden sollen kannst du dies manuel vor der Löschung des Kontos machen oder bei einem Admin beauftragen. + + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/src/pages/Auth/RegisterPage.jsx b/src/pages/Auth/RegisterPage.jsx index 4d2fd81..46770b9 100644 --- a/src/pages/Auth/RegisterPage.jsx +++ b/src/pages/Auth/RegisterPage.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useNavigate, Link as RouterLink } from 'react-router-dom'; import { Box, Card, CardContent, TextField, Button, Typography, - Alert, CircularProgress, Link, Divider, + Alert, CircularProgress, Link, Divider, FormControlLabel, Checkbox } from '@mui/material'; import PersonAddOutlinedIcon from '@mui/icons-material/PersonAddOutlined'; import axiosInstance from '../../api/axiosInstance.js'; @@ -12,6 +12,7 @@ export default function RegisterPage() { const [form, setForm] = useState({ email: '', nickname: '', password: '', confirmPassword: '' }); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [accepted, setAccepted] = useState(false); const handleChange = (e) => setForm((prev) => ({ ...prev, [e.target.name]: e.target.value })); @@ -114,13 +115,32 @@ export default function RegisterPage() { } /> + setAccepted(e.target.checked)} + size="small" + /> + } + label={ + + Ich habe die{' '} + + Datenschutzerklärung + + {' '}gelesen und stimme zu. + + } + /> + diff --git a/src/pages/Auth/UnauthorizedPage.jsx b/src/pages/Auth/UnauthorizedPage.jsx new file mode 100644 index 0000000..6546a3f --- /dev/null +++ b/src/pages/Auth/UnauthorizedPage.jsx @@ -0,0 +1,17 @@ +import { Box, Typography, Button } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; + +export default function UnauthorizedPage() { + const navigate = useNavigate(); + return ( + + Kein Zugriff + + Du hast keine Berechtigung, diese Seite zu sehen. + + + + ); +} \ No newline at end of file diff --git a/src/pages/Company/CompanyPage.jsx b/src/pages/Company/CompanyPage.jsx index 6327e31..7590345 100644 --- a/src/pages/Company/CompanyPage.jsx +++ b/src/pages/Company/CompanyPage.jsx @@ -11,12 +11,18 @@ import { Chip } from '@mui/material'; import axiosInstance from '../../api/axiosInstance.js'; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import {useNavigate} from "react-router-dom"; +import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; export default function CompanyPage() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [openChange, setOpenChange] = useState(false); + const [changing, setChanging] = useState(false); + const [changingId, setChangingId] = useState(''); + const [changingError, setChangingError] = useState(''); + const [userOptions, setUserOptions] = useState([]); const [usersLoading, setUsersLoading] = useState(false); @@ -30,6 +36,11 @@ export default function CompanyPage() { email: '' }); + + const [changeForm, setChangeForm] = useState({ + email: '' + }) + const loadCompanies = async () => { try { const { data } = await axiosInstance.get('/company'); @@ -45,6 +56,37 @@ export default function CompanyPage() { loadCompanies(); }, []); + const handleOpenChange = (id) => { + setOpenChange(true); + setChangingId(id); + loadUsers(); + } + + const handleChangeClose = () => { + setOpenChange(false); + setChangeForm({ email: ''}); + setChangingError(''); + setChangingId(''); + } + + + const handleChange = async () => { + setChanging(true); + setChangingError(''); + try { + await axiosInstance.put(`/company/owner/${changingId}`, { + newOwner: changeForm.email + }); + handleChangeClose(); + await loadCompanies(); + alert('Verwalter geändert'); + } catch (err) { + setChangingError(err.response?.data?.error) + } finally { + setChanging(false); + } + } + const handleFormChange = (e) => { const { name, value } = e.target; setCreateForm((prev) => ({ ...prev, [name]: value })); @@ -143,12 +185,17 @@ export default function CompanyPage() { sortable: false, renderCell: ({ row }) => ( - handleDelete(row.id)}> - - - handleEdit(row.id)}> - - + + handleDelete(row.id)}> + + + handleEdit(row.id)}> + + + handleOpenChange(row.id)}> + + + ), }, @@ -184,6 +231,66 @@ export default function CompanyPage() { /> + + {/* Change Owner Dialog */} + + Unternehmensverwalter ändern + + + + {changingError && {changingError}} + + { + setChangeForm(prev => ({ ...prev, email: newValue ?? '' })); + }} + onInputChange={(_, newInputValue) => { + setChangeForm(prev => ({ ...prev, email: newInputValue })); + }} + freeSolo + filterOptions={(options, { inputValue }) => + options.filter(o => o.toLowerCase().includes(inputValue.toLowerCase())) + } + renderInput={(params) => ( + + {usersLoading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + + + + + + + {/* Create Dialog */} Unternehmen erstellen diff --git a/src/pages/Legal/DatenschutzPage.jsx b/src/pages/Legal/DatenschutzPage.jsx new file mode 100644 index 0000000..c8b3ed2 --- /dev/null +++ b/src/pages/Legal/DatenschutzPage.jsx @@ -0,0 +1,184 @@ +import { useNavigate } from 'react-router-dom'; +import { + Box, Card, CardContent, Typography, Divider, Button, +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +function Section({ title, children }) { + return ( + + + {title} + + {children} + + ); +} + +function Body({ children }) { + return ( + + {children} + + ); +} + +export default function DatenschutzPage() { + const navigate = useNavigate(); + + return ( + + + + + {/* Header */} + + + + Datenschutzerklärung + + + + + Gemäß DSGVO (EU) 2016/679 — Stand: [Monat Jahr] + + + + + {/* 1. Verantwortlicher */} +
+ + Verantwortlicher im Sinne der DSGVO ist: + + + [Name / Firma]
+ [Straße, Hausnummer]
+ [PLZ] [Ort]
+ E-Mail: [kontakt@example.com] +
+
+ + {/* 2. Erhobene Daten */} +
+ + Bei der Registrierung und Nutzung des Admin-Panels erheben wir folgende personenbezogene Daten: + + + • E-Mail-Adresse
+ • Nickname (Benutzername)
+ • Passwort (verschlüsselt gespeichert, nicht im Klartext)
+ • Zeitpunkt der Kontoerstellung
+ • Zugewiesene Benutzerrolle (z. B. ADMIN, REPORTER, SITE_OWNER) +
+
+ + {/* 3. Zweck der Verarbeitung */} +
+ + Die erhobenen Daten werden ausschließlich für folgende Zwecke verarbeitet: + + + • Authentifizierung und Zugangskontrolle zum Admin-Panel
+ • Verwaltung von Inhalten der DeineDorfApp (Neuigkeiten, Veranstaltungen, Sehenswürdigkeiten etc.)
+ • Versand von Push-Benachrichtigungen an App-Nutzer (nur für berechtigte Rollen)
+ • Nachvollziehbarkeit von Änderungen im System +
+
+ + {/* 4. Rechtsgrundlage */} +
+ + Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO + (Vertragserfüllung bzw. vorvertragliche Maßnahmen) sowie Art. 6 Abs. 1 lit. a DSGVO + (Einwilligung), die bei der Registrierung erteilt wird. + +
+ + {/* 5. Speicherdauer */} +
+ + Personenbezogene Daten werden so lange gespeichert, wie der Account aktiv ist oder + dies für die Erfüllung der genannten Zwecke erforderlich ist. Nach Löschung des + Accounts werden die Daten gelöscht, sofern keine gesetzlichen Aufbewahrungspflichten + entgegenstehen. + +
+ + {/* 6. Weitergabe an Dritte */} +
+ + Eine Weitergabe der Daten an Dritte erfolgt nicht, außer es ist zur Erbringung + des Dienstes notwendig. Folgende Drittanbieter werden eingesetzt: + + + • Firebase (Google) — Push-Benachrichtigungen (FCM). Datenschutzerklärung: + policies.google.com/privacy
+ • [ggf. weiterer Dienst] — [Zweck] +
+
+ + {/* 7. Hosting */} +
+ + Die Anwendung wird auf eigenen Servern betrieben, gehostet mittels Docker. + Der Serverstandort befindet sich in [Land / Rechenzentrum]. + Es findet keine Übermittlung in Drittstaaten außerhalb der EU statt, + sofern nicht oben anders angegeben. + +
+ + {/* 8. Betroffenenrechte */} +
+ + Sie haben gegenüber uns folgende Rechte hinsichtlich Ihrer personenbezogenen Daten: + + + • Auskunft (Art. 15 DSGVO)
+ • Berichtigung (Art. 16 DSGVO)
+ • Löschung (Art. 17 DSGVO)
+ • Einschränkung der Verarbeitung (Art. 18 DSGVO)
+ • Widerspruch (Art. 21 DSGVO)
+ • Datenübertragbarkeit (Art. 20 DSGVO) +
+ + Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: [kontakt@example.com] + +
+ + {/* 9. Beschwerderecht */} +
+ + Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehörde über die + Verarbeitung Ihrer personenbezogenen Daten zu beschweren. Die zuständige + Behörde richtet sich nach Ihrem Wohnsitz oder dem Sitz unseres Unternehmens. + +
+ + + + + Alle Platzhalter in eckigen Klammern müssen vor Inbetriebnahme durch korrekte Angaben ersetzt werden. + Im Zweifel einen Rechtsanwalt oder Datenschutzbeauftragten hinzuziehen. + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Legal/ImpressumPage.jsx b/src/pages/Legal/ImpressumPage.jsx new file mode 100644 index 0000000..1860ae0 --- /dev/null +++ b/src/pages/Legal/ImpressumPage.jsx @@ -0,0 +1,118 @@ +import { useNavigate } from 'react-router-dom'; +import { + Box, Card, CardContent, Typography, Divider, Button, +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +function Section({ title, children }) { + return ( + + + {title} + + {children} + + ); +} + +function Line({ label, value }) { + return ( + + {label && {label}: } + {value} + + ); +} + +export default function ImpressumPage() { + const navigate = useNavigate(); + + return ( + + + + + {/* Header */} + + + + Impressum + + + + + Angaben gemäß § 5 TMG + + + + + {/* Anbieter */} +
+ + + + +
+ + {/* Kontakt */} +
+ + +
+ + {/* Vertreten durch */} +
+ +
+ + {/* Registereintrag – nur ausfüllen wenn vorhanden */} +
+ + +
+ + {/* Umsatzsteuer-ID – nur wenn vorhanden */} +
+ +
+ + + + {/* Haftungsausschluss */} +
+ + Die Inhalte dieser Anwendung wurden mit größtmöglicher Sorgfalt erstellt. + Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte übernehmen + wir jedoch keine Gewähr. Als Diensteanbieter sind wir gemäß § 7 Abs. 1 TMG + für eigene Inhalte nach den allgemeinen Gesetzen verantwortlich. + +
+ + {/* Stand */} + + Stand: [Monat Jahr] — Bitte alle Platzhalter in eckigen Klammern ersetzen. + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Organization/OrganizationPage.jsx b/src/pages/Organization/OrganizationPage.jsx index 78da244..723d7c7 100644 --- a/src/pages/Organization/OrganizationPage.jsx +++ b/src/pages/Organization/OrganizationPage.jsx @@ -9,6 +9,7 @@ import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import AddIcon from '@mui/icons-material/Add'; import { Chip } from '@mui/material'; import axiosInstance from '../../api/axiosInstance.js'; +import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import {useNavigate} from "react-router-dom"; @@ -20,6 +21,11 @@ export default function OrganizationPage() { const [userOptions, setUserOptions] = useState([]); const [usersLoading, setUsersLoading] = useState(false); + const [openChange, setOpenChange] = useState(false); + const [changing, setChanging] = useState(false); + const [changingId, setChangingId] = useState(''); + const [changingError, setChangingError] = useState(''); + const [open, setOpen] = useState(false); const [creating, setCreating] = useState(false); const [creatingError, setCreatingError] = useState(''); @@ -30,6 +36,10 @@ export default function OrganizationPage() { email: '' }); + const [changeForm, setChangeForm] = useState({ + email: '' + }) + const loadOrganizations = async () => { try { const { data } = await axiosInstance.get('/organization'); @@ -46,6 +56,12 @@ export default function OrganizationPage() { loadUsers(); } + const handleOpenChange = (id) => { + setOpenChange(true); + setChangingId(id); + loadUsers(); + } + useEffect(() => { loadOrganizations(); }, []); @@ -96,6 +112,30 @@ export default function OrganizationPage() { setCreatingError(''); }; + const handleChangeClose = () => { + setOpenChange(false); + setChangeForm({ email: ''}); + setChangingError(''); + setChangingId(''); + } + + const handleChange = async () => { + setChanging(true); + setChangingError(''); + try { + await axiosInstance.put(`/organization/owner/${changingId}`, { + newOwner: changeForm.email + }); + handleChangeClose(); + await loadOrganizations(); + alert('Verwalter geändert'); + } catch (err) { + setChangingError(err.response?.data?.error) + } finally { + setChanging(false); + } + } + const handleDelete = async (id) => { if (!window.confirm('Organisation wirklich löschen?')) return; @@ -144,12 +184,17 @@ export default function OrganizationPage() { sortable: false, renderCell: ({ row }) => ( - handleDelete(row.id)}> - - - handleEdit(row.id)}> - - + + handleDelete(row.id)}> + + + handleEdit(row.id)}> + + + handleOpenChange(row.id)}> + + + ), }, @@ -185,6 +230,65 @@ export default function OrganizationPage() { /> + {/* Change Owner Dialog */} + + Organisationsverwalter ändern + + + + {changingError && {changingError}} + + { + setChangeForm(prev => ({ ...prev, email: newValue ?? '' })); + }} + onInputChange={(_, newInputValue) => { + setChangeForm(prev => ({ ...prev, email: newInputValue })); + }} + freeSolo + filterOptions={(options, { inputValue }) => + options.filter(o => o.toLowerCase().includes(inputValue.toLowerCase())) + } + renderInput={(params) => ( + + {usersLoading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + + + + + + + {/* Create Dialog */} Organisation erstellen