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:
2025-11-11 17:08:45 +01:00
parent 974a5a75d8
commit a8b4ac3d60
26 changed files with 605 additions and 347 deletions

View File

@@ -3,11 +3,7 @@ import { useEffect } from "react";
import Dashboard from "./Dashboard"; import Dashboard from "./Dashboard";
import Login from "./Login"; import Login from "./Login";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
const Layout: React.FC = () => { const Layout: React.FC = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -15,12 +11,15 @@ const Layout: React.FC = () => {
useEffect(() => { useEffect(() => {
if (Cookies.get("token")) { if (Cookies.get("token")) {
const verifyToken = async () => { const verifyToken = async () => {
const response = await fetch(`${API_BASE}/api/verifyToken`, { const response = await fetch(
`${API_BASE}/api/admin/user-mgmt/verify-token`,
{
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}); }
);
if (response.ok) { if (response.ok) {
setIsLoggedIn(true); setIsLoggedIn(true);
} else { } else {

View File

@@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { useEffect, useState } from "react";
import { Box, Flex, VStack, Heading, Text, Link } from "@chakra-ui/react"; import { Box, Flex, VStack, Heading, Text, Link } from "@chakra-ui/react";
import { API_BASE } from "@/config/api.config";
type SidebarProps = { type SidebarProps = {
viewAusleihen: () => void; viewAusleihen: () => void;
@@ -15,10 +17,22 @@ const Sidebar: React.FC<SidebarProps> = ({
viewUser, viewUser,
viewAPI, 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 ( return (
<Box <Box
as="aside" as="aside"
w="260px" w="180px"
minH="100vh" minH="100vh"
bg="gray.800" bg="gray.800"
color="gray.100" color="gray.100"
@@ -72,7 +86,33 @@ const Sidebar: React.FC<SidebarProps> = ({
</VStack> </VStack>
<Box mt="auto" pt={8} fontSize="xs" color="gray.500"> <Box mt="auto" pt={8} fontSize="xs" color="gray.500">
<Text>&copy; Made with by Theis Gaedigk</Text> <Text mb={2}>&copy; 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> </Box>
</Flex> </Flex>
</Box> </Box>

View File

@@ -17,17 +17,14 @@ import { useState, useEffect } from "react";
import { deleteAPKey } from "@/utils/userActions"; import { deleteAPKey } from "@/utils/userActions";
import AddAPIKey from "./AddAPIKey"; import AddAPIKey from "./AddAPIKey";
import { formatDateTime } from "@/utils/userFuncs"; import { formatDateTime } from "@/utils/userFuncs";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
type Items = { type Items = {
id: number; id: number;
apiKey: string; api_key: string;
user: string; entry_name: string;
entry_created_at: string; entry_created_at: string;
last_used_at: string | null;
}; };
const APIKeyTable: React.FC = () => { const APIKeyTable: React.FC = () => {
@@ -56,13 +53,17 @@ const APIKeyTable: React.FC = () => {
const fetchData = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`${API_BASE}/api/apiKeys`, { const response = await fetch(
`${API_BASE}/api/admin/api-data/get-api-keys`,
{
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}); }
);
const data = await response.json(); const data = await response.json();
console.log(data);
return data; return data;
} catch (error) { } catch (error) {
setError("error", "Failed to fetch items", "There is an error"); setError("error", "Failed to fetch items", "There is an error");
@@ -159,29 +160,37 @@ const APIKeyTable: React.FC = () => {
<strong>API Key</strong> <strong>API Key</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Benutzer</strong> <strong>Name</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Eintrag erstellt am</strong> <strong>Eintrag erstellt am</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Zuletzt benutzt am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Aktionen</strong> <strong>Aktionen</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{items.map((apiKey) => ( {items.map((item) => (
<Table.Row key={apiKey.id}> <Table.Row key={item.id}>
<Table.Cell>{apiKey.id}</Table.Cell> <Table.Cell>{item.id}</Table.Cell>
<Table.Cell>{apiKey.apiKey}</Table.Cell> <Table.Cell>{item.api_key}</Table.Cell>
<Table.Cell>{apiKey.user}</Table.Cell> <Table.Cell>{item.entry_name}</Table.Cell>
<Table.Cell>{formatDateTime(apiKey.entry_created_at)}</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> <Table.Cell>
<Button <Button
onClick={() => onClick={() =>
deleteAPKey(apiKey.id).then((response) => { deleteAPKey(item.id).then((response) => {
if (response.success) { if (response.success) {
setItems(items.filter((i) => i.id !== apiKey.id)); setItems(items.filter((i) => i.id !== item.id));
setError( setError(
"success", "success",
"Gegenstand gelöscht", "Gegenstand gelöscht",

View File

@@ -1,6 +1,15 @@
import React from "react"; 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 { createAPIentry } from "@/utils/userActions";
import { useState } from "react";
type AddAPIKeyProps = { type AddAPIKeyProps = {
onClose: () => void; onClose: () => void;
@@ -12,6 +21,8 @@ type AddAPIKeyProps = {
}; };
const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => { const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
const [value, setValue] = useState("");
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <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.Root maxW="sm">
@@ -23,13 +34,26 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
</Card.Header> </Card.Header>
<Card.Body> <Card.Body>
<Stack gap="4" w="full"> <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.Root>
<Field.Label>API key</Field.Label> <Field.Label>Name</Field.Label>
<Input type="number" id="apiKey" /> <Input id="name" type="text" />
</Field.Root>
<Field.Root>
<Field.Label>Benutzer</Field.Label>
<Input id="user" type="text" />
</Field.Root> </Field.Root>
</Stack> </Stack>
</Card.Body> </Card.Body>
@@ -44,14 +68,14 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
( (
document.getElementById("apiKey") as HTMLInputElement document.getElementById("apiKey") as HTMLInputElement
)?.value.trim() || ""; )?.value.trim() || "";
const user = const name =
( (
document.getElementById("user") as HTMLInputElement document.getElementById("name") as HTMLInputElement
)?.value.trim() || ""; )?.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) { if (res.success) {
alert( alert(
"success", "success",

View File

@@ -1,5 +1,13 @@
import React from "react"; 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"; import { createUser } from "@/utils/userActions";
type AddFormProps = { type AddFormProps = {
@@ -12,37 +20,71 @@ type AddFormProps = {
}; };
const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => { const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
const [admin, setAdmin] = React.useState(false);
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<Card.Root maxW="sm"> <Card.Root maxW="sm">
<Card.Header> <Card.Header>
<Card.Title>Neuen Nutzer erstellen</Card.Title> <Card.Title>Neuen Nutzer erstellen</Card.Title>
<Card.Description> <Card.Description>
Füllen Sie das folgende Formular aus, um einen Nutzer zu erstellen. Füllen Sie das folgende Formular aus, um einen Nutzer zu
erstellen.
</Card.Description> </Card.Description>
</Card.Header> </Card.Header>
<Card.Body> <Card.Body>
<Stack gap="4" w="full"> <Stack gap="4" w="full">
<Field.Root> <Field.Root>
<Field.Label>Username</Field.Label> <Field.Label>Benutzername</Field.Label>
<Input id="username" /> <Input id="username" />
</Field.Root> </Field.Root>
<Field.Root> <Field.Root>
<Field.Label>Password</Field.Label> <Field.Label>Passwort</Field.Label>
<Input id="password" type="password" /> <Input id="password" type="password" />
</Field.Root> </Field.Root>
<Field.Root> <Field.Root>
<Field.Label>Role</Field.Label> <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>
{/* 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" /> <Input id="role" type="number" />
</Field.Root> </Field.Root>
</Stack> </Stack>
</Card.Body> </Card.Body>
<Card.Footer justifyContent="flex-end"> <Card.Footer justifyContent="flex-end">
<Text>Der Benutzername kann nicht mehr geändert werden.</Text>
<Button variant="outline" onClick={onClose}> <Button variant="outline" onClick={onClose}>
Abbrechen Abbrechen
</Button> </Button>
<Button <Button
variant="solid" variant="solid"
type="submit"
onClick={async () => { onClick={async () => {
const username = const username =
( (
@@ -54,10 +96,30 @@ const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
const role = Number( const role = Number(
(document.getElementById("role") as HTMLInputElement)?.value (document.getElementById("role") as HTMLInputElement)?.value
); );
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() || "";
if (!username || !password || Number.isNaN(role)) return; // admin kommt jetzt zuverlässig aus dem State
const res = await createUser(
username,
role,
password,
firstname,
lastname,
email,
admin
);
const res = await createUser(username, role, password);
if (res.success) { if (res.success) {
alert( alert(
"success", "success",
@@ -79,6 +141,7 @@ const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
</Button> </Button>
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
</form>
</div> </div>
); );
}; };

View File

@@ -30,18 +30,17 @@ import {
} from "@/utils/userActions"; } from "@/utils/userActions";
import AddItemForm from "./AddItemForm"; import AddItemForm from "./AddItemForm";
import { formatDateTime } from "@/utils/userFuncs"; import { formatDateTime } from "@/utils/userFuncs";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
type Items = { type Items = {
id: number; id: number;
item_name: string; item_name: string;
can_borrow_role: string; can_borrow_role: string;
inSafe: boolean; in_safe: boolean;
entry_created_at: string; entry_created_at: string;
entry_updated_at: string;
last_borrowed_person: string | null;
currently_borrowing: string | null;
}; };
const ItemTable: React.FC = () => { const ItemTable: React.FC = () => {
@@ -82,12 +81,15 @@ const ItemTable: React.FC = () => {
const fetchData = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`${API_BASE}/api/allItems`, { const response = await fetch(
`${API_BASE}/api/admin/item-data/all-items`,
{
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}); }
);
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -193,6 +195,15 @@ const ItemTable: React.FC = () => {
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Eintrag erstellt am</strong> <strong>Eintrag erstellt am</strong>
</Table.ColumnHeader> </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> <Table.ColumnHeader>
<strong>Aktionen</strong> <strong>Aktionen</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
@@ -229,31 +240,34 @@ const ItemTable: React.FC = () => {
py={1} py={1}
gap={2} gap={2}
variant="ghost" variant="ghost"
color={item.inSafe ? "green.600" : "red.600"} color={item.in_safe ? "green.600" : "red.600"}
borderWidth="1px" borderWidth="1px"
borderColor={item.inSafe ? "green.300" : "red.300"} borderColor={item.in_safe ? "green.300" : "red.300"}
_hover={{ _hover={{
bg: item.inSafe ? "green.50" : "red.50", bg: item.in_safe ? "green.50" : "red.50",
borderColor: item.inSafe ? "green.400" : "red.400", borderColor: item.in_safe ? "green.400" : "red.400",
transform: "translateY(-1px)", transform: "translateY(-1px)",
shadow: "sm", shadow: "sm",
}} }}
_active={{ transform: "translateY(0)" }} _active={{ transform: "translateY(0)" }}
aria-label={ 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 <Icon
as={item.inSafe ? CheckCircle2 : XCircle} as={item.in_safe ? CheckCircle2 : XCircle}
boxSize={3.5} boxSize={3.5}
mr={2} mr={2}
/> />
<Text as="span" fontSize="xs" fontWeight="semibold"> <Text as="span" fontSize="xs" fontWeight="semibold">
{item.inSafe ? "Yes" : "No"} {item.in_safe ? "Yes" : "No"}
</Text> </Text>
</Button> </Button>
</Table.Cell> </Table.Cell>
<Table.Cell>{formatDateTime(item.entry_created_at)}</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> <Table.Cell>
<Button <Button
onClick={() => onClick={() =>

View File

@@ -17,11 +17,7 @@ import MyAlert from "./myChakra/MyAlert";
import { formatDateTime } from "@/utils/userFuncs"; import { formatDateTime } from "@/utils/userFuncs";
import { Trash2, RefreshCcwDot } from "lucide-react"; import { Trash2, RefreshCcwDot } from "lucide-react";
import { deleteLoan } from "@/utils/userActions"; import { deleteLoan } from "@/utils/userActions";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
const LoanTable: React.FC = () => { const LoanTable: React.FC = () => {
const [items, setItems] = useState<Loan[]>([]); const [items, setItems] = useState<Loan[]>([]);
@@ -55,18 +51,22 @@ const LoanTable: React.FC = () => {
created_at: string; created_at: string;
loaned_items_name: string[]; loaned_items_name: string[];
deleted: boolean; deleted: boolean;
note: string;
}; };
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`${API_BASE}/api/allLoans`, { const response = await fetch(
`${API_BASE}/api/admin/loan-data/all-loans`,
{
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}); }
);
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -161,6 +161,9 @@ const LoanTable: React.FC = () => {
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Ausgeliehene Artikel</strong> <strong>Ausgeliehene Artikel</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Notiz</strong>
</Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Aktionen</strong> <strong>Aktionen</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
@@ -180,6 +183,7 @@ const LoanTable: React.FC = () => {
<Table.Cell>{formatDateTime(item.returned_date)}</Table.Cell> <Table.Cell>{formatDateTime(item.returned_date)}</Table.Cell>
<Table.Cell>{formatDateTime(item.created_at)}</Table.Cell> <Table.Cell>{formatDateTime(item.created_at)}</Table.Cell>
<Table.Cell>{item.loaned_items_name.join(", ")}</Table.Cell> <Table.Cell>{item.loaned_items_name.join(", ")}</Table.Cell>
<Table.Cell>{item.note}</Table.Cell>
<Table.Cell> <Table.Cell>
<Button <Button
onClick={() => onClick={() =>

View File

@@ -10,6 +10,7 @@ import {
HStack, HStack,
IconButton, IconButton,
Heading, Heading,
Switch, // neu
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { Tooltip } from "@/components/ui/tooltip"; import { Tooltip } from "@/components/ui/tooltip";
import { fetchUserData } from "@/utils/fetcher"; import { fetchUserData } from "@/utils/fetcher";
@@ -23,9 +24,13 @@ import ChangePWform from "./ChangePWform";
type User = { type User = {
id: number; id: number;
username: string; username: string;
password: string; first_name: string;
role: string; last_name: string;
email: string;
is_admin: boolean;
role: number;
entry_created_at: string; entry_created_at: string;
entry_updated_at: string;
}; };
const UserTable: React.FC = () => { const UserTable: React.FC = () => {
@@ -52,10 +57,20 @@ const UserTable: React.FC = () => {
setIsError(true); setIsError(true);
}; };
const handleInputChange = (userId: number, field: string, value: string) => { const handleInputChange = (userId: number, field: string, value: any) => {
setUsers((prevUsers) => setUsers((prevUsers) =>
prevUsers.map((user) => 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); setIsLoading(true);
try { try {
const data = await fetchUserData(); const data = await fetchUserData();
console.log("user api response", data); console.log(data);
if (Array.isArray(data)) { if (Array.isArray(data)) {
setUsers(data); setUsers(data);
} else { } else {
@@ -189,6 +204,18 @@ const UserTable: React.FC = () => {
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Benutzername</strong> <strong>Benutzername</strong>
</Table.ColumnHeader> </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> <Table.ColumnHeader>
<strong>Passwort ändern</strong> <strong>Passwort ändern</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
@@ -198,6 +225,9 @@ const UserTable: React.FC = () => {
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Eintrag erstellt am</strong> <strong>Eintrag erstellt am</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag aktualisiert am</strong>
</Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Aktionen</strong> <strong>Aktionen</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
@@ -207,14 +237,58 @@ const UserTable: React.FC = () => {
{users.map((user) => ( {users.map((user) => (
<Table.Row key={user.id}> <Table.Row key={user.id}>
<Table.Cell>{user.id}</Table.Cell> <Table.Cell>{user.id}</Table.Cell>
<Table.Cell>{user.username}</Table.Cell>
{/* Vorname */}
<Table.Cell> <Table.Cell>
<Input <Input
size="sm"
value={user.first_name ?? ""}
onChange={(e) => onChange={(e) =>
handleInputChange(user.id, "username", e.target.value) handleInputChange(user.id, "first_name", e.target.value)
} }
value={user.username}
/> />
</Table.Cell> </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> <Table.Cell>
<Button onClick={() => handlePasswordChange(user.username)}> <Button onClick={() => handlePasswordChange(user.username)}>
Passwort ändern Passwort ändern
@@ -230,13 +304,17 @@ const UserTable: React.FC = () => {
/> />
</Table.Cell> </Table.Cell>
<Table.Cell>{formatDateTime(user.entry_created_at)}</Table.Cell> <Table.Cell>{formatDateTime(user.entry_created_at)}</Table.Cell>
<Table.Cell>{formatDateTime(user.entry_updated_at)}</Table.Cell>
<Table.Cell> <Table.Cell>
<Button <Button
onClick={() => onClick={() =>
handleEdit( handleEdit(
user.id, user.id,
user.username, user.first_name,
user.role, user.last_name,
user.email,
user.is_admin,
Number(user.role)
).then((response) => { ).then((response) => {
if (response.success) { if (response.success) {
setError( setError(

View File

@@ -1,12 +1,8 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
export const fetchUserData = async () => { export const fetchUserData = async () => {
const response = await fetch(`${API_BASE}/api/allUsers`, { const response = await fetch(`${API_BASE}/api/admin/user-data/users`, {
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },

View File

@@ -1,9 +1,5 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
export type LoginSuccess = { success: true }; export type LoginSuccess = { success: true };
export type LoginFailure = { export type LoginFailure = {
@@ -18,12 +14,20 @@ export const loginFunc = async (
password: string password: string
): Promise<LoginResult> => { ): Promise<LoginResult> => {
try { try {
const response = await fetch(`${API_BASE}/api/loginAdmin`, { const response = await fetch(`${API_BASE}/api/admin/user-mgmt/login`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }), 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) { if (!response.ok) {
return { return {
success: false, success: false,
@@ -39,6 +43,7 @@ export const loginFunc = async (
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error("Error logging in:", error); console.error("Error logging in:", error);
return { return {
success: false, success: false,
message: "Login failed!", message: "Login failed!",

View File

@@ -1,14 +1,10 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { API_BASE } from "@/config/api.config";
const API_BASE =
(import.meta as any).env?.VITE_BACKEND_URL ||
import.meta.env.VITE_BACKEND_URL ||
"http://localhost:8002";
export const handleDelete = async (userId: number) => { export const handleDelete = async (userId: number) => {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/deleteUser/${userId}`, `${API_BASE}/api/admin/user-data/delete-user/${userId}`,
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
@@ -28,19 +24,28 @@ export const handleDelete = async (userId: number) => {
export const handleEdit = async ( export const handleEdit = async (
userId: number, userId: number,
username: string, first_name: string,
role: string last_name: string,
email: string,
is_admin: boolean,
role: number
) => { ) => {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/editUser/${userId}`, `${API_BASE}/api/admin/user-data/edit-user/${userId}`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
body: JSON.stringify({ username, role }), body: JSON.stringify({
first_name,
last_name,
role,
email,
is_admin,
}),
} }
); );
if (!response.ok) { if (!response.ok) {
@@ -56,17 +61,32 @@ export const handleEdit = async (
export const createUser = async ( export const createUser = async (
username: string, username: string,
role: number, role: number,
password: string password: string,
first_name: string,
last_name: string,
email: string,
isAdmin: boolean
) => { ) => {
try { try {
const response = await fetch(`${API_BASE}/api/createUser`, { const response = await fetch(
`${API_BASE}/api/admin/user-data/create-user`,
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
body: JSON.stringify({ username, role, password }), body: JSON.stringify({
}); username,
role,
password,
isAdmin,
email,
first_name,
last_name,
}),
}
);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to create user"); throw new Error("Failed to create user");
} }
@@ -79,14 +99,17 @@ export const createUser = async (
export const changePW = async (newPassword: string, username: string) => { export const changePW = async (newPassword: string, username: string) => {
try { try {
const response = await fetch(`${API_BASE}/api/changePWadmin`, { const response = await fetch(
`${API_BASE}/api/admin/user-data/change-password`,
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
body: JSON.stringify({ newPassword, username }), body: JSON.stringify({ username, password: newPassword }),
}); }
);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to change password"); 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) => { export const deleteLoan = async (loanId: number) => {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/deleteLoan/${loanId}`, `${API_BASE}/api/admin/loan-data/delete-loan/${loanId}`,
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
@@ -121,7 +144,7 @@ export const deleteLoan = async (loanId: number) => {
export const deleteItem = async (itemId: number) => { export const deleteItem = async (itemId: number) => {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/deleteItem/${itemId}`, `${API_BASE}/api/admin/item-data/delete-item/${itemId}`,
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
@@ -144,14 +167,17 @@ export const createItem = async (
can_borrow_role: number can_borrow_role: number
) => { ) => {
try { try {
const response = await fetch(`${API_BASE}/api/createItem`, { const response = await fetch(
`${API_BASE}/api/admin/item-data/create-item`,
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
body: JSON.stringify({ item_name, can_borrow_role }), body: JSON.stringify({ item_name, can_borrow_role }),
}); }
);
if (!response.ok) { if (!response.ok) {
return { return {
success: false, success: false,
@@ -172,14 +198,17 @@ export const handleEditItems = async (
can_borrow_role: string can_borrow_role: string
) => { ) => {
try { try {
const response = await fetch(`${API_BASE}/api/updateItemByID`, { const response = await fetch(
`${API_BASE}/api/admin/item-data/edit-item/${itemId}`,
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
body: JSON.stringify({ itemId, item_name, can_borrow_role }), body: JSON.stringify({ item_name, can_borrow_role }),
}); }
);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to edit item"); throw new Error("Failed to edit item");
} }
@@ -193,9 +222,9 @@ export const handleEditItems = async (
export const changeSafeState = async (itemId: number) => { export const changeSafeState = async (itemId: number) => {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/changeSafeState/${itemId}`, `${API_BASE}/api/admin/item-data/change-safe-state/${itemId}`,
{ {
method: "PUT", method: "POST",
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, 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 { try {
const response = await fetch(`${API_BASE}/api/createAPIentry`, { const response = await fetch(
`${API_BASE}/api/admin/api-data/create-api-key`,
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
body: JSON.stringify({ apiKey, user }), body: JSON.stringify({ apiKey, entryName: name }),
}); }
);
if (!response.ok) { if (!response.ok) {
return { return {
success: false, success: false,
@@ -238,7 +270,7 @@ export const createAPIentry = async (apiKey: string, user: string) => {
export const deleteAPKey = async (apiKeyId: number) => { export const deleteAPKey = async (apiKeyId: number) => {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE}/api/deleteAPKey/${apiKeyId}`, `${API_BASE}/api/admin/api-data/delete-api-key/${apiKeyId}`,
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {

View File

@@ -8,21 +8,21 @@ dotenv.config();
import { import {
getAllApiKeys, getAllApiKeys,
createAPIentry, createAPIentry,
deleteAPIKey, deleteAPKey,
} from "./database/apiDataMgmt.database.js"; } from "./database/apiDataMgmt.database.js";
router.get("/get-api-keys", authenticateAdmin, async (req, res) => { router.get("/get-api-keys", authenticateAdmin, async (req, res) => {
const result = await getAllApiKeys(); const result = await getAllApiKeys();
if (result.success) { if (result.success) {
return res.status(200).json({ apiKeys: result.data }); return res.status(200).json(result.data);
} }
return res.status(500).json({ message: "Failed to retrieve API keys" }); return res.status(500).json({ message: "Failed to retrieve API keys" });
}); });
router.post("/create-api-key", authenticateAdmin, async (req, res) => { router.post("/create-api-key", authenticateAdmin, async (req, res) => {
const apiKey = req.body.apiKey; const apiKey = req.body.apiKey;
const username = req.body.username; const entryName = req.body.entryName;
const result = await createAPIentry(apiKey, username); const result = await createAPIentry(apiKey, entryName);
if (result.success) { if (result.success) {
return res.status(201).json({ message: "API key created successfully" }); return res.status(201).json({ message: "API key created successfully" });
} }
@@ -31,7 +31,7 @@ router.post("/create-api-key", authenticateAdmin, async (req, res) => {
router.delete("/delete-api-key/:id", authenticateAdmin, async (req, res) => { router.delete("/delete-api-key/:id", authenticateAdmin, async (req, res) => {
const apiKeyId = req.params.id; const apiKeyId = req.params.id;
const result = await deleteAPIKey(apiKeyId); const result = await deleteAPKey(apiKeyId);
if (result.success) { if (result.success) {
return res.status(200).json({ message: "API key deleted successfully" }); return res.status(200).json({ message: "API key deleted successfully" });
} }

View File

@@ -19,10 +19,10 @@ export const getAllApiKeys = async () => {
return { success: false }; return { success: false };
}; };
export const createAPIentry = async (apiKey, user) => { export const createAPIentry = async (apiKey, entryName) => {
const [result] = await pool.query( const [result] = await pool.query(
"INSERT INTO apiKeys (api_key, username) VALUES (?, ?)", "INSERT INTO apiKeys (api_key, entry_name) VALUES (?, ?)",
[apiKey, user] [apiKey, entryName]
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };

View File

@@ -26,7 +26,7 @@ export const deleteItemById = async (itemId) => {
export const createItem = async (item_name, can_borrow_role, in_safe) => { export const createItem = async (item_name, can_borrow_role, in_safe) => {
const [result] = await pool.query( const [result] = await pool.query(
"INSERT INTO items (item_name, can_borrow_role, in_safe) VALUES (?, ?, ?)", "INSERT INTO items (item_name, can_borrow_role, in_safe) VALUES (?, ?, ?)",
[item_name, can_borrow_role, in_safe] [item_name, can_borrow_role, true]
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
@@ -34,9 +34,37 @@ export const createItem = async (item_name, can_borrow_role, in_safe) => {
export const editItemById = async (itemId, item_name, can_borrow_role) => { export const editItemById = async (itemId, item_name, can_borrow_role) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE items SET item_name = ?, can_borrow_role = ? WHERE id = ?", "UPDATE items SET item_name = ?, can_borrow_role = ?, entry_updated_at = NOW() WHERE id = ?",
[item_name, can_borrow_role, itemId] [item_name, can_borrow_role, itemId]
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
}; };
export const changeSafeState = async (itemId) => {
const currentState = await pool.query(
"SELECT in_safe FROM items WHERE id = ?",
[itemId]
);
if (currentState[0].length === 0) {
return { success: false };
}
if (currentState[0][0].in_safe) {
const [result] = await pool.query(
"UPDATE items SET in_safe = false WHERE id = ?",
[itemId]
);
if (result.affectedRows > 0) return { success: true };
}
if (!currentState[0][0].in_safe) {
const [result] = await pool.query(
"UPDATE items SET in_safe = true WHERE id = ?",
[itemId]
);
if (result.affectedRows > 0) return { success: true };
}
return { success: false };
};

View File

@@ -61,7 +61,7 @@ export const editUserById = async (
export const getAllUsers = async () => { export const getAllUsers = async () => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users" "SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users"
); );
if (result.length > 0) return { success: true, data: result }; if (result.length > 0) return { success: true, data: result };
return { success: false }; return { success: false };

View File

@@ -16,7 +16,7 @@ import {
router.get("/all-items", authenticateAdmin, async (req, res) => { router.get("/all-items", authenticateAdmin, async (req, res) => {
const result = await getAllItems(); const result = await getAllItems();
if (result.success) { if (result.success) {
return res.status(200).json({ items: result.data }); return res.status(200).json(result.data);
} }
return res.status(500).json({ message: "Failed to retrieve items" }); return res.status(500).json({ message: "Failed to retrieve items" });
}); });
@@ -31,8 +31,8 @@ router.delete("/delete-item/:id", authenticateAdmin, async (req, res) => {
}); });
router.post("/create-item", authenticateAdmin, async (req, res) => { router.post("/create-item", authenticateAdmin, async (req, res) => {
const { item_name, can_borrow_role, in_safe } = req.body; const { item_name, can_borrow_role } = req.body;
const result = await createItem(item_name, can_borrow_role, in_safe); const result = await createItem(item_name, can_borrow_role);
if (result.success) { if (result.success) {
return res.status(201).json({ message: "Item created successfully" }); return res.status(201).json({ message: "Item created successfully" });
} }
@@ -55,8 +55,7 @@ router.post("/edit-item/:id", authenticateAdmin, async (req, res) => {
router.post("/change-safe-state/:id", authenticateAdmin, async (req, res) => { router.post("/change-safe-state/:id", authenticateAdmin, async (req, res) => {
const itemId = req.params.id; const itemId = req.params.id;
const { in_safe } = req.body; const result = await changeSafeState(itemId);
const result = await changeSafeState(itemId, in_safe);
if (result.success) { if (result.success) {
return res.status(200).json({ message: "Safe state changed successfully" }); return res.status(200).json({ message: "Safe state changed successfully" });
} }

View File

@@ -13,7 +13,7 @@ import {
router.get("/all-loans", authenticateAdmin, async (req, res) => { router.get("/all-loans", authenticateAdmin, async (req, res) => {
const result = await getAllLoans(); const result = await getAllLoans();
if (result.success) { if (result.success) {
return res.status(200).json({ loans: result.data }); return res.status(200).json(result.data);
} }
return res.status(500).json({ message: "Failed to retrieve loans" }); return res.status(500).json({ message: "Failed to retrieve loans" });
}); });

View File

@@ -47,7 +47,6 @@ router.delete("/delete-user/:id", authenticateAdmin, async (req, res) => {
}); });
router.post("/edit-user/:id", authenticateAdmin, async (req, res) => { router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
const password = req.body.password;
const first_name = req.body.first_name; const first_name = req.body.first_name;
const last_name = req.body.last_name; const last_name = req.body.last_name;
const role = req.body.role; const role = req.body.role;
@@ -57,7 +56,6 @@ router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
const result = await editUserById( const result = await editUserById(
userId, userId,
password,
first_name, first_name,
last_name, last_name,
role, role,
@@ -109,7 +107,7 @@ router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
router.get("/users", authenticateAdmin, async (req, res) => { router.get("/users", authenticateAdmin, async (req, res) => {
const result = await getAllUsers(); const result = await getAllUsers();
if (result.success) { if (result.success) {
return res.status(200).json({ users: result.data }); return res.status(200).json(result.data);
} }
return res.status(500).json({ message: "Failed to retrieve users" }); return res.status(500).json({ message: "Failed to retrieve users" });
}); });

View File

@@ -1,7 +1,9 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js"; import {
generateToken,
authenticateAdmin,
} from "../../services/authentication.js";
const router = express.Router(); const router = express.Router();
import nodemailer from "nodemailer";
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
@@ -9,7 +11,12 @@ dotenv.config();
import { loginAdmin } from "./database/userMgmt.database.js"; import { loginAdmin } from "./database/userMgmt.database.js";
router.post("/login", async (req, res) => { router.post("/login", async (req, res) => {
const result = await loginAdmin(req.body.username, req.body.password); const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ message: "Missing username or password" });
}
const result = await loginAdmin(username, password);
if (result.success) { if (result.success) {
const token = await generateToken({ const token = await generateToken({
@@ -18,7 +25,11 @@ router.post("/login", async (req, res) => {
last_name: result.data.last_name, last_name: result.data.last_name,
admin: result.data.is_admin, admin: result.data.is_admin,
}); });
return res.status(200).json({ message: "Login erfolgreich", token }); return res.status(200).json({
message: "Login erfolgreich",
token,
first_name: result.data.first_name,
});
} }
if (result.reason === "not_admin") { if (result.reason === "not_admin") {
@@ -27,3 +38,9 @@ router.post("/login", async (req, res) => {
return res.status(401).json({ message: "Ungültige Anmeldedaten" }); return res.status(401).json({ message: "Ungültige Anmeldedaten" });
}); });
router.get("/verify-token", authenticateAdmin, async (req, res) => {
return res.status(200).json({ message: "Token is valid" });
});
export default router;

View File

@@ -1,3 +1,5 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
export default router;

View File

@@ -1,5 +1,5 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../services/tokenService.js"; import { authenticate, generateToken } from "../../services/authentication.js";
const router = express.Router(); const router = express.Router();
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import dotenv from "dotenv"; import dotenv from "dotenv";
@@ -21,3 +21,4 @@ router.post("/login", async (req, res) => {
} }
}); });
export default router;

View File

@@ -1,91 +1,39 @@
-- MUST BE UPDATED BEFORE USE -- Mock data for borrow_system_new
USE borrow_system_new; USE borrow_system_new;
-- Optional: keep insert order predictable START TRANSACTION;
SET time_zone = '+00:00';
-- Users -- Users
INSERT INTO users (username, password, first_name, last_name, role, is_admin) INSERT INTO users (username, password, email, first_name, last_name, role, is_admin, entry_created_at)
VALUES VALUES
('alice', 'password123', 'Alice', 'Andersen', 1, false), ('admin', '$2b$12$adminhashedpasswordplaceholder0000000000', 'admin@example.com', 'System', 'Admin', 99, TRUE, '2025-01-01 08:00:00'),
('bob', 'password123', 'Bob', 'Berg', 2, false), ('alice', '$2b$12$alicehashedpasswordplaceholder0000000000', 'alice@example.com', 'Alice', 'Anderson', 1, FALSE, '2025-06-01 09:10:00'),
('carol', 'password123', 'Carol', 'Christie', 2, false), ('bob', '$2b$12$bobhashedpasswordplaceholder000000000000', 'bob@example.com', 'Bob', 'Brown', 2, FALSE, '2025-06-02 10:15:00'),
('dave', 'password123', 'Dave', 'Dawson', 1, false), ('carol', '$2b$12$carolhashedpasswordplaceholder00000000000', 'carol@example.com', 'Carol', 'Clark', 0, FALSE, '2025-06-03 11:20:00');
('eve', 'password123', 'Eve', 'Evans', 1, false),
('admin', 'password123', 'Admin', 'User', 3, true);
-- Items -- Items (ids will start at 1)
INSERT INTO items (item_name, can_borrow_role, in_safe, last_borrowed_person, currently_borrowing) INSERT INTO items (item_name, can_borrow_role, in_safe, entry_created_at, last_borrowed_person, currently_borrowing)
VALUES VALUES
('Canon EOS 90D Camera', 1, false, 'bob', 'alice'), ('MacBook Pro 16\"', 1, TRUE, '2025-05-01 09:00:00', 'alice', NULL),
('Rode NT1 Microphone', 1, true, 'dave', NULL), ('Projector Epson X200', 2, TRUE, '2025-04-20 10:00:00', 'bob', NULL),
('MacBook Pro 13', 2, false, 'bob', 'carol'), ('Canon EOS R6', 1, TRUE, '2025-03-15 14:30:00', NULL, NULL),
('Tripod Manfrotto', 1, false, 'carol', 'alice'), ('Wireless Microphone', 0, TRUE,'2025-05-10 12:00:00', 'carol', NULL),
('LED Panel Aputure', 1, true, NULL, NULL), ('USB-C Charger', 0, FALSE, '2025-05-11 12:30:00', 'alice', 'alice');
('Zoom H6 Recorder', 1, true, 'dave', NULL),
('Wacom Intuos Tablet', 1, true, NULL, NULL),
('DJI Ronin-S Gimbal', 2, true, NULL, NULL),
('Sony A7 III Body', 2, false, 'carol', 'eve'),
('Sigma 24-70mm Lens', 2, false, 'carol', 'eve');
-- Capture item IDs for JSON arrays
SET @id_canon = (SELECT id FROM items WHERE item_name='Canon EOS 90D Camera');
SET @id_rode = (SELECT id FROM items WHERE item_name='Rode NT1 Microphone');
SET @id_mac13 = (SELECT id FROM items WHERE item_name='MacBook Pro 13');
SET @id_tripod = (SELECT id FROM items WHERE item_name='Tripod Manfrotto');
SET @id_led = (SELECT id FROM items WHERE item_name='LED Panel Aputure');
SET @id_zoom = (SELECT id FROM items WHERE item_name='Zoom H6 Recorder');
SET @id_tablet = (SELECT id FROM items WHERE item_name='Wacom Intuos Tablet');
SET @id_ronin = (SELECT id FROM items WHERE item_name='DJI Ronin-S Gimbal');
SET @id_sony = (SELECT id FROM items WHERE item_name='Sony A7 III Body');
SET @id_sigma = (SELECT id FROM items WHERE item_name='Sigma 24-70mm Lens');
-- Loans -- Loans
INSERT INTO loans ( INSERT INTO loans (username, loan_code, start_date, end_date, take_date, returned_date, created_at, loaned_items_id, loaned_items_name, deleted, note)
username, loan_code, start_date, end_date, take_date, returned_date, loaned_items_id, loaned_items_name, deleted
) VALUES
-- Ongoing loan: Alice has Canon + Tripod
('alice', 100001, '2025-10-01 09:00:00', '2025-10-08 17:00:00', '2025-10-01 09:15:00', NULL,
JSON_ARRAY(@id_canon, @id_tripod),
JSON_ARRAY('Canon EOS 90D Camera','Tripod Manfrotto'),
false
),
-- Ongoing loan: Carol has MacBook Pro 13
('carol', 100002, '2025-10-03 10:00:00', '2025-10-10 16:00:00', '2025-10-03 10:05:00', NULL,
JSON_ARRAY(@id_mac13),
JSON_ARRAY('MacBook Pro 13'),
false
),
-- Returned loan: Dave had Zoom + Rode
('dave', 100003, '2025-09-10 08:30:00', '2025-09-12 16:00:00', '2025-09-10 08:45:00', '2025-09-12 15:40:00',
JSON_ARRAY(@id_zoom, @id_rode),
JSON_ARRAY('Zoom H6 Recorder','Rode NT1 Microphone'),
false
),
-- Cancelled/deleted booking (never taken): Bob reserved Tablet
('bob', 100004, '2025-10-05 09:00:00', '2025-10-06 09:00:00', NULL, NULL,
JSON_ARRAY(@id_tablet),
JSON_ARRAY('Wacom Intuos Tablet'),
true
),
-- Ongoing loan, likely overdue: Eve has Sony + Sigma
('eve', 100005, '2025-10-15 11:00:00', '2025-10-20 12:00:00', '2025-10-15 11:10:00', NULL,
JSON_ARRAY(@id_sony, @id_sigma),
JSON_ARRAY('Sony A7 III Body','Sigma 24-70mm Lens'),
false
),
-- Completed single-day loan: Bob used LED panel
('bob', 100006, '2025-09-20 13:00:00', '2025-09-20 18:00:00', '2025-09-20 13:05:00', '2025-09-20 17:30:00',
JSON_ARRAY(@id_led),
JSON_ARRAY('LED Panel Aputure'),
false
);
-- API keys
INSERT INTO apiKeys (api_key, username)
VALUES VALUES
(71002123, 'alice'), ('alice', '000101', '2025-06-10 09:00:00', '2025-06-17 09:00:00', '2025-06-10 09:05:00', NULL, '2025-06-10 09:00:00',
(71002124, 'bob'), JSON_ARRAY(1,5), JSON_ARRAY('MacBook Pro 16\"','USB-C Charger'), FALSE, 'For project work'),
(71002125, 'carol'), ('bob', '000102', '2025-06-01 14:00:00', '2025-06-04 12:00:00', '2025-06-01 14:10:00', '2025-06-04 11:50:00', '2025-06-01 14:00:00',
(99999999, 'admin'); JSON_ARRAY(2), JSON_ARRAY('Projector Epson X200'), FALSE, NULL),
('carol', '000103', '2025-06-05 08:00:00', '2025-06-06 18:00:00', NULL, NULL, '2025-06-05 08:00:00',
JSON_ARRAY(4), JSON_ARRAY('Wireless Microphone'), FALSE, 'Reserved for event');
-- API keys (15 digits)
INSERT INTO apiKeys (api_key, entry_name, entry_created_at, last_used_at)
VALUES
('000000000000001', 'internal-service-key', '2025-01-02 07:00:00', NULL),
('123456789012345', 'ci-pipeline', '2025-02-15 08:30:00', '2025-06-10 09:00:00');
COMMIT;

View File

@@ -17,7 +17,7 @@ CREATE TABLE users (
CREATE TABLE loans ( CREATE TABLE loans (
id int NOT NULL AUTO_INCREMENT, id int NOT NULL AUTO_INCREMENT,
username varchar(100) NOT NULL, username varchar(100) NOT NULL,
loan_code int NOT NULL UNIQUE, loan_code Char(6) NOT NULL UNIQUE,
start_date timestamp NOT NULL, start_date timestamp NOT NULL,
end_date timestamp NOT NULL, end_date timestamp NOT NULL,
take_date timestamp NULL DEFAULT NULL, take_date timestamp NULL DEFAULT NULL,
@@ -28,10 +28,7 @@ CREATE TABLE loans (
deleted bool NOT NULL DEFAULT false, deleted bool NOT NULL DEFAULT false,
note varchar(500) DEFAULT NULL, note varchar(500) DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
CONSTRAINT fk_loans_username CHECK (loan_code REGEXP '^[0-9]{6}$')
FOREIGN KEY (username) REFERENCES users(username)
ON UPDATE CASCADE
ON DELETE RESTRICT
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE items ( CREATE TABLE items (
@@ -47,15 +44,11 @@ CREATE TABLE items (
); );
CREATE TABLE apiKeys ( CREATE TABLE apiKeys (
id int NOT NULL AUTO_INCREMENT, id INT NOT NULL AUTO_INCREMENT,
api_key CHAR(15) NOT NULL UNIQUE, api_key CHAR(15) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL, entry_name VARCHAR(100) NOT NULL,
last_used_at timestamp DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, last_used_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
CONSTRAINT chk_api_key_len CHECK (CHAR_LENGTH(api_key) = 15), CHECK (api_key REGEXP '^[0-9]{15}$')
CONSTRAINT fk_apikeys_username
FOREIGN KEY (username) REFERENCES users(username)
ON UPDATE CASCADE
ON DELETE RESTRICT
) ENGINE=InnoDB; ) ENGINE=InnoDB;

View File

@@ -1,43 +1,51 @@
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import env from "dotenv"; import env from "dotenv";
import info from "./info.json" assert { type: "json" };
// frontend routes // frontend routes
import loansMgmtRouter from "./routes/app/loanMgmt.route.js"; import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
import userMgmtRouter from "./routes/app/userMgmt.route.js"; import userMgmtRouterAPP from "./routes/app/userMgmt.route.js";
// admin routes // admin routes
import userDataMgmtRouter from "./routes/admin/userDataMgmt.route.js"; import userDataMgmtRouter from "./routes/admin/userDataMgmt.route.js";
import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js"; import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js";
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js"; import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js"; import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
env.config(); env.config();
const app = express(); const app = express();
const port = 8002; const port = 8004;
app.use(cors()); app.use(cors());
// Body-Parser VOR den Routen registrieren
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// frontend routes // frontend routes
app.use("/api/loans", loansMgmtRouter); app.use("/api/loans", loansMgmtRouter);
app.use("/api/users", userMgmtRouter); app.use("/api/users", userMgmtRouterAPP);
// admin routes // admin routes
app.use("/api/admin/loan-data", loanDataMgmtRouter); app.use("/api/admin/loan-data", loanDataMgmtRouter);
app.use("/api/admin/user-data", userDataMgmtRouter); app.use("/api/admin/user-data", userDataMgmtRouter);
app.use("/api/admin/item-data", itemDataMgmtRouter); app.use("/api/admin/item-data", itemDataMgmtRouter);
app.use("/api/admin/api-data", apiDataMgmtRouter); app.use("/api/admin/api-data", apiDataMgmtRouter);
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
// Increase body size limits to support large CSV JSON payloads
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(express.json({ limit: "10mb" }));
app.listen(port, () => { app.listen(port, () => {
console.log(`Server is running on port: ${port}`); console.log(`Server is running on port: ${port}`);
}); });
app.get("/", (req, res) => {
res.send(info);
});
// error handling code // error handling code
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
// Log the error stack and send a generic error response
console.error(err.stack); console.error(err.stack);
res.status(500).send("Something broke!"); res.status(500).send("Something broke!");
}); });

View File

@@ -1,6 +1,6 @@
import { SignJWT, jwtVerify } from "jose"; import { SignJWT, jwtVerify } from "jose";
import env from "dotenv"; import env from "dotenv";
import { getAllApiKeys } from "./database"; import { getAllApiKeys } from "./database.js";
env.config(); env.config();
const secretKey = process.env.SECRET_KEY; const secretKey = process.env.SECRET_KEY;

View File

@@ -43,7 +43,7 @@ services:
DB_HOST: mysql_v2 DB_HOST: mysql_v2
DB_USER: root DB_USER: root
DB_PASSWORD: ${DB_PASSWORD_V2} DB_PASSWORD: ${DB_PASSWORD_V2}
DB_NAME: borrow_system_v2 DB_NAME: borrow_system_new
depends_on: depends_on:
- mysql_v2 - mysql_v2
restart: unless-stopped restart: unless-stopped
@@ -68,7 +68,7 @@ services:
restart: unless-stopped restart: unless-stopped
environment: environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
MYSQL_DATABASE: borrow_system_v2 MYSQL_DATABASE: borrow_system_new
TZ: Europe/Berlin TZ: Europe/Berlin
volumes: volumes:
- mysql-v2-data:/var/lib/mysql - mysql-v2-data:/var/lib/mysql