Userverwaltung integriert

This commit is contained in:
Marc Wieland 2025-04-21 18:13:54 +02:00
parent 8a92201b74
commit e2fe6cf33a
6 changed files with 368 additions and 30 deletions

View File

@ -1,32 +1,25 @@
import { Link, useNavigate } from "react-router-dom"; import { Link } from "react-router-dom";
import { Card, CardContent, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useAuth } from "@/context/AuthContext"; import { useAuth } from "@/context/AuthContext";
import { LogOut } from "lucide-react";
const AdminDashboard = () => { const AdminDashboard = () => {
const { logout, username } = useAuth(); const { isAuthenticated, username, isAdmin, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/");
};
return ( return (
<div className="max-w-6xl mx-auto py-12 px-4"> <div className="max-w-6xl mx-auto py-12 px-4">
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-frog-600">Willkommen, {username}!</h1> <h1 className="text-3xl font-bold text-frog-600">Willkommen, {username}!</h1>
<Button <Button
onClick={handleLogout} onClick={logout}
className="bg-frog-500 hover:bg-frog-600 text-white flex items-center gap-2" className="bg-frog-500 hover:bg-frog-600 text-white"
> >
<LogOut className="w-4 h-4" />
Logout Logout
</Button> </Button>
</div> </div>
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{/* Jeder eingeloggt Benutzer darf News verwalten */}
<Link to="/admin/news"> <Link to="/admin/news">
<Card className="hover:shadow-lg transition-shadow cursor-pointer"> <Card className="hover:shadow-lg transition-shadow cursor-pointer">
<CardContent className="p-6 text-center"> <CardContent className="p-6 text-center">
@ -36,15 +29,17 @@ const AdminDashboard = () => {
</Card> </Card>
</Link> </Link>
{/* Hier später weitere Admin-Bereiche */} {/* Nur Admins sehen diese Card */}
{isAdmin && (
<Link to="/admin/users"> <Link to="/admin/users">
<Card className="hover:shadow-lg transition-shadow cursor-pointer"> <Card className="hover:shadow-lg transition-shadow cursor-pointer">
<CardContent className="p-6 text-center"> <CardContent className="p-6 text-center">
<CardTitle className="text-frog-600">Benutzer verwalten</CardTitle> <CardTitle className="text-frog-600">Benutzer verwalten</CardTitle>
<p className="text-gray-600 mt-2 text-sm">Admins erstellen, bearbeiten und löschen</p> <p className="text-gray-600 mt-2 text-sm">Admins und Benutzer verwalten</p>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>
)}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useNavigate } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";
const UserCreatePage = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState("user");
const { token } = useAuth();
const {isAuthenticated, isAdmin} = useAuth();
const navigate = useNavigate();
useEffect(() => {
if(!isAuthenticated || !isAdmin) {
navigate("/admin");
}
}, [isAuthenticated, isAdmin, navigate]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await fetch("http://192.168.50.65:3000/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ username, password, role }),
});
if (!res.ok) {
throw new Error("Fehler beim Anlegen des Benutzers");
}
navigate("/admin/users");
} catch (err) {
console.error(err);
alert("Benutzer konnte nicht angelegt werden");
}
};
return (
<div className="max-w-3xl mx-auto py-12 px-4">
<Card>
<CardHeader>
<CardTitle>Benutzer erstellen</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
placeholder="Benutzername"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<Input
type="password"
placeholder="Passwort"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="w-full border rounded-md p-2"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white">
Benutzer anlegen
</Button>
</form>
</CardContent>
</Card>
</div>
);
};
export default UserCreatePage;

105
src/admin/UserEditPage.tsx Normal file
View File

@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useAuth } from "@/context/AuthContext";
const UserEditPage = () => {
const { id } = useParams<{ id: string }>();
const { token, isAuthenticated, isAdmin } = useAuth();
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); // optional neu setzen
const [role, setRole] = useState("user");
useEffect(() => {
if (!isAuthenticated || !isAdmin) {
navigate("/admin");
}
}, [isAuthenticated, isAdmin, navigate]);
useEffect(() => {
const fetchUser = async () => {
try {
const res = await fetch(`http://192.168.50.65:3000/api/users/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setUsername(data.username);
setRole(data.role);
} catch (err) {
console.error(err);
}
};
if (id) {
fetchUser();
}
}, [id, token]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await fetch(`http://192.168.50.65:3000/api/users/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
username,
password: password || undefined, // Passwort nur mitschicken wenn eingegeben
role,
}),
});
navigate("/admin/users");
} catch (err) {
console.error(err);
alert("Fehler beim Aktualisieren des Benutzers");
}
};
return (
<div className="max-w-3xl mx-auto py-12 px-4">
<Card>
<CardHeader>
<CardTitle>Benutzer bearbeiten</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
placeholder="Benutzername"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<Input
type="password"
placeholder="Neues Passwort (leer lassen für kein Wechsel)"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="w-full border rounded-md p-2"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white">
Benutzer speichern
</Button>
</form>
</CardContent>
</Card>
</div>
);
};
export default UserEditPage;

View File

@ -0,0 +1,135 @@
import { useEffect, useState } from "react";
import { Card, CardContent, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { useAuth } from "@/context/AuthContext";
import { useNavigate } from "react-router-dom";
interface User {
id: number;
username: string;
email: string;
role: string;
}
const UserManagementPage = () => {
const [users, setUsers] = useState<User[]>([]);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [userToDelete, setUserToDelete] = useState<User | null>(null);
const { token } = useAuth();
const {isAuthenticated, isAdmin} = useAuth();
const navigate = useNavigate();
useEffect(() => {
if(!isAuthenticated || !isAdmin) {
navigate("/admin");
}
}, [isAuthenticated, isAdmin, navigate]);
useEffect(() => {
const fetchUsers = async () => {
try {
const res = await fetch("http://192.168.50.65:3000/api/users", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await res.json();
setUsers(data);
} catch (err) {
console.error(err);
}
};
fetchUsers();
}, [token]);
const handleDelete = async () => {
if (!userToDelete) return;
try {
await fetch(`http://192.168.50.65:3000/api/users/${userToDelete.id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
// Nach dem Löschen die Liste neu laden
setUsers((prev) => prev.filter((u) => u.id !== userToDelete.id));
setIsDeleteModalOpen(false);
} catch (err) {
console.error(err);
}
};
return (
<div className="max-w-6xl mx-auto py-12 px-4">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-frog-600">Benutzerverwaltung</h1>
<Button
className="bg-frog-500 hover:bg-frog-600 text-white"
onClick={() => navigate("/admin/users/new")}
>
Benutzer hinzufügen
</Button>
</div>
<div className="grid md:grid-cols-2 gap-6">
{users.map((user) => (
<Card key={user.id} className="hover:shadow-lg transition-shadow">
<CardContent className="p-6">
<CardTitle className="text-frog-600">{user.username}</CardTitle>
<p className="text-gray-600 text-sm">{user.email}</p>
<p className="text-sm mt-1">
Rolle: <span className={user.role === "admin" ? "text-frog-500 font-semibold" : "text-gray-500"}>{user.role}</span>
</p>
<div className="flex gap-4 mt-4">
<Button
variant="outline"
onClick={() => navigate(`/admin/users/edit/${user.id}`)}
className="border-frog-500 text-frog-600 hover:bg-frog-50"
>
Bearbeiten
</Button>
<Button
variant="destructive"
onClick={() => {
setUserToDelete(user);
setIsDeleteModalOpen(true);
}}
>
Löschen
</Button>
</div>
</CardContent>
</Card>
))}
</div>
{/* Delete Confirm Modal */}
<Dialog open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Möchtest du {userToDelete?.username} wirklich löschen?</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setIsDeleteModalOpen(false)}>
Abbrechen
</Button>
<Button variant="destructive" onClick={handleDelete}>
Löschen
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default UserManagementPage;

View File

@ -1,11 +1,11 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react"; import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
interface AuthContextType { interface AuthContextType {
token: string | null; token: string | null;
username: string | null; username: string | null;
role: string | null;
isAdmin: boolean;
login: (token: string) => void; login: (token: string) => void;
logout: () => void; logout: () => void;
isAuthenticated: boolean; isAuthenticated: boolean;
@ -13,15 +13,22 @@ interface AuthContextType {
const AuthContext = createContext<AuthContextType>({ const AuthContext = createContext<AuthContextType>({
token: null, token: null,
username: null,
role: null,
isAdmin: false,
login: () => {}, login: () => {},
logout: () => {}, logout: () => {},
isAuthenticated: false, isAuthenticated: false,
username: null
}); });
export const AuthProvider = ({ children }: { children: ReactNode }) => { export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [token, setToken] = useState<string | null>(null); const [token, setToken] = useState<string | null>(null);
const [username, setUsername] = useState<string | null>(null); const [username, setUsername] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
const isAdmin = role === "admin";
const isAuthenticated = !!token;
useEffect(() => { useEffect(() => {
const storedToken = localStorage.getItem("token"); const storedToken = localStorage.getItem("token");
@ -29,7 +36,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
setToken(storedToken); setToken(storedToken);
try { try {
const decoded: any = jwtDecode(storedToken); const decoded: any = jwtDecode(storedToken);
setUsername(decoded.username); // <-- Username speichern setUsername(decoded.username);
setRole(decoded.role);
} catch (error) { } catch (error) {
console.error("Token konnte nicht gelesen werden"); console.error("Token konnte nicht gelesen werden");
} }
@ -44,12 +52,12 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const logout = () => { const logout = () => {
localStorage.removeItem("token"); localStorage.removeItem("token");
setToken(null); setToken(null);
setUsername(null);
setRole(null);
}; };
const isAuthenticated = !!token;
return ( return (
<AuthContext.Provider value={{ token, username, login, logout, isAuthenticated }}> <AuthContext.Provider value={{ token, username, role, isAdmin, login, logout, isAuthenticated }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@ -16,6 +16,9 @@ import AdminDashboard from "./admin/AdminDashboard";
import NewsManager from "./admin/NewsManager"; import NewsManager from "./admin/NewsManager";
import { AuthProvider } from "./context/AuthContext"; import { AuthProvider } from "./context/AuthContext";
import PrivateRoute from "./components/PrivateRoute"; import PrivateRoute from "./components/PrivateRoute";
import UserManagementPage from "./admin/UserManagementPage";
import UserCreatePage from "./admin/UserCreatePage";
import UserEditPage from "./admin/UserEditPage";
@ -53,6 +56,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Route path="/admin/login" element={<LoginPage />} /> <Route path="/admin/login" element={<LoginPage />} />
<Route path="/admin" element={<PrivateRoute><Layout><AdminDashboard /></Layout></PrivateRoute>} /> <Route path="/admin" element={<PrivateRoute><Layout><AdminDashboard /></Layout></PrivateRoute>} />
<Route path="/admin/news" element={<PrivateRoute><Layout><NewsManager /></Layout></PrivateRoute>} /> <Route path="/admin/news" element={<PrivateRoute><Layout><NewsManager /></Layout></PrivateRoute>} />
<Route path="/admin/users" element={<PrivateRoute><Layout><UserManagementPage /></Layout></PrivateRoute>} />
<Route path="/admin/users/new" element={<PrivateRoute><Layout><UserCreatePage /></Layout></PrivateRoute>} />
<Route path="/admin/users/edit/:id" element={<PrivateRoute><Layout><UserEditPage /></Layout></PrivateRoute>} />
</Routes> </Routes>
</AuthProvider> </AuthProvider>
</BrowserRouter> </BrowserRouter>