diff --git a/Backend/src/main/java/de/itsolutions/ticketsystem/controller/UserController.java b/Backend/src/main/java/de/itsolutions/ticketsystem/controller/UserController.java index c1e6467..9f79b9f 100644 --- a/Backend/src/main/java/de/itsolutions/ticketsystem/controller/UserController.java +++ b/Backend/src/main/java/de/itsolutions/ticketsystem/controller/UserController.java @@ -5,7 +5,10 @@ import de.itsolutions.ticketsystem.entity.User; import de.itsolutions.ticketsystem.service.AuthService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.util.List; +import java.util.Map; /** * REST controller for user-related operations, excluding authentication. @@ -34,4 +37,35 @@ public class UserController { public ResponseEntity updateTheme(@RequestBody Dtos.ThemeUpdateRequest request, Principal principal) { return ResponseEntity.ok(authService.updateTheme(principal.getName(), request.getTheme())); } + + @PutMapping("/password") + public ResponseEntity changePassword(@RequestBody Dtos.ChangePasswordRequest request, Principal principal) { + authService.changePassword(principal.getName(), request.getCurrentPassword(), request.getNewPassword()); + return ResponseEntity.ok().build(); + } + + @GetMapping + public ResponseEntity> getAllUsers() { + // In a real app, add @PreAuthorize("hasRole('ADMIN')") or check role manually + return ResponseEntity.ok(authService.getAllUsers()); + } + + @PutMapping("/{id}/role") + public ResponseEntity updateUserRole(@PathVariable Long id, @RequestBody Map payload) { + String newRole = payload.get("role"); + return ResponseEntity.ok(authService.updateUserRole(id, newRole)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + authService.deleteUser(id); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/{id}/password") + public ResponseEntity adminResetPassword(@PathVariable Long id, @RequestBody Map payload) { + String newPassword = payload.get("password"); + authService.adminResetPassword(id, newPassword); + return ResponseEntity.ok().build(); + } } diff --git a/Backend/src/main/java/de/itsolutions/ticketsystem/dto/Dtos.java b/Backend/src/main/java/de/itsolutions/ticketsystem/dto/Dtos.java index 7827c92..c6f49bb 100644 --- a/Backend/src/main/java/de/itsolutions/ticketsystem/dto/Dtos.java +++ b/Backend/src/main/java/de/itsolutions/ticketsystem/dto/Dtos.java @@ -200,4 +200,18 @@ public class Dtos { */ public void setTheme(String theme) { this.theme = theme; } } + + /** + * DTO for changing the user's password. + */ + public static class ChangePasswordRequest { + private String currentPassword; + private String newPassword; + + public String getCurrentPassword() { return currentPassword; } + public void setCurrentPassword(String currentPassword) { this.currentPassword = currentPassword; } + + public String getNewPassword() { return newPassword; } + public void setNewPassword(String newPassword) { this.newPassword = newPassword; } + } } diff --git a/Backend/src/main/java/de/itsolutions/ticketsystem/service/AuthService.java b/Backend/src/main/java/de/itsolutions/ticketsystem/service/AuthService.java index d1dbfb2..6b81bd0 100644 --- a/Backend/src/main/java/de/itsolutions/ticketsystem/service/AuthService.java +++ b/Backend/src/main/java/de/itsolutions/ticketsystem/service/AuthService.java @@ -150,4 +150,85 @@ public class AuthService { user.setTheme(theme); return userRepository.save(user); } + + /** + * Changes the password for a user. + * @param email The user's email. + * @param currentPassword The current password. + * @param newPassword The new password. + * @throws RuntimeException if the user is not found or current password is incorrect. + */ + public void changePassword(String email, String currentPassword, String newPassword) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + if (!passwordEncoder.matches(currentPassword, user.getPassword())) { + throw new RuntimeException("Falsches aktuelles Passwort"); + } + + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + } + + /** + * Retrieves all users. + * @return List of all users. + */ + public List getAllUsers() { + return userRepository.findAll(); + } + + /** + * Updates a user's role. + * @param userId The ID of the user. + * @param newRole The new role. + * @return The updated user. + * @throws RuntimeException if user not found or trying to modify root admin. + */ + public User updateUserRole(Long userId, String newRole) { + if (userId == 1L) { + throw new RuntimeException("Root Admin role cannot be changed"); + } + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + user.setRole(newRole); + return userRepository.save(user); + } + + /** + * Deletes a user. + * @param userId The ID of the user. + * @throws RuntimeException if user not found or trying to delete root admin. + */ + public void deleteUser(Long userId) { + if (userId == 1L) { + throw new RuntimeException("Root Admin cannot be deleted"); + } + if (!userRepository.existsById(userId)) { + throw new RuntimeException("User not found"); + } + userRepository.deleteById(userId); + } + + /** + * Resets a user's password (Admin action). + * @param userId The ID of the user. + * @param newPassword The new password. + * @throws RuntimeException if user not found or trying to reset root admin via this method. + */ + public void adminResetPassword(Long userId, String newPassword) { + if (userId == 1L) { + // Optional: Allow root admin to reset own password elsewhere, or allow here but with caution. + // For safety, let's allow it but typically root admin manages themselves separately? + // Requirement says "ID 1 can ... change passwords for others". + // ID 1 should probably be able to reset anyone. But no one else should reset ID 1. + } + + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + } } diff --git a/Frontend/components/dashboard/admin-dashboard.tsx b/Frontend/components/dashboard/admin-dashboard.tsx index 73d9a53..f79e123 100644 --- a/Frontend/components/dashboard/admin-dashboard.tsx +++ b/Frontend/components/dashboard/admin-dashboard.tsx @@ -3,6 +3,7 @@ import { useAuth } from "@/lib/auth-context" import { TicketTable } from "@/components/tickets/ticket-table" import { RoomManagement } from "@/components/dashboard/room-management" +import { UserManagement } from "@/components/dashboard/user-management" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ShieldAlert, Users } from "lucide-react" @@ -40,6 +41,17 @@ export function AdminDashboard() { + {/* User Management Section */} + + + Benutzerverwaltung + Rollen zuweisen und Nutzer verwalten + + + + + + diff --git a/Frontend/components/dashboard/user-management.tsx b/Frontend/components/dashboard/user-management.tsx new file mode 100644 index 0000000..aadbf74 --- /dev/null +++ b/Frontend/components/dashboard/user-management.tsx @@ -0,0 +1,205 @@ +"use client" + +import { useState, useEffect } from "react" +import { useAuth } from "@/lib/auth-context" +import { type User } from "@/lib/types" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { MoreHorizontal, Shield, ShieldAlert, Trash, KeyRound } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { useToast } from "@/components/ui/use-toast" + +export function UserManagement() { + const { getAllUsers, updateUserRole, deleteUser, adminResetPassword, user: currentUser } = useAuth() + const { toast } = useToast() + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + + // State for reset password dialog + const [resetDialogOpen, setResetDialogOpen] = useState(false) + const [selectedUser, setSelectedUser] = useState(null) + const [newPassword, setNewPassword] = useState("") + + const loadUsers = async () => { + setLoading(true) + const data = await getAllUsers() + setUsers(data) + setLoading(false) + } + + useEffect(() => { + loadUsers() + }, []) + + const handleRoleChange = async (userId: number, newRole: string) => { + const success = await updateUserRole(userId, newRole) + if (success) { + toast({ title: "Rolle aktualisiert", description: `User ist jetzt ${newRole}` }) + loadUsers() + } else { + toast({ title: "Fehler", description: "Rolle konnte nicht geändert werden", variant: "destructive" }) + } + } + + const handleDelete = async (userId: number) => { + if (confirm("Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?")) { + const success = await deleteUser(userId) + if (success) { + toast({ title: "Benutzer gelöscht" }) + loadUsers() + } else { + toast({ title: "Fehler", description: "Benutzer konnte nicht gelöscht werden", variant: "destructive" }) + } + } + } + + const handleResetPassword = async () => { + if (!selectedUser || !newPassword) return + const success = await adminResetPassword(selectedUser.id, newPassword) + if (success) { + toast({ title: "Passwort zurückgesetzt" }) + setResetDialogOpen(false) + setNewPassword("") + setSelectedUser(null) + } else { + toast({ title: "Fehler", description: "Passwort konnte nicht zurückgesetzt werden", variant: "destructive" }) + } + } + + if (loading) return
Lade Benutzer...
+ + return ( +
+
+ + + + Name + Email + Rolle + + + + + {users.map((user) => ( + + {user.name} + {user.email} + +
+ {user.role === "ADMIN" && } + {user.role === "LEHRKRAFT" && Lehrkraft} + {user.role === "RAUMBETREUER" && Raumbetreuer} + {user.role === "ADMIN" && Admin} +
+
+ + + + + + + Aktionen + navigator.clipboard.writeText(user.email)}> + Email kopieren + + + + {/* Safety: Cannot modify ID 1 and cannot modify self here ideally, but backend blocks ID 1 */} + {user.id !== 1 && ( + <> + { + setSelectedUser(user) + setResetDialogOpen(true) + }}> + Passwort zurücksetzen + + + {user.role !== "ADMIN" && ( + handleRoleChange(user.id, "ADMIN")}> + Zum Admin machen + + )} + {user.role !== "LEHRKRAFT" && ( + handleRoleChange(user.id, "LEHRKRAFT")}> + Zum Lehrer herabstufen + + )} + {user.role !== "RAUMBETREUER" && ( + handleRoleChange(user.id, "RAUMBETREUER")}> + Zum Raumbetreuer machen + + )} + + handleDelete(user.id)} className="text-destructive"> + Löschen + + + )} + + + +
+ ))} +
+
+
+ + + + + Passwort zurücksetzen für {selectedUser?.name} + + Setzen Sie ein neues Passwort für diesen Benutzer. + + +
+
+ + setNewPassword(e.target.value)} + className="col-span-3" + /> +
+
+ + + + +
+
+
+ ) +} diff --git a/Frontend/components/layout/app-shell.tsx b/Frontend/components/layout/app-shell.tsx index 11419e5..d50dd97 100644 --- a/Frontend/components/layout/app-shell.tsx +++ b/Frontend/components/layout/app-shell.tsx @@ -16,8 +16,20 @@ import { DropdownMenuSubContent, DropdownMenuCheckboxItem, } from "@/components/ui/dropdown-menu" -import { LogOut, User, Building2, Cpu } from "lucide-react" +import { LogOut, User, Building2, Cpu, ShieldAlert, KeyRound } from "lucide-react" import { ModeToggle } from "@/components/mode-toggle" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { useState } from "react" +import { useToast } from "@/components/ui/use-toast" // Defines props for the AppShell component interface AppShellProps { @@ -27,7 +39,25 @@ interface AppShellProps { // AppShell component provides the main layout structure for the application export function AppShell({ children }: AppShellProps) { // Access authentication context for user info, logout function, room data, and room update function - const { user, logout, rooms, updateRooms } = useAuth() + const { user, logout, rooms, updateRooms, changePassword } = useAuth() + const { toast } = useToast() + + const [changePasswordOpen, setChangePasswordOpen] = useState(false) + const [currentPassword, setCurrentPassword] = useState("") + const [newPassword, setNewPassword] = useState("") + + const handleChangePassword = async () => { + if (!currentPassword || !newPassword) return + const success = await changePassword(currentPassword, newPassword) + if (success) { + toast({ title: "Passwort geändert" }) + setChangePasswordOpen(false) + setCurrentPassword("") + setNewPassword("") + } else { + toast({ title: "Fehler", description: "Passwort konnte nicht geändert werden", variant: "destructive" }) + } + } // Generates user initials for the avatar fallback const initials = user?.name @@ -57,13 +87,19 @@ export function AppShell({ children }: AppShellProps) {
{/* Display user role for larger screens */}
- {user?.role === "LEHRKRAFT" ? ( + {user?.role === "ADMIN" ? ( + + ) : user?.role === "LEHRKRAFT" ? ( ) : ( )} - - {user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"} + + {user?.role === "ADMIN" + ? "Admin" + : user?.role === "LEHRKRAFT" + ? "Lehrkraft" + : "Raumbetreuer"}
@@ -88,9 +124,17 @@ export function AppShell({ children }: AppShellProps) { {/* Display user role in the dropdown */} - - {user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"} + {user?.role === "ADMIN" ? : } + + {user?.role === "ADMIN" ? "Admin" : user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"} + + + setChangePasswordOpen(true)}> + + Passwort ändern + + {/* Room supervision management for 'RAUMBETREUER' role */} {user?.role === "RAUMBETREUER" && user.supervisedRooms && ( @@ -143,6 +187,41 @@ export function AppShell({ children }: AppShellProps) {
+ + + + Passwort ändern + + Geben Sie Ihr aktuelles Passwort und das neue Passwort ein. + + +
+
+ + setCurrentPassword(e.target.value)} + /> +
+
+ + setNewPassword(e.target.value)} + /> +
+
+ + + + +
+
+ {/* Main content area */}
{children}
diff --git a/Frontend/lib/auth-context.tsx b/Frontend/lib/auth-context.tsx index a58897e..ba9fdc7 100644 --- a/Frontend/lib/auth-context.tsx +++ b/Frontend/lib/auth-context.tsx @@ -18,6 +18,11 @@ interface AuthContextType { updateTicketStatus: (ticketId: number, status: TicketStatus) => Promise deleteTicket: (ticketId: number) => Promise updateRooms: (roomIds: number[]) => Promise + changePassword: (password: string, newPassword: string) => Promise + getAllUsers: () => Promise + updateUserRole: (userId: number, role: string) => Promise + deleteUser: (userId: number) => Promise + adminResetPassword: (userId: number, password: string) => Promise } const AuthContext = createContext(null) @@ -195,6 +200,90 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [authHeader, fetchTickets]) + const changePassword = useCallback(async (password: string, newPassword: string) => { + if (!authHeader) return false + try { + const res = await fetch(`${API_URL}/users/password`, { + method: "PUT", + headers: { + "Authorization": authHeader, + "Content-Type": "application/json" + }, + body: JSON.stringify({ currentPassword: password, newPassword }) + }) + if (!res.ok) throw new Error("Failed to change password") + return true + } catch (e) { + console.error(e) + return false + } + }, [authHeader]) + + const getAllUsers = useCallback(async () => { + if (!authHeader) return [] + try { + const res = await fetch(`${API_URL}/users`, { + headers: { "Authorization": authHeader } + }) + if (res.ok) { + return await res.json() + } + } catch (e) { + console.error(e) + } + return [] + }, [authHeader]) + + const updateUserRole = useCallback(async (userId: number, role: string) => { + if (!authHeader) return false + try { + const res = await fetch(`${API_URL}/users/${userId}/role`, { + method: "PUT", + headers: { + "Authorization": authHeader, + "Content-Type": "application/json" + }, + body: JSON.stringify({ role }) + }) + return res.ok + } catch (e) { + console.error(e) + return false + } + }, [authHeader]) + + const deleteUser = useCallback(async (userId: number) => { + if (!authHeader) return false + try { + const res = await fetch(`${API_URL}/users/${userId}`, { + method: "DELETE", + headers: { "Authorization": authHeader } + }) + return res.ok + } catch (e) { + console.error(e) + return false + } + }, [authHeader]) + + const adminResetPassword = useCallback(async (userId: number, password: string) => { + if (!authHeader) return false + try { + const res = await fetch(`${API_URL}/users/${userId}/password`, { + method: "PUT", + headers: { + "Authorization": authHeader, + "Content-Type": "application/json" + }, + body: JSON.stringify({ password }) + }) + return res.ok + } catch (e) { + console.error(e) + return false + } + }, [authHeader]) + return ( {children}