diff --git a/admin/src/Layout/Dashboard.tsx b/admin/src/Layout/Dashboard.tsx index 23cdbf6..0d60ae5 100644 --- a/admin/src/Layout/Dashboard.tsx +++ b/admin/src/Layout/Dashboard.tsx @@ -6,6 +6,7 @@ import UserTable from "../components/UserTable"; import ItemTable from "../components/ItemTable"; import LockerTable from "../components/LockerTable"; import LoanTable from "../components/LoanTable"; +import { MoveLeft } from "lucide-react"; type DashboardProps = { onLogout?: () => void; @@ -46,7 +47,23 @@ const Dashboard: React.FC = ({ onLogout }) => { - {activeView === "" && Bitte wählen Sie eine Ansicht aus.} + {activeView === "" && ( + + + Bitte wählen Sie eine Ansicht aus. + + )} {activeView === "User" && } {activeView === "Ausleihen" && } {activeView === "Gegenstände" && } diff --git a/admin/src/Layout/Layout.tsx b/admin/src/Layout/Layout.tsx index a329532..b7df89a 100644 --- a/admin/src/Layout/Layout.tsx +++ b/admin/src/Layout/Layout.tsx @@ -13,7 +13,22 @@ const Layout: React.FC = ({ children }) => { useEffect(() => { 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(); } }, []); diff --git a/admin/src/Layout/Login.tsx b/admin/src/Layout/Login.tsx index 934657b..8646f99 100644 --- a/admin/src/Layout/Login.tsx +++ b/admin/src/Layout/Login.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useState } from "react"; 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"; const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => { @@ -50,7 +50,7 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => { - {isError && } + {isError && } diff --git a/admin/src/components/AddForm.tsx b/admin/src/components/AddForm.tsx new file mode 100644 index 0000000..e40c80e --- /dev/null +++ b/admin/src/components/AddForm.tsx @@ -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 = ({ onClose, alert }) => { + return ( +
+ + + Neuen Nutzer erstellen + + Füllen Sie das folgende Formular aus, um einen Nutzer zu erstellen. + + + + + + Username + + + + Password + + + + Role + + + + + + + + + +
+ ); +}; + +export default AddForm; diff --git a/admin/src/components/UserTable.tsx b/admin/src/components/UserTable.tsx index a011e29..b9efaa6 100644 --- a/admin/src/components/UserTable.tsx +++ b/admin/src/components/UserTable.tsx @@ -1,7 +1,253 @@ 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 = () => { - return <>User Table; + const [isLoading, setIsLoading] = useState(false); + const [users, setUsers] = useState([]); + 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 */} + + + setReload(!reload)} + > + + + + + + + + + {/* End action toolbar */} + + {isError && ( + + )} + {addForm && ( + { + setAddForm(false); + setReload(!reload); + }} + alert={setError} + /> + )} + {isLoading && ( + + + Loading... + + )} + {!isLoading && ( + + + + id + Username + Password + Role + Entry Created At + Actions + + + + {users.map((user) => ( + + {user.id} + + + handleInputChange(user.id, "username", e.target.value) + } + value={user.username} + /> + + + + handleInputChange(user.id, "password", e.target.value) + } + value={user.password} + /> + + + + handleInputChange(user.id, "role", e.target.value) + } + value={user.role} + /> + + {user.entry_created_at} + + + + + + ))} + + + )} + + ); }; export default UserTable; diff --git a/admin/src/components/myChakra/MyAlert.tsx b/admin/src/components/myChakra/MyAlert.tsx index 7ec9af5..737e541 100644 --- a/admin/src/components/myChakra/MyAlert.tsx +++ b/admin/src/components/myChakra/MyAlert.tsx @@ -2,13 +2,14 @@ import React from "react"; import { Alert } from "@chakra-ui/react"; type MyAlertProps = { + status: "error" | "success"; title: string; description: string; }; -const MyAlert: React.FC = ({ title, description }) => { +const MyAlert: React.FC = ({ title, description, status }) => { return ( - + {title} diff --git a/admin/src/utils/fetcher.ts b/admin/src/utils/fetcher.ts new file mode 100644 index 0000000..a3bee7d --- /dev/null +++ b/admin/src/utils/fetcher.ts @@ -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; +}; diff --git a/admin/src/utils/userActions.ts b/admin/src/utils/userActions.ts new file mode 100644 index 0000000..1d7985c --- /dev/null +++ b/admin/src/utils/userActions.ts @@ -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 }; + } +}; diff --git a/backend/routes/api.js b/backend/routes/api.js index b5f4da1..30f4306 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -10,6 +10,10 @@ import { onTake, loginAdmin, onReturn, + getAllUsers, + deleteUserID, + handleEdit, + createUser, } from "../services/database.js"; import { authenticate, generateToken } from "../services/tokenService.js"; const router = express.Router(); @@ -167,8 +171,6 @@ router.post("/createLoan", authenticate, async (req, res) => { } }); - - // Admin panel functions router.post("/loginAdmin", async (req, res) => { @@ -196,4 +198,44 @@ router.post("/loginAdmin", async (req, res) => { 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; diff --git a/backend/services/database.js b/backend/services/database.js index f67c3e2..2d41101 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -328,3 +328,33 @@ export const loginAdmin = async (username, password) => { if (result.length > 0) return { success: true, data: result[0] }; 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 }; +};