add user management features: implement user creation, editing, and deletion; enhance dashboard with user selection prompt; improve token verification and alert handling

This commit is contained in:
2025-08-31 20:02:51 +02:00
parent 217803ba8f
commit c77bef5cf3
10 changed files with 527 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ import UserTable from "../components/UserTable";
import ItemTable from "../components/ItemTable"; import ItemTable from "../components/ItemTable";
import LockerTable from "../components/LockerTable"; import LockerTable from "../components/LockerTable";
import LoanTable from "../components/LoanTable"; import LoanTable from "../components/LoanTable";
import { MoveLeft } from "lucide-react";
type DashboardProps = { type DashboardProps = {
onLogout?: () => void; onLogout?: () => void;
@@ -46,7 +47,23 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
</Flex> </Flex>
</Flex> </Flex>
<Box as="main" flex="1" p={6}> <Box as="main" flex="1" p={6}>
{activeView === "" && <Text>Bitte wählen Sie eine Ansicht aus.</Text>} {activeView === "" && (
<Flex
align="center"
gap={3}
p={4}
border="1px dashed"
borderColor="gray.300"
borderRadius="md"
bg="gray.50"
color="gray.700"
fontSize="lg"
fontWeight="semibold"
>
<MoveLeft size={20} />
Bitte wählen Sie eine Ansicht aus.
</Flex>
)}
{activeView === "User" && <UserTable />} {activeView === "User" && <UserTable />}
{activeView === "Ausleihen" && <LoanTable />} {activeView === "Ausleihen" && <LoanTable />}
{activeView === "Gegenstände" && <ItemTable />} {activeView === "Gegenstände" && <ItemTable />}

View File

@@ -13,7 +13,22 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
useEffect(() => { useEffect(() => {
if (Cookies.get("token")) { if (Cookies.get("token")) {
setIsLoggedIn(true); const verifyToken = async () => {
const response = await fetch("http://localhost:8002/api/verifyToken", {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
if (response.ok) {
setIsLoggedIn(true);
} else {
Cookies.remove("token");
setIsLoggedIn(false);
window.location.reload();
}
};
verifyToken();
} }
}, []); }, []);

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { useState } from "react"; import { useState } from "react";
import { loginFunc } from "@/utils/loginUser"; import { loginFunc } from "@/utils/loginUser";
import MyAlert from "@/components/myChakra/MyAlert"; import MyAlert from "../components/myChakra/MyAlert";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react"; import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => { const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
@@ -50,7 +50,7 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
</Stack> </Stack>
</Card.Body> </Card.Body>
<Card.Footer justifyContent="flex-end"> <Card.Footer justifyContent="flex-end">
{isError && <MyAlert title={errorMsg} description={errorDsc} />} {isError && <MyAlert status="error" title={errorMsg} description={errorDsc} />}
<Button onClick={() => handleLogin()} variant="solid"> <Button onClick={() => handleLogin()} variant="solid">
Login Login
</Button> </Button>

View File

@@ -0,0 +1,79 @@
import React from "react";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import { createUser } from "@/utils/userActions";
type AddFormProps = {
onClose: () => void;
alert: (
status: "success" | "error",
message: string,
description: string
) => void;
};
const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<Card.Root maxW="sm">
<Card.Header>
<Card.Title>Neuen Nutzer erstellen</Card.Title>
<Card.Description>
Füllen Sie das folgende Formular aus, um einen Nutzer zu erstellen.
</Card.Description>
</Card.Header>
<Card.Body>
<Stack gap="4" w="full">
<Field.Root>
<Field.Label>Username</Field.Label>
<Input id="username" />
</Field.Root>
<Field.Root>
<Field.Label>Password</Field.Label>
<Input id="password" type="password" />
</Field.Root>
<Field.Root>
<Field.Label>Role</Field.Label>
<Input id="role" type="number" />
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end">
<Button variant="outline" onClick={onClose}>
Abbrechen
</Button>
<Button
variant="solid"
onClick={async () => {
const username =
(
document.getElementById("username") as HTMLInputElement
)?.value.trim() || "";
const password =
(document.getElementById("password") as HTMLInputElement)
?.value || "";
const role = Number(
(document.getElementById("role") as HTMLInputElement)?.value
);
if (!username || !password || Number.isNaN(role)) return;
const res = await createUser(username, role, password);
if (res.success) {
alert(
"success",
"Nutzer erstellt",
"Der Nutzer wurde erfolgreich erstellt."
);
onClose();
}
}}
>
Erstellen
</Button>
</Card.Footer>
</Card.Root>
</div>
);
};
export default AddForm;

View File

@@ -1,7 +1,253 @@
import React from "react"; import React from "react";
import { useState, useEffect } from "react";
import {
Table,
Spinner,
Text,
VStack,
Button,
Input,
HStack,
IconButton,
} from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip";
import { fetchUserData } from "@/utils/fetcher";
import { Save, Trash2, RefreshCcwDot, CirclePlus } from "lucide-react";
import { handleDelete, handleEdit } from "@/utils/userActions";
import MyAlert from "./myChakra/MyAlert";
import AddForm from "./AddForm";
type User = {
id: number;
username: string;
password: string;
role: string;
entry_created_at: string;
};
const UserTable: React.FC = () => { const UserTable: React.FC = () => {
return <>User Table</>; const [isLoading, setIsLoading] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const [isError, setIsError] = useState(false);
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
const [errorMessage, setErrorMessage] = useState("");
const [errorDsc, setErrorDsc] = useState("");
const [reload, setReload] = useState(false);
const [addForm, setAddForm] = useState(false);
const setError = (
status: "error" | "success",
message: string,
description: string
) => {
setIsError(false);
setErrorStatus(status);
setErrorMessage(message);
setErrorDsc(description);
setIsError(true);
};
const handleInputChange = (userId: number, field: string, value: string) => {
setUsers((prevUsers) =>
prevUsers.map((user) =>
user.id === userId ? { ...user, [field]: value } : user
)
);
};
useEffect(() => {
const fetchUsers = async () => {
setIsLoading(true);
try {
const data = await fetchUserData();
console.log("user api response", data);
if (Array.isArray(data)) {
setUsers(data);
} else {
setError(
"error",
"Failed to load users",
"Invalid data format received"
);
}
} catch (e) {
console.error("Failed to fetch users", e);
if (e instanceof Error) {
setError(
"error",
"Failed to fetch users",
e.message || "Unknown error"
);
} else {
setError("error", "Failed to fetch users", "Unknown error");
}
} finally {
setIsLoading(false);
}
};
fetchUsers();
}, [reload]);
return (
<>
{/* Action toolbar */}
<HStack
mb={4}
gap={3}
justify="flex-start"
align="center"
flexWrap="wrap"
>
<Tooltip content="Benutzer neu laden" openDelay={300}>
<IconButton
aria-label="Refresh users"
size="sm"
variant="outline"
rounded="md"
shadow="sm"
_hover={{ shadow: "md", transform: "translateY(-2px)" }}
_active={{ transform: "translateY(0)" }}
onClick={() => setReload(!reload)}
>
<RefreshCcwDot size={18} />
</IconButton>
</Tooltip>
<Tooltip content="Neuen Nutzer hinzufügen" openDelay={300}>
<Button
size="sm"
colorPalette="teal"
variant="solid"
rounded="md"
fontWeight="semibold"
shadow="sm"
_hover={{ shadow: "md", bg: "colorPalette.600" }}
_active={{ bg: "colorPalette.700" }}
onClick={() => {
setAddForm(true);
}}
>
<CirclePlus size={18} style={{ marginRight: 6 }} />
Neuen Nutzer hinzufügen
</Button>
</Tooltip>
</HStack>
{/* End action toolbar */}
{isError && (
<MyAlert
status={errorStatus}
description={errorDsc}
title={errorMessage}
/>
)}
{addForm && (
<AddForm
onClose={() => {
setAddForm(false);
setReload(!reload);
}}
alert={setError}
/>
)}
{isLoading && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">Loading...</Text>
</VStack>
)}
{!isLoading && (
<Table.Root size="sm" striped>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>id</Table.ColumnHeader>
<Table.ColumnHeader>Username</Table.ColumnHeader>
<Table.ColumnHeader>Password</Table.ColumnHeader>
<Table.ColumnHeader>Role</Table.ColumnHeader>
<Table.ColumnHeader>Entry Created At</Table.ColumnHeader>
<Table.ColumnHeader>Actions</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{users.map((user) => (
<Table.Row key={user.id}>
<Table.Cell>{user.id}</Table.Cell>
<Table.Cell>
<Input
onChange={(e) =>
handleInputChange(user.id, "username", e.target.value)
}
value={user.username}
/>
</Table.Cell>
<Table.Cell>
<Input
onChange={(e) =>
handleInputChange(user.id, "password", e.target.value)
}
value={user.password}
/>
</Table.Cell>
<Table.Cell>
<Input
type="number"
onChange={(e) =>
handleInputChange(user.id, "role", e.target.value)
}
value={user.role}
/>
</Table.Cell>
<Table.Cell>{user.entry_created_at}</Table.Cell>
<Table.Cell>
<Button
onClick={() =>
handleEdit(
user.id,
user.username,
user.role,
user.password
).then((response) => {
if (response.success) {
setError(
"success",
"User edited",
"The user has been successfully edited."
);
}
})
}
colorPalette="teal"
size="sm"
>
<Save />
</Button>
<Button
onClick={() =>
handleDelete(user.id).then((response) => {
if (response.success) {
setUsers(users.filter((u) => u.id !== user.id));
setError(
"success",
"User deleted",
"The user has been successfully deleted."
);
}
})
}
colorPalette="red"
size="sm"
ml={2}
>
<Trash2 />
</Button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
)}
</>
);
}; };
export default UserTable; export default UserTable;

View File

@@ -2,13 +2,14 @@ import React from "react";
import { Alert } from "@chakra-ui/react"; import { Alert } from "@chakra-ui/react";
type MyAlertProps = { type MyAlertProps = {
status: "error" | "success";
title: string; title: string;
description: string; description: string;
}; };
const MyAlert: React.FC<MyAlertProps> = ({ title, description }) => { const MyAlert: React.FC<MyAlertProps> = ({ title, description, status }) => {
return ( return (
<Alert.Root status="error"> <Alert.Root status={status}>
<Alert.Indicator /> <Alert.Indicator />
<Alert.Content> <Alert.Content>
<Alert.Title>{title}</Alert.Title> <Alert.Title>{title}</Alert.Title>

View File

@@ -0,0 +1,11 @@
import Cookies from "js-cookie";
export const fetchUserData = async () => {
const response = await fetch("http://localhost:8002/api/allUsers", {
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
const data = await response.json();
return data;
};

View File

@@ -0,0 +1,77 @@
import Cookies from "js-cookie";
export const handleDelete = async (userId: number) => {
try {
const response = await fetch(
`http://localhost:8002/api/deleteUser/${userId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
}
);
if (!response.ok) {
throw new Error("Failed to delete user");
}
return { success: true };
} catch (error) {
console.error("Error deleting user:", error);
return { success: false };
}
};
export const handleEdit = async (
userId: number,
username: string,
role: string,
password: string
) => {
try {
const response = await fetch(
`http://localhost:8002/api/editUser/${userId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`,
},
body: JSON.stringify({ username, role, password }),
}
);
if (!response.ok) {
throw new Error("Failed to edit user");
}
return { success: true };
} catch (error) {
console.error("Error editing user:", error);
return { success: false };
}
};
export const createUser = async (
username: string,
role: number,
password: string
) => {
try {
const response = await fetch(
`http://localhost:8002/api/createUser`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`,
},
body: JSON.stringify({ username, role, password }),
}
);
if (!response.ok) {
throw new Error("Failed to create user");
}
return { success: true };
} catch (error) {
console.error("Error creating user:", error);
return { success: false };
}
};

View File

@@ -10,6 +10,10 @@ import {
onTake, onTake,
loginAdmin, loginAdmin,
onReturn, onReturn,
getAllUsers,
deleteUserID,
handleEdit,
createUser,
} from "../services/database.js"; } from "../services/database.js";
import { authenticate, generateToken } from "../services/tokenService.js"; import { authenticate, generateToken } from "../services/tokenService.js";
const router = express.Router(); const router = express.Router();
@@ -167,8 +171,6 @@ router.post("/createLoan", authenticate, async (req, res) => {
} }
}); });
// Admin panel functions // Admin panel functions
router.post("/loginAdmin", async (req, res) => { router.post("/loginAdmin", async (req, res) => {
@@ -196,4 +198,44 @@ router.post("/loginAdmin", async (req, res) => {
return res.status(401).json({ message: "Invalid credentials" }); return res.status(401).json({ message: "Invalid credentials" });
}); });
router.get("/allUsers", authenticate, async (req, res) => {
const result = await getAllUsers();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to fetch users" });
});
router.delete("/deleteUser/:id", authenticate, async (req, res) => {
const userId = req.params.id;
const result = await deleteUserID(userId);
if (result.success) {
return res.status(200).json({ message: "User deleted successfully" });
}
return res.status(500).json({ message: "Failed to delete user" });
});
router.get("/verifyToken", authenticate, async (req, res) => {
res.status(200).json({ message: "Token is valid" });
});
router.put("/editUser/:id", authenticate, async (req, res) => {
const userId = req.params.id;
const { username, role, password } = req.body || {};
const result = await handleEdit(userId, username, role, password);
if (result.success) {
return res.status(200).json({ message: "User edited successfully" });
}
return res.status(500).json({ message: "Failed to edit user" });
});
router.post("/createUser", authenticate, async (req, res) => {
const { username, role, password } = req.body || {};
const result = await createUser(username, role, password);
if (result.success) {
return res.status(201).json({ message: "User created successfully" });
}
return res.status(500).json({ message: "Failed to create user" });
});
export default router; export default router;

View File

@@ -328,3 +328,33 @@ export const loginAdmin = async (username, password) => {
if (result.length > 0) return { success: true, data: result[0] }; if (result.length > 0) return { success: true, data: result[0] };
return { success: false }; return { success: false };
}; };
export const getAllUsers = async () => {
const [result] = await pool.query("SELECT * FROM users");
if (result.length > 0) return { success: true, data: result };
return { success: false };
};
export const deleteUserID = async (userId) => {
const [result] = await pool.query("DELETE FROM users WHERE id = ?", [userId]);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const handleEdit = async (userId, username, role, password) => {
const [result] = await pool.query(
"UPDATE users SET username = ?, role = ?, password = ? WHERE id = ?",
[username, role, password, userId]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const createUser = async (username, role, password) => {
const [result] = await pool.query(
"INSERT INTO users (username, role, password) VALUES (?, ?, ?)",
[username, role, password]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};