legal sachen, profil löschen etc
This commit is contained in:
17
src/App.jsx
17
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() {
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/impressum" element={<ImpressumPage />} />
|
||||
<Route path="/datenschutz" element={<DatenschutzPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/unauthorized" element={<UnauthorizedPage />} /> {/* ← neu */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
@@ -60,7 +68,14 @@ export default function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER', 'SITE_OWNER']}>
|
||||
<HomePage />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="users"
|
||||
element={
|
||||
|
||||
@@ -13,7 +13,7 @@ axiosInstance.interceptors.response.use(
|
||||
const isLogin = error.config?.url?.includes('/auth/login');
|
||||
|
||||
if (error.response?.status === 401 && !isAuthCheck && !isLogin) {
|
||||
window.location.href = '/login';
|
||||
window.dispatchEvent(new CustomEvent('auth:expired'));
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { useNavigate, useLocation, Outlet, Link as RouterLink } from 'react-router-dom';
|
||||
import {
|
||||
Box, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
|
||||
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip, IconButton
|
||||
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip, IconButton, Link
|
||||
} from '@mui/material';
|
||||
import PeopleIcon from '@mui/icons-material/People';
|
||||
import ArticleIcon from '@mui/icons-material/Article';
|
||||
@@ -156,6 +156,14 @@ export default function Layout() {
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Box sx={{ px: 2, py: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||
<Link component={RouterLink} to="/impressum" variant="caption" color="text.disabled" sx={{ mr: 2 }}>
|
||||
Impressum
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/datenschutz" variant="caption" color="text.disabled">
|
||||
Datenschutz
|
||||
</Link>
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
{/* Main content */}
|
||||
|
||||
@@ -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_', ''));
|
||||
|
||||
@@ -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() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Konto Löschen Card */}
|
||||
<Card sx={{ border: '1px solid', borderColor: 'error.light' }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<DeleteIcon color="error" />
|
||||
<Typography variant="h6" fontWeight={600} color="error">
|
||||
Konto löschen
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Dein Konto und alle zugehörigen Daten werden dauerhaft gelöscht.
|
||||
Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
fullWidth
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Konto löschen
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Konto Löschen Dialog */}
|
||||
<Dialog open={open} onClose={handleDeleteClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600}}>Konto löschen</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2.5} sx={{ mt: 1 }}>
|
||||
{deletingError && <Alert severity="error">{deletingError}</Alert>}
|
||||
|
||||
<Alert severity="warning">
|
||||
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.
|
||||
</Alert>
|
||||
|
||||
<TextField
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={deleteForm.password}
|
||||
onChange={handleFormChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
||||
<Button onClick={handleDeleteClose} disabled={deleting}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
startIcon={deleting ? <CircularProgress size={16} color="inherit" /> : <DeleteIcon />}
|
||||
>
|
||||
Konto löschen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={accepted}
|
||||
onChange={(e) => setAccepted(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
Ich habe die{' '}
|
||||
<Link component={RouterLink} to="/datenschutz" underline="hover" target="_blank">
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
{' '}gelesen und stimme zu.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
disabled={loading}
|
||||
sx={{ mt: 1 }}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
disabled={loading || !accepted}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} color="inherit" /> : 'Registrieren'}
|
||||
</Button>
|
||||
|
||||
17
src/pages/Auth/UnauthorizedPage.jsx
Normal file
17
src/pages/Auth/UnauthorizedPage.jsx
Normal file
@@ -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 (
|
||||
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" minHeight="100vh">
|
||||
<Typography variant="h4" gutterBottom>Kein Zugriff</Typography>
|
||||
<Typography variant="body1" color="text.secondary" mb={3}>
|
||||
Du hast keine Berechtigung, diese Seite zu sehen.
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={() => navigate('/login')}>
|
||||
Zurück zum Login
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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 }) => (
|
||||
<Tooltip title="">
|
||||
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="Verwalter ändern" size="small" color="grey" onClick={() => handleOpenChange(row.id)}>
|
||||
<ManageAccountsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
@@ -184,6 +231,66 @@ export default function CompanyPage() {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Change Owner Dialog */}
|
||||
<Dialog open={openChange} onClose={handleChangeClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600}}>Unternehmensverwalter ändern</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2.5} sx={{ mt: 1}}>
|
||||
{changingError && <Alert severity="error">{changingError}</Alert>}
|
||||
|
||||
<Autocomplete
|
||||
options={userOptions}
|
||||
loading={usersLoading}
|
||||
value={changeForm.email}
|
||||
onChange={(_, newValue) => {
|
||||
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) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Neue Verwalter-Email"
|
||||
name="email"
|
||||
type="email"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<>
|
||||
{usersLoading ? <CircularProgress size={16} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
||||
<Button onClick={handleChangeClose} disabled={changing}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleChange}
|
||||
disabled={changing}
|
||||
startIcon={changing ? <CircularProgress size={16} color="inherit" /> : null}
|
||||
>
|
||||
Verwalter ändern
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600}}>Unternehmen erstellen</DialogTitle>
|
||||
|
||||
184
src/pages/Legal/DatenschutzPage.jsx
Normal file
184
src/pages/Legal/DatenschutzPage.jsx
Normal file
@@ -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 (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Body({ children }) {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DatenschutzPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
bgcolor: 'grey.50',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 6,
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Card sx={{ width: '100%', maxWidth: 680, p: 1, alignSelf: 'flex-start' }}>
|
||||
<CardContent>
|
||||
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 1 }}>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
Datenschutzerklärung
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" color="text.disabled" display="block" sx={{ mb: 3 }}>
|
||||
Gemäß DSGVO (EU) 2016/679 — Stand: [Monat Jahr]
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
{/* 1. Verantwortlicher */}
|
||||
<Section title="1. Verantwortlicher">
|
||||
<Body>
|
||||
Verantwortlicher im Sinne der DSGVO ist:
|
||||
</Body>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
[Name / Firma]<br />
|
||||
[Straße, Hausnummer]<br />
|
||||
[PLZ] [Ort]<br />
|
||||
E-Mail: [kontakt@example.com]
|
||||
</Typography>
|
||||
</Section>
|
||||
|
||||
{/* 2. Erhobene Daten */}
|
||||
<Section title="2. Welche Daten wir erheben">
|
||||
<Body>
|
||||
Bei der Registrierung und Nutzung des Admin-Panels erheben wir folgende personenbezogene Daten:
|
||||
</Body>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
• E-Mail-Adresse<br />
|
||||
• Nickname (Benutzername)<br />
|
||||
• Passwort (verschlüsselt gespeichert, nicht im Klartext)<br />
|
||||
• Zeitpunkt der Kontoerstellung<br />
|
||||
• Zugewiesene Benutzerrolle (z. B. ADMIN, REPORTER, SITE_OWNER)
|
||||
</Typography>
|
||||
</Section>
|
||||
|
||||
{/* 3. Zweck der Verarbeitung */}
|
||||
<Section title="3. Zweck der Datenverarbeitung">
|
||||
<Body>
|
||||
Die erhobenen Daten werden ausschließlich für folgende Zwecke verarbeitet:
|
||||
</Body>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
• Authentifizierung und Zugangskontrolle zum Admin-Panel<br />
|
||||
• Verwaltung von Inhalten der DeineDorfApp (Neuigkeiten, Veranstaltungen, Sehenswürdigkeiten etc.)<br />
|
||||
• Versand von Push-Benachrichtigungen an App-Nutzer (nur für berechtigte Rollen)<br />
|
||||
• Nachvollziehbarkeit von Änderungen im System
|
||||
</Typography>
|
||||
</Section>
|
||||
|
||||
{/* 4. Rechtsgrundlage */}
|
||||
<Section title="4. Rechtsgrundlage">
|
||||
<Body>
|
||||
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.
|
||||
</Body>
|
||||
</Section>
|
||||
|
||||
{/* 5. Speicherdauer */}
|
||||
<Section title="5. Speicherdauer">
|
||||
<Body>
|
||||
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.
|
||||
</Body>
|
||||
</Section>
|
||||
|
||||
{/* 6. Weitergabe an Dritte */}
|
||||
<Section title="6. Weitergabe an Dritte">
|
||||
<Body>
|
||||
Eine Weitergabe der Daten an Dritte erfolgt nicht, außer es ist zur Erbringung
|
||||
des Dienstes notwendig. Folgende Drittanbieter werden eingesetzt:
|
||||
</Body>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
• <strong>Firebase (Google)</strong> — Push-Benachrichtigungen (FCM). Datenschutzerklärung:
|
||||
policies.google.com/privacy<br />
|
||||
• <strong>[ggf. weiterer Dienst]</strong> — [Zweck]
|
||||
</Typography>
|
||||
</Section>
|
||||
|
||||
{/* 7. Hosting */}
|
||||
<Section title="7. Hosting">
|
||||
<Body>
|
||||
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.
|
||||
</Body>
|
||||
</Section>
|
||||
|
||||
{/* 8. Betroffenenrechte */}
|
||||
<Section title="8. Ihre Rechte">
|
||||
<Body>
|
||||
Sie haben gegenüber uns folgende Rechte hinsichtlich Ihrer personenbezogenen Daten:
|
||||
</Body>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
• <strong>Auskunft</strong> (Art. 15 DSGVO)<br />
|
||||
• <strong>Berichtigung</strong> (Art. 16 DSGVO)<br />
|
||||
• <strong>Löschung</strong> (Art. 17 DSGVO)<br />
|
||||
• <strong>Einschränkung der Verarbeitung</strong> (Art. 18 DSGVO)<br />
|
||||
• <strong>Widerspruch</strong> (Art. 21 DSGVO)<br />
|
||||
• <strong>Datenübertragbarkeit</strong> (Art. 20 DSGVO)
|
||||
</Typography>
|
||||
<Body>
|
||||
Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: [kontakt@example.com]
|
||||
</Body>
|
||||
</Section>
|
||||
|
||||
{/* 9. Beschwerderecht */}
|
||||
<Section title="9. Beschwerderecht">
|
||||
<Body>
|
||||
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.
|
||||
</Body>
|
||||
</Section>
|
||||
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
Alle Platzhalter in eckigen Klammern müssen vor Inbetriebnahme durch korrekte Angaben ersetzt werden.
|
||||
Im Zweifel einen Rechtsanwalt oder Datenschutzbeauftragten hinzuziehen.
|
||||
</Typography>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
118
src/pages/Legal/ImpressumPage.jsx
Normal file
118
src/pages/Legal/ImpressumPage.jsx
Normal file
@@ -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 (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Line({ label, value }) {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{label && <strong>{label}: </strong>}
|
||||
{value}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImpressumPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
bgcolor: 'grey.50',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 6,
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Card sx={{ width: '100%', maxWidth: 680, p: 1, alignSelf: 'flex-start' }}>
|
||||
<CardContent>
|
||||
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 1 }}>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
Impressum
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" color="text.disabled" display="block" sx={{ mb: 3 }}>
|
||||
Angaben gemäß § 5 TMG
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
{/* Anbieter */}
|
||||
<Section title="Anbieter">
|
||||
<Line value="[Vollständiger Name oder Firmenname]" />
|
||||
<Line value="[Straße und Hausnummer]" />
|
||||
<Line value="[PLZ] [Ort]" />
|
||||
<Line value="[Land]" />
|
||||
</Section>
|
||||
|
||||
{/* Kontakt */}
|
||||
<Section title="Kontakt">
|
||||
<Line label="Telefon" value="[+49 000 000000]" />
|
||||
<Line label="E-Mail" value="[kontakt@example.com]" />
|
||||
</Section>
|
||||
|
||||
{/* Vertreten durch */}
|
||||
<Section title="Vertreten durch">
|
||||
<Line value="[Vor- und Nachname des Vertreters / Geschäftsführers]" />
|
||||
</Section>
|
||||
|
||||
{/* Registereintrag – nur ausfüllen wenn vorhanden */}
|
||||
<Section title="Registereintrag (falls vorhanden)">
|
||||
<Line label="Registergericht" value="[Amtsgericht ...]" />
|
||||
<Line label="Registernummer" value="[HRB / HRA ...]" />
|
||||
</Section>
|
||||
|
||||
{/* Umsatzsteuer-ID – nur wenn vorhanden */}
|
||||
<Section title="Umsatzsteuer-ID (falls vorhanden)">
|
||||
<Line
|
||||
value="Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG: [DE000000000]"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
{/* Haftungsausschluss */}
|
||||
<Section title="Haftungsausschluss">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
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.
|
||||
</Typography>
|
||||
</Section>
|
||||
|
||||
{/* Stand */}
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
Stand: [Monat Jahr] — Bitte alle Platzhalter in eckigen Klammern ersetzen.
|
||||
</Typography>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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 }) => (
|
||||
<Tooltip title="">
|
||||
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="Verwalter ändern" size="small" color="grey" onClick={() => handleOpenChange(row.id)}>
|
||||
<ManageAccountsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
@@ -185,6 +230,65 @@ export default function OrganizationPage() {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Change Owner Dialog */}
|
||||
<Dialog open={openChange} onClose={handleChangeClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600}}>Organisationsverwalter ändern</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2.5} sx={{ mt: 1}}>
|
||||
{changingError && <Alert severity="error">{changingError}</Alert>}
|
||||
|
||||
<Autocomplete
|
||||
options={userOptions}
|
||||
loading={usersLoading}
|
||||
value={changeForm.email}
|
||||
onChange={(_, newValue) => {
|
||||
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) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Neue Verwalter-Email"
|
||||
name="email"
|
||||
type="email"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<>
|
||||
{usersLoading ? <CircularProgress size={16} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
||||
<Button onClick={handleChangeClose} disabled={changing}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleChange}
|
||||
disabled={changing}
|
||||
startIcon={changing ? <CircularProgress size={16} color="inherit" /> : null}
|
||||
>
|
||||
Verwalter ändern
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600}}>Organisation erstellen</DialogTitle>
|
||||
|
||||
Reference in New Issue
Block a user