negro
This commit is contained in:
parent
dfef90726c
commit
30e29363a4
7 changed files with 526 additions and 7 deletions
|
|
@ -5,7 +5,10 @@ import de.itsolutions.ticketsystem.entity.User;
|
||||||
import de.itsolutions.ticketsystem.service.AuthService;
|
import de.itsolutions.ticketsystem.service.AuthService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST controller for user-related operations, excluding authentication.
|
* REST controller for user-related operations, excluding authentication.
|
||||||
|
|
@ -34,4 +37,35 @@ public class UserController {
|
||||||
public ResponseEntity<User> updateTheme(@RequestBody Dtos.ThemeUpdateRequest request, Principal principal) {
|
public ResponseEntity<User> updateTheme(@RequestBody Dtos.ThemeUpdateRequest request, Principal principal) {
|
||||||
return ResponseEntity.ok(authService.updateTheme(principal.getName(), request.getTheme()));
|
return ResponseEntity.ok(authService.updateTheme(principal.getName(), request.getTheme()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/password")
|
||||||
|
public ResponseEntity<Void> changePassword(@RequestBody Dtos.ChangePasswordRequest request, Principal principal) {
|
||||||
|
authService.changePassword(principal.getName(), request.getCurrentPassword(), request.getNewPassword());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<User>> getAllUsers() {
|
||||||
|
// In a real app, add @PreAuthorize("hasRole('ADMIN')") or check role manually
|
||||||
|
return ResponseEntity.ok(authService.getAllUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/role")
|
||||||
|
public ResponseEntity<User> updateUserRole(@PathVariable Long id, @RequestBody Map<String, String> payload) {
|
||||||
|
String newRole = payload.get("role");
|
||||||
|
return ResponseEntity.ok(authService.updateUserRole(id, newRole));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
||||||
|
authService.deleteUser(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/password")
|
||||||
|
public ResponseEntity<Void> adminResetPassword(@PathVariable Long id, @RequestBody Map<String, String> payload) {
|
||||||
|
String newPassword = payload.get("password");
|
||||||
|
authService.adminResetPassword(id, newPassword);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,4 +200,18 @@ public class Dtos {
|
||||||
*/
|
*/
|
||||||
public void setTheme(String theme) { this.theme = theme; }
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,4 +150,85 @@ public class AuthService {
|
||||||
user.setTheme(theme);
|
user.setTheme(theme);
|
||||||
return userRepository.save(user);
|
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<User> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useAuth } from "@/lib/auth-context"
|
import { useAuth } from "@/lib/auth-context"
|
||||||
import { TicketTable } from "@/components/tickets/ticket-table"
|
import { TicketTable } from "@/components/tickets/ticket-table"
|
||||||
import { RoomManagement } from "@/components/dashboard/room-management"
|
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { ShieldAlert, Users } from "lucide-react"
|
import { ShieldAlert, Users } from "lucide-react"
|
||||||
|
|
||||||
|
|
@ -40,6 +41,17 @@ export function AdminDashboard() {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* User Management Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Benutzerverwaltung</CardTitle>
|
||||||
|
<CardDescription>Rollen zuweisen und Nutzer verwalten</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<UserManagement />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<RoomManagement />
|
<RoomManagement />
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
||||||
205
Frontend/components/dashboard/user-management.tsx
Normal file
205
Frontend/components/dashboard/user-management.tsx
Normal file
|
|
@ -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<User[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
// State for reset password dialog
|
||||||
|
const [resetDialogOpen, setResetDialogOpen] = useState(false)
|
||||||
|
const [selectedUser, setSelectedUser] = useState<User | null>(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 <div>Lade Benutzer...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Rolle</TableHead>
|
||||||
|
<TableHead className="w-[70px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell className="font-medium">{user.name}</TableCell>
|
||||||
|
<TableCell>{user.email}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{user.role === "ADMIN" && <ShieldAlert className="h-4 w-4 text-destructive" />}
|
||||||
|
{user.role === "LEHRKRAFT" && <span className="text-muted-foreground">Lehrkraft</span>}
|
||||||
|
{user.role === "RAUMBETREUER" && <span className="text-primary">Raumbetreuer</span>}
|
||||||
|
{user.role === "ADMIN" && <span className="font-bold text-destructive">Admin</span>}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(user.email)}>
|
||||||
|
Email kopieren
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Safety: Cannot modify ID 1 and cannot modify self here ideally, but backend blocks ID 1 */}
|
||||||
|
{user.id !== 1 && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onClick={() => {
|
||||||
|
setSelectedUser(user)
|
||||||
|
setResetDialogOpen(true)
|
||||||
|
}}>
|
||||||
|
<KeyRound className="mr-2 h-4 w-4" /> Passwort zurücksetzen
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{user.role !== "ADMIN" && (
|
||||||
|
<DropdownMenuItem onClick={() => handleRoleChange(user.id, "ADMIN")}>
|
||||||
|
<ShieldAlert className="mr-2 h-4 w-4" /> Zum Admin machen
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{user.role !== "LEHRKRAFT" && (
|
||||||
|
<DropdownMenuItem onClick={() => handleRoleChange(user.id, "LEHRKRAFT")}>
|
||||||
|
Zum Lehrer herabstufen
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{user.role !== "RAUMBETREUER" && (
|
||||||
|
<DropdownMenuItem onClick={() => handleRoleChange(user.id, "RAUMBETREUER")}>
|
||||||
|
<Shield className="mr-2 h-4 w-4" /> Zum Raumbetreuer machen
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => handleDelete(user.id)} className="text-destructive">
|
||||||
|
<Trash className="mr-2 h-4 w-4" /> Löschen
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={resetDialogOpen} onOpenChange={setResetDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Passwort zurücksetzen für {selectedUser?.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Setzen Sie ein neues Passwort für diesen Benutzer.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="new-pass" className="text-right">
|
||||||
|
Neues Passwort
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="new-pass"
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setResetDialogOpen(false)}>Abbrechen</Button>
|
||||||
|
<Button onClick={handleResetPassword}>Speichern</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,8 +16,20 @@ import {
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} 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 { 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
|
// Defines props for the AppShell component
|
||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
|
|
@ -27,7 +39,25 @@ interface AppShellProps {
|
||||||
// AppShell component provides the main layout structure for the application
|
// AppShell component provides the main layout structure for the application
|
||||||
export function AppShell({ children }: AppShellProps) {
|
export function AppShell({ children }: AppShellProps) {
|
||||||
// Access authentication context for user info, logout function, room data, and room update function
|
// 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
|
// Generates user initials for the avatar fallback
|
||||||
const initials = user?.name
|
const initials = user?.name
|
||||||
|
|
@ -57,13 +87,19 @@ export function AppShell({ children }: AppShellProps) {
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Display user role for larger screens */}
|
{/* Display user role for larger screens */}
|
||||||
<div className="hidden items-center gap-2 rounded-lg bg-muted/50 px-3 py-1.5 md:flex">
|
<div className="hidden items-center gap-2 rounded-lg bg-muted/50 px-3 py-1.5 md:flex">
|
||||||
{user?.role === "LEHRKRAFT" ? (
|
{user?.role === "ADMIN" ? (
|
||||||
|
<ShieldAlert className="h-4 w-4 text-destructive" />
|
||||||
|
) : user?.role === "LEHRKRAFT" ? (
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
<User className="h-4 w-4 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium capitalize">
|
<span className={`text-sm font-medium capitalize ${user?.role === "ADMIN" ? "text-destructive" : ""}`}>
|
||||||
{user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"}
|
{user?.role === "ADMIN"
|
||||||
|
? "Admin"
|
||||||
|
: user?.role === "LEHRKRAFT"
|
||||||
|
? "Lehrkraft"
|
||||||
|
: "Raumbetreuer"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,9 +124,17 @@ export function AppShell({ children }: AppShellProps) {
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{/* Display user role in the dropdown */}
|
{/* Display user role in the dropdown */}
|
||||||
<DropdownMenuItem className="text-muted-foreground">
|
<DropdownMenuItem className="text-muted-foreground">
|
||||||
<User className="mr-2 h-4 w-4" />
|
{user?.role === "ADMIN" ? <ShieldAlert className="mr-2 h-4 w-4 text-destructive" /> : <User className="mr-2 h-4 w-4" />}
|
||||||
<span className="capitalize">{user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"}</span>
|
<span className={`capitalize ${user?.role === "ADMIN" ? "text-destructive font-bold" : ""}`}>
|
||||||
|
{user?.role === "ADMIN" ? "Admin" : user?.role === "LEHRKRAFT" ? "Lehrkraft" : "Raumbetreuer"}
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={() => setChangePasswordOpen(true)}>
|
||||||
|
<KeyRound className="mr-2 h-4 w-4" />
|
||||||
|
<span>Passwort ändern</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* Room supervision management for 'RAUMBETREUER' role */}
|
{/* Room supervision management for 'RAUMBETREUER' role */}
|
||||||
{user?.role === "RAUMBETREUER" && user.supervisedRooms && (
|
{user?.role === "RAUMBETREUER" && user.supervisedRooms && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
|
|
@ -143,6 +187,41 @@ export function AppShell({ children }: AppShellProps) {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<Dialog open={changePasswordOpen} onOpenChange={setChangePasswordOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Passwort ändern</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Geben Sie Ihr aktuelles Passwort und das neue Passwort ein.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="current">Aktuelles Passwort</Label>
|
||||||
|
<Input
|
||||||
|
id="current"
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="new">Neues Passwort</Label>
|
||||||
|
<Input
|
||||||
|
id="new"
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setChangePasswordOpen(false)}>Abbrechen</Button>
|
||||||
|
<Button onClick={handleChangePassword}>Speichern</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<main className="w-full max-w-[95%] mx-auto px-4 py-8">{children}</main>
|
<main className="w-full max-w-[95%] mx-auto px-4 py-8">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ interface AuthContextType {
|
||||||
updateTicketStatus: (ticketId: number, status: TicketStatus) => Promise<void>
|
updateTicketStatus: (ticketId: number, status: TicketStatus) => Promise<void>
|
||||||
deleteTicket: (ticketId: number) => Promise<void>
|
deleteTicket: (ticketId: number) => Promise<void>
|
||||||
updateRooms: (roomIds: number[]) => Promise<void>
|
updateRooms: (roomIds: number[]) => Promise<void>
|
||||||
|
changePassword: (password: string, newPassword: string) => Promise<boolean>
|
||||||
|
getAllUsers: () => Promise<User[]>
|
||||||
|
updateUserRole: (userId: number, role: string) => Promise<boolean>
|
||||||
|
deleteUser: (userId: number) => Promise<boolean>
|
||||||
|
adminResetPassword: (userId: number, password: string) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | null>(null)
|
const AuthContext = createContext<AuthContextType | null>(null)
|
||||||
|
|
@ -195,6 +200,90 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [authHeader, fetchTickets])
|
}, [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 (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -209,6 +298,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
updateTicketStatus,
|
updateTicketStatus,
|
||||||
deleteTicket,
|
deleteTicket,
|
||||||
updateRooms,
|
updateRooms,
|
||||||
|
changePassword,
|
||||||
|
getAllUsers,
|
||||||
|
updateUserRole,
|
||||||
|
deleteUser,
|
||||||
|
adminResetPassword,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue