Refactor loan and user management components and backend routes
- Updated LoanTable component to fetch loan data from new API endpoint and display notes. - Enhanced UserTable component to include additional user fields (first name, last name, email, admin status) and updated input handling. - Modified fetcher utility to use new user data API endpoint. - Adjusted login functionality to point to the new admin login endpoint and handle unauthorized access. - Refactored user actions utility to align with updated API endpoints for user management. - Updated backend routes for user and loan data management to reflect new structure and naming conventions. - Revised SQL schema and mock data to accommodate new fields and constraints. - Changed Docker configuration to use the new database name.
This commit is contained in:
@@ -3,11 +3,7 @@ import { useEffect } from "react";
|
||||
import Dashboard from "./Dashboard";
|
||||
import Login from "./Login";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
const Layout: React.FC = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
@@ -15,12 +11,15 @@ const Layout: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (Cookies.get("token")) {
|
||||
const verifyToken = async () => {
|
||||
const response = await fetch(`${API_BASE}/api/verifyToken`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/user-mgmt/verify-token`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
setIsLoggedIn(true);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Flex, VStack, Heading, Text, Link } from "@chakra-ui/react";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
type SidebarProps = {
|
||||
viewAusleihen: () => void;
|
||||
@@ -15,10 +17,22 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
viewUser,
|
||||
viewAPI,
|
||||
}) => {
|
||||
const [info, setInfo] = useState<any>(null);
|
||||
|
||||
const fetchInfo = async () => {
|
||||
const response = await fetch(`${API_BASE}/`);
|
||||
const data = await response.json();
|
||||
setInfo(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="aside"
|
||||
w="260px"
|
||||
w="180px"
|
||||
minH="100vh"
|
||||
bg="gray.800"
|
||||
color="gray.100"
|
||||
@@ -72,7 +86,33 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
</VStack>
|
||||
|
||||
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
||||
<Text>© Made with ❤️ by Theis Gaedigk</Text>
|
||||
<Text mb={2}>© Made with ❤️ by Theis Gaedigk</Text>
|
||||
{info ? (
|
||||
<Flex gap={2} wrap="wrap">
|
||||
<Box
|
||||
as="span"
|
||||
px={2}
|
||||
py={0.5}
|
||||
rounded="full"
|
||||
bg="gray.700"
|
||||
color="gray.200"
|
||||
>
|
||||
Panel {info?.["admin-panel-info"]?.version ?? "—"}
|
||||
</Box>
|
||||
<Box
|
||||
as="span"
|
||||
px={2}
|
||||
py={0.5}
|
||||
rounded="full"
|
||||
bg="gray.700"
|
||||
color="gray.200"
|
||||
>
|
||||
Backend {info?.["backend-info"]?.version ?? "—"}
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
<Text color="gray.600">Lade Versionsinfos…</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -17,17 +17,14 @@ import { useState, useEffect } from "react";
|
||||
import { deleteAPKey } from "@/utils/userActions";
|
||||
import AddAPIKey from "./AddAPIKey";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
apiKey: string;
|
||||
user: string;
|
||||
api_key: string;
|
||||
entry_name: string;
|
||||
entry_created_at: string;
|
||||
last_used_at: string | null;
|
||||
};
|
||||
|
||||
const APIKeyTable: React.FC = () => {
|
||||
@@ -56,13 +53,17 @@ const APIKeyTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/apiKeys`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/api-data/get-api-keys`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
setError("error", "Failed to fetch items", "There is an error");
|
||||
@@ -159,29 +160,37 @@ const APIKeyTable: React.FC = () => {
|
||||
<strong>API Key</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Benutzer</strong>
|
||||
<strong>Name</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag erstellt am</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Zuletzt benutzt am</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Aktionen</strong>
|
||||
</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{items.map((apiKey) => (
|
||||
<Table.Row key={apiKey.id}>
|
||||
<Table.Cell>{apiKey.id}</Table.Cell>
|
||||
<Table.Cell>{apiKey.apiKey}</Table.Cell>
|
||||
<Table.Cell>{apiKey.user}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(apiKey.entry_created_at)}</Table.Cell>
|
||||
{items.map((item) => (
|
||||
<Table.Row key={item.id}>
|
||||
<Table.Cell>{item.id}</Table.Cell>
|
||||
<Table.Cell>{item.api_key}</Table.Cell>
|
||||
<Table.Cell>{item.entry_name}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{!item.last_used_at
|
||||
? "Nie benutzt"
|
||||
: formatDateTime(item.last_used_at)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
deleteAPKey(apiKey.id).then((response) => {
|
||||
deleteAPKey(item.id).then((response) => {
|
||||
if (response.success) {
|
||||
setItems(items.filter((i) => i.id !== apiKey.id));
|
||||
setItems(items.filter((i) => i.id !== item.id));
|
||||
setError(
|
||||
"success",
|
||||
"Gegenstand gelöscht",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import React from "react";
|
||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Field,
|
||||
Input,
|
||||
Stack,
|
||||
InputGroup,
|
||||
Span,
|
||||
} from "@chakra-ui/react";
|
||||
import { createAPIentry } from "@/utils/userActions";
|
||||
import { useState } from "react";
|
||||
|
||||
type AddAPIKeyProps = {
|
||||
onClose: () => void;
|
||||
@@ -12,6 +21,8 @@ type AddAPIKeyProps = {
|
||||
};
|
||||
|
||||
const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
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">
|
||||
@@ -23,13 +34,26 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Stack gap="4" w="full">
|
||||
<InputGroup
|
||||
endElement={
|
||||
<Span color="fg.muted" textStyle="xs">
|
||||
{value.length} / {15}
|
||||
</Span>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
placeholder="Er muss 15 Zeichen lang sein"
|
||||
value={value}
|
||||
id="apiKey"
|
||||
maxLength={15}
|
||||
onChange={(e) => {
|
||||
setValue(e.currentTarget.value.slice(0, 15));
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Field.Root>
|
||||
<Field.Label>API key</Field.Label>
|
||||
<Input type="number" id="apiKey" />
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>Benutzer</Field.Label>
|
||||
<Input id="user" type="text" />
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Input id="name" type="text" />
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
@@ -44,14 +68,14 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
|
||||
(
|
||||
document.getElementById("apiKey") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const user =
|
||||
const name =
|
||||
(
|
||||
document.getElementById("user") as HTMLInputElement
|
||||
document.getElementById("name") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
|
||||
if (!apiKey || !user) return;
|
||||
if (!apiKey || !name) return;
|
||||
|
||||
const res = await createAPIentry(apiKey, user);
|
||||
const res = await createAPIentry(apiKey, name);
|
||||
if (res.success) {
|
||||
alert(
|
||||
"success",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import React from "react";
|
||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Field,
|
||||
Input,
|
||||
Stack,
|
||||
Text,
|
||||
Checkbox,
|
||||
} from "@chakra-ui/react";
|
||||
import { createUser } from "@/utils/userActions";
|
||||
|
||||
type AddFormProps = {
|
||||
@@ -12,73 +20,128 @@ type AddFormProps = {
|
||||
};
|
||||
|
||||
const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
|
||||
const [admin, setAdmin] = React.useState(false);
|
||||
|
||||
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
|
||||
);
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
if (!username || !password || Number.isNaN(role)) return;
|
||||
<Card.Body>
|
||||
<Stack gap="4" w="full">
|
||||
<Field.Root>
|
||||
<Field.Label>Benutzername</Field.Label>
|
||||
<Input id="username" />
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>Passwort</Field.Label>
|
||||
<Input id="password" type="password" />
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>Vorname</Field.Label>
|
||||
<Input id="firstname" />
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>Nachname</Field.Label>
|
||||
<Input id="lastname" />
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>E-Mail</Field.Label>
|
||||
<Input id="email" type="email" />
|
||||
</Field.Root>
|
||||
|
||||
const res = await createUser(username, role, password);
|
||||
if (res.success) {
|
||||
alert(
|
||||
"success",
|
||||
"Nutzer erstellt",
|
||||
"Der Nutzer wurde erfolgreich erstellt."
|
||||
{/* Kontrollierte Checkbox */}
|
||||
<Checkbox.Root
|
||||
checked={admin}
|
||||
onCheckedChange={(e: any) => setAdmin(Boolean(e?.checked ?? e))}
|
||||
>
|
||||
<Checkbox.HiddenInput />
|
||||
<Checkbox.Control />
|
||||
<Checkbox.Label>Admin</Checkbox.Label>
|
||||
</Checkbox.Root>
|
||||
|
||||
<Field.Root>
|
||||
<Field.Label>Rolle</Field.Label>
|
||||
<Input id="role" type="number" />
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
<Text>Der Benutzername kann nicht mehr geändert werden.</Text>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
type="submit"
|
||||
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
|
||||
);
|
||||
onClose();
|
||||
} else {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler beim Erstellen des Nutzers",
|
||||
"Es gab einen Fehler beim Erstellen des Nutzers. Vielleicht gibt es bereits einen Nutzer mit diesem Benutzernamen."
|
||||
const firstname =
|
||||
(
|
||||
document.getElementById("firstname") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const lastname =
|
||||
(
|
||||
document.getElementById("lastname") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const email =
|
||||
(
|
||||
document.getElementById("email") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
|
||||
// admin kommt jetzt zuverlässig aus dem State
|
||||
const res = await createUser(
|
||||
username,
|
||||
role,
|
||||
password,
|
||||
firstname,
|
||||
lastname,
|
||||
email,
|
||||
admin
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
if (res.success) {
|
||||
alert(
|
||||
"success",
|
||||
"Nutzer erstellt",
|
||||
"Der Nutzer wurde erfolgreich erstellt."
|
||||
);
|
||||
onClose();
|
||||
} else {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler beim Erstellen des Nutzers",
|
||||
"Es gab einen Fehler beim Erstellen des Nutzers. Vielleicht gibt es bereits einen Nutzer mit diesem Benutzernamen."
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,18 +30,17 @@ import {
|
||||
} from "@/utils/userActions";
|
||||
import AddItemForm from "./AddItemForm";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
item_name: string;
|
||||
can_borrow_role: string;
|
||||
inSafe: boolean;
|
||||
in_safe: boolean;
|
||||
entry_created_at: string;
|
||||
entry_updated_at: string;
|
||||
last_borrowed_person: string | null;
|
||||
currently_borrowing: string | null;
|
||||
};
|
||||
|
||||
const ItemTable: React.FC = () => {
|
||||
@@ -82,12 +81,15 @@ const ItemTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/allItems`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/item-data/all-items`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
@@ -193,6 +195,15 @@ const ItemTable: React.FC = () => {
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag erstellt am</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag aktualisiert am</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Letzte ausleihende Person</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Derzeit ausgeliehen von</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Aktionen</strong>
|
||||
</Table.ColumnHeader>
|
||||
@@ -229,31 +240,34 @@ const ItemTable: React.FC = () => {
|
||||
py={1}
|
||||
gap={2}
|
||||
variant="ghost"
|
||||
color={item.inSafe ? "green.600" : "red.600"}
|
||||
color={item.in_safe ? "green.600" : "red.600"}
|
||||
borderWidth="1px"
|
||||
borderColor={item.inSafe ? "green.300" : "red.300"}
|
||||
borderColor={item.in_safe ? "green.300" : "red.300"}
|
||||
_hover={{
|
||||
bg: item.inSafe ? "green.50" : "red.50",
|
||||
borderColor: item.inSafe ? "green.400" : "red.400",
|
||||
bg: item.in_safe ? "green.50" : "red.50",
|
||||
borderColor: item.in_safe ? "green.400" : "red.400",
|
||||
transform: "translateY(-1px)",
|
||||
shadow: "sm",
|
||||
}}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
aria-label={
|
||||
item.inSafe ? "Mark as not in safe" : "Mark as in safe"
|
||||
item.in_safe ? "Mark as not in safe" : "Mark as in safe"
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
as={item.inSafe ? CheckCircle2 : XCircle}
|
||||
as={item.in_safe ? CheckCircle2 : XCircle}
|
||||
boxSize={3.5}
|
||||
mr={2}
|
||||
/>
|
||||
<Text as="span" fontSize="xs" fontWeight="semibold">
|
||||
{item.inSafe ? "Yes" : "No"}
|
||||
{item.in_safe ? "Yes" : "No"}
|
||||
</Text>
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
|
||||
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
||||
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
|
||||
@@ -17,11 +17,7 @@ import MyAlert from "./myChakra/MyAlert";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
import { Trash2, RefreshCcwDot } from "lucide-react";
|
||||
import { deleteLoan } from "@/utils/userActions";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
const LoanTable: React.FC = () => {
|
||||
const [items, setItems] = useState<Loan[]>([]);
|
||||
@@ -55,18 +51,22 @@ const LoanTable: React.FC = () => {
|
||||
created_at: string;
|
||||
loaned_items_name: string[];
|
||||
deleted: boolean;
|
||||
note: string;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/allLoans`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/loan-data/all-loans`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
@@ -161,6 +161,9 @@ const LoanTable: React.FC = () => {
|
||||
<Table.ColumnHeader>
|
||||
<strong>Ausgeliehene Artikel</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Notiz</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Aktionen</strong>
|
||||
</Table.ColumnHeader>
|
||||
@@ -180,6 +183,7 @@ const LoanTable: React.FC = () => {
|
||||
<Table.Cell>{formatDateTime(item.returned_date)}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(item.created_at)}</Table.Cell>
|
||||
<Table.Cell>{item.loaned_items_name.join(", ")}</Table.Cell>
|
||||
<Table.Cell>{item.note}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
HStack,
|
||||
IconButton,
|
||||
Heading,
|
||||
Switch, // neu
|
||||
} from "@chakra-ui/react";
|
||||
import { Tooltip } from "@/components/ui/tooltip";
|
||||
import { fetchUserData } from "@/utils/fetcher";
|
||||
@@ -23,9 +24,13 @@ import ChangePWform from "./ChangePWform";
|
||||
type User = {
|
||||
id: number;
|
||||
username: string;
|
||||
password: string;
|
||||
role: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
is_admin: boolean;
|
||||
role: number;
|
||||
entry_created_at: string;
|
||||
entry_updated_at: string;
|
||||
};
|
||||
|
||||
const UserTable: React.FC = () => {
|
||||
@@ -52,10 +57,20 @@ const UserTable: React.FC = () => {
|
||||
setIsError(true);
|
||||
};
|
||||
|
||||
const handleInputChange = (userId: number, field: string, value: string) => {
|
||||
const handleInputChange = (userId: number, field: string, value: any) => {
|
||||
setUsers((prevUsers) =>
|
||||
prevUsers.map((user) =>
|
||||
user.id === userId ? { ...user, [field]: value } : user
|
||||
user.id === userId
|
||||
? {
|
||||
...user,
|
||||
[field]:
|
||||
field === "role"
|
||||
? Number(value)
|
||||
: field === "is_admin"
|
||||
? value === true || value === "true" || value === 1
|
||||
: value,
|
||||
}
|
||||
: user
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -70,7 +85,7 @@ const UserTable: React.FC = () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchUserData();
|
||||
console.log("user api response", data);
|
||||
console.log(data);
|
||||
if (Array.isArray(data)) {
|
||||
setUsers(data);
|
||||
} else {
|
||||
@@ -189,6 +204,18 @@ const UserTable: React.FC = () => {
|
||||
<Table.ColumnHeader>
|
||||
<strong>Benutzername</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Vorname</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Nachname</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>E-Mail</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Admin</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Passwort ändern</strong>
|
||||
</Table.ColumnHeader>
|
||||
@@ -198,6 +225,9 @@ const UserTable: React.FC = () => {
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag erstellt am</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag aktualisiert am</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Aktionen</strong>
|
||||
</Table.ColumnHeader>
|
||||
@@ -207,14 +237,58 @@ const UserTable: React.FC = () => {
|
||||
{users.map((user) => (
|
||||
<Table.Row key={user.id}>
|
||||
<Table.Cell>{user.id}</Table.Cell>
|
||||
<Table.Cell>{user.username}</Table.Cell>
|
||||
|
||||
{/* Vorname */}
|
||||
<Table.Cell>
|
||||
<Input
|
||||
size="sm"
|
||||
value={user.first_name ?? ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "username", e.target.value)
|
||||
handleInputChange(user.id, "first_name", e.target.value)
|
||||
}
|
||||
value={user.username}
|
||||
/>
|
||||
</Table.Cell>
|
||||
|
||||
{/* Nachname */}
|
||||
<Table.Cell>
|
||||
<Input
|
||||
size="sm"
|
||||
value={user.last_name ?? ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "last_name", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
|
||||
{/* E-Mail */}
|
||||
<Table.Cell>
|
||||
<Input
|
||||
type="email"
|
||||
size="sm"
|
||||
value={user.email ?? ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "email", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
|
||||
{/* Admin */}
|
||||
<Table.Cell>
|
||||
<Switch.Root
|
||||
size="sm"
|
||||
checked={!!user.is_admin}
|
||||
onCheckedChange={(details) =>
|
||||
handleInputChange(user.id, "is_admin", details.checked)
|
||||
}
|
||||
aria-label="Adminrechte umschalten"
|
||||
>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Switch.HiddenInput />
|
||||
</Switch.Root>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button onClick={() => handlePasswordChange(user.username)}>
|
||||
Passwort ändern
|
||||
@@ -230,13 +304,17 @@ const UserTable: React.FC = () => {
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(user.entry_created_at)}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(user.entry_updated_at)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleEdit(
|
||||
user.id,
|
||||
user.username,
|
||||
user.role,
|
||||
user.first_name,
|
||||
user.last_name,
|
||||
user.email,
|
||||
user.is_admin,
|
||||
Number(user.role)
|
||||
).then((response) => {
|
||||
if (response.success) {
|
||||
setError(
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
export const fetchUserData = async () => {
|
||||
const response = await fetch(`${API_BASE}/api/allUsers`, {
|
||||
const response = await fetch(`${API_BASE}/api/admin/user-data/users`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
export type LoginSuccess = { success: true };
|
||||
export type LoginFailure = {
|
||||
@@ -18,12 +14,20 @@ export const loginFunc = async (
|
||||
password: string
|
||||
): Promise<LoginResult> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/loginAdmin`, {
|
||||
const response = await fetch(`${API_BASE}/api/admin/user-mgmt/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Login failed!",
|
||||
description: "You are not an admin user.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -39,6 +43,7 @@ export const loginFunc = async (
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error logging in:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: "Login failed!",
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
export const handleDelete = async (userId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteUser/${userId}`,
|
||||
`${API_BASE}/api/admin/user-data/delete-user/${userId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -28,19 +24,28 @@ export const handleDelete = async (userId: number) => {
|
||||
|
||||
export const handleEdit = async (
|
||||
userId: number,
|
||||
username: string,
|
||||
role: string
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
email: string,
|
||||
is_admin: boolean,
|
||||
role: number
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/editUser/${userId}`,
|
||||
`${API_BASE}/api/admin/user-data/edit-user/${userId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ username, role }),
|
||||
body: JSON.stringify({
|
||||
first_name,
|
||||
last_name,
|
||||
role,
|
||||
email,
|
||||
is_admin,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
@@ -56,17 +61,32 @@ export const handleEdit = async (
|
||||
export const createUser = async (
|
||||
username: string,
|
||||
role: number,
|
||||
password: string
|
||||
password: string,
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
email: string,
|
||||
isAdmin: boolean
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/createUser`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ username, role, password }),
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/user-data/create-user`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
role,
|
||||
password,
|
||||
isAdmin,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create user");
|
||||
}
|
||||
@@ -79,14 +99,17 @@ export const createUser = async (
|
||||
|
||||
export const changePW = async (newPassword: string, username: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/changePWadmin`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ newPassword, username }),
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/user-data/change-password`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ username, password: newPassword }),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to change password");
|
||||
}
|
||||
@@ -100,7 +123,7 @@ export const changePW = async (newPassword: string, username: string) => {
|
||||
export const deleteLoan = async (loanId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteLoan/${loanId}`,
|
||||
`${API_BASE}/api/admin/loan-data/delete-loan/${loanId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -121,7 +144,7 @@ export const deleteLoan = async (loanId: number) => {
|
||||
export const deleteItem = async (itemId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteItem/${itemId}`,
|
||||
`${API_BASE}/api/admin/item-data/delete-item/${itemId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -144,14 +167,17 @@ export const createItem = async (
|
||||
can_borrow_role: number
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/createItem`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ item_name, can_borrow_role }),
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/item-data/create-item`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ item_name, can_borrow_role }),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -172,14 +198,17 @@ export const handleEditItems = async (
|
||||
can_borrow_role: string
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/updateItemByID`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ itemId, item_name, can_borrow_role }),
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/item-data/edit-item/${itemId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ item_name, can_borrow_role }),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to edit item");
|
||||
}
|
||||
@@ -193,9 +222,9 @@ export const handleEditItems = async (
|
||||
export const changeSafeState = async (itemId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/changeSafeState/${itemId}`,
|
||||
`${API_BASE}/api/admin/item-data/change-safe-state/${itemId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
@@ -211,16 +240,19 @@ export const changeSafeState = async (itemId: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const createAPIentry = async (apiKey: string, user: string) => {
|
||||
export const createAPIentry = async (apiKey: string, name: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/createAPIentry`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ apiKey, user }),
|
||||
});
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/api-data/create-api-key`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ apiKey, entryName: name }),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -238,7 +270,7 @@ export const createAPIentry = async (apiKey: string, user: string) => {
|
||||
export const deleteAPKey = async (apiKeyId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteAPKey/${apiKeyId}`,
|
||||
`${API_BASE}/api/admin/api-data/delete-api-key/${apiKeyId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user