Compare commits
45 Commits
ad2395f98b
...
debian12_v
Author | SHA1 | Date | |
---|---|---|---|
893a7e041d | |||
6ea1ff799c | |||
5131266242 | |||
bb17bc735c | |||
d4b2e8db20 | |||
af7d15c97a | |||
04453fd885 | |||
bf36a6605f | |||
8f9696991f | |||
9cad1e8b6b | |||
880029a0cf | |||
b52d707bf5 | |||
32abe60d98 | |||
b6ebfcd631 | |||
ea965971f1 | |||
7ecd9dad3f | |||
eff1f61422 | |||
a4c0323100 | |||
fc755edadf | |||
7f9ed23a86 | |||
21b152ef2b | |||
0fca896cc2 | |||
f83f321876 | |||
b9d637665c | |||
378720b235 | |||
451a5a92dd | |||
85b519c5b1 | |||
49f4ba8483 | |||
27db4c7390 | |||
1b08344a0f | |||
ea5d31384a | |||
7f5f464841 | |||
ab93c9959d | |||
679ef7dcbd | |||
c3572a3d70 | |||
4fc60a08d9 | |||
a0be100db5 | |||
2143d53eb5 | |||
c4d5ebd9ae | |||
5159877d8d | |||
938e9000f8 | |||
558a8330af | |||
b3ddfd9aa5 | |||
755ebfd06b | |||
e198fce791 |
@@ -1,4 +1,4 @@
|
||||
# Backend API docs
|
||||
# Backend API docs (apiV2)
|
||||
|
||||
If you want to cooperate with me, or build something new with my backend API, feel free to reach out!
|
||||
|
||||
@@ -6,49 +6,51 @@ On this page you will learn how my API works.
|
||||
|
||||
## General information
|
||||
|
||||
When you look at my backend folder and file structure, you can see that I have two files called `API`. The first file called `api.js` is for my web frontend, because this file works together with my JWT token service.
|
||||
When you look at my backend folder and file structure, you can see that I have two files called `API`. The first file called `api.js` which is for my web frontend, because this file works together with my JWT token service.
|
||||
|
||||
**\*But I have built a second API. You can see the second API file in the same directory, the file is called `apiV2.js`.**
|
||||
But I have built a second API. You can see the second API file in the same directory, the file is called `apiV2.js`.
|
||||
|
||||
This is the file that you can use to build an API.
|
||||
But first you have to get an API Key. You can get the API key from my admin dashboard. When you don't have any access to my admin dashboard, please contact your administrator or me.
|
||||
|
||||
But first you have to get the Admin API key, stored in an `.env` file on my server.
|
||||
---
|
||||
|
||||
## Base URL
|
||||
|
||||
- Frontend: `https://insta.the1s.de`
|
||||
- Backend: `https://backend.insta.the1s.de`
|
||||
- Base path for this API: `https://backend.insta.the1s.de/apiV2`
|
||||
|
||||
You can see the status of this and all my other services at `https://status.the1s.de`.
|
||||
|
||||
_I have also build a [fallback page](https://git.the1s.de/theis.gaedigk/fallback-page). When only the application is down, you will see a friendly message and a link to the status page. (Only if the server is not down)_
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require the Admin API key (`ADMIN_ID`) as a URL parameter.
|
||||
All endpoints require an API key as a path parameter named `:key`.
|
||||
|
||||
Example: `/apiV2/items/{ADMIN_ID}`
|
||||
Example: `/apiV2/items/:key`
|
||||
|
||||
If the key is missing or invalid, the API responds with `401 Unauthorized`.
|
||||
|
||||
---
|
||||
|
||||
## URL
|
||||
## Endpoints
|
||||
|
||||
- The frontend is currently running on `https://insta.the1s.de`.
|
||||
### 1) Get all items
|
||||
|
||||
- The backend is currently running on `https://backend.insta.the1s.de`.
|
||||
GET `/apiV2/items/:key`
|
||||
|
||||
You can see the status of this and all my other services at `https://status.the1s.de`.
|
||||
Returns a list of all items wrapped in a `data` object.
|
||||
|
||||
---
|
||||
|
||||
## Current endpoints
|
||||
|
||||
### 1. Get All Items
|
||||
|
||||
**GET** `/apiV2/items/:key`
|
||||
|
||||
Returns a list of all items and their details.
|
||||
|
||||
#### Example Request
|
||||
Example request:
|
||||
|
||||
```
|
||||
GET https://backend.insta.the1s.de/apiV2/items/your_admin_key
|
||||
GET https://backend.insta.the1s.de/apiV2/items/12345
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
Example response:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -59,206 +61,66 @@ GET https://backend.insta.the1s.de/apiV2/items/your_admin_key
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"item_name": "DJI 2er Mikro 1",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"item_name": "DJI 2er Mikro 2",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"item_name": "Rode Richt Mikrofon",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"item_name": "Kamera Stativ",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"item_name": "SONY Kamera - inkl. Akkus und Objektiv",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"item_name": "MacBook inkl. Adapter",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"item_name": "SD Karten",
|
||||
"can_borrow_role": 3,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"item_name": "Kameragimbal",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"item_name": "ATEM MINI PRO",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"item_name": "Handygimbal",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"item_name": "Kameralfter",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"item_name": "Kleine Kamera 1 - inkl. Objektiv",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"item_name": "Kleine Kamera 2 - inkl. Objektiv",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each item has the following properties:
|
||||
Fields:
|
||||
|
||||
- `id`: The unique identifier for the item.
|
||||
- `item_name`: The name of the item.
|
||||
- `can_borrow_role`: The role ID that is allowed to borrow the item.
|
||||
- `inSafe`: Indicates whether the item is currently in the locker (1) or not (0). This variable/state can change over time.
|
||||
- `id`: Unique identifier
|
||||
- `item_name`: Item name
|
||||
- `can_borrow_role`: Role allowed to borrow
|
||||
- `inSafe`: 1 if in locker, 0 otherwise
|
||||
- `entry_created_at`: Creation timestamp
|
||||
|
||||
_You also get an http 200 status code._
|
||||
Status: 200 on success, 500 on failure.
|
||||
|
||||
---
|
||||
|
||||
### 2. Change Item Safe State
|
||||
### 2) Change item safe state
|
||||
|
||||
**POST** `/apiV2/controlInSafe/:key/:itemId/:state`
|
||||
POST `/apiV2/controlInSafe/:key/:itemId/:state`
|
||||
|
||||
Updates the `inSafe` state of an item (whether it is in the locker).
|
||||
Updates `inSafe` (locker) state of an item.
|
||||
|
||||
- `state` must be `"1"` (in safe) or `"0"` (not in safe).
|
||||
- `state` must be `"1"` (in safe) or `"0"` (not in safe)
|
||||
|
||||
#### Example Request
|
||||
Example request:
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/controlInSafe/your_admin_key/item_id/new_item_state
|
||||
POST https://backend.insta.the1s.de/apiV2/controlInSafe/12345/123/1
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
Example response (shape depends on database service):
|
||||
|
||||
```
|
||||
{}
|
||||
{ "data": { /* update result */ } }
|
||||
```
|
||||
|
||||
_An empty object means, that the operation was successful and no further information is returned._
|
||||
Status:
|
||||
|
||||
_You also get an http 200 status code._
|
||||
- 200 on success
|
||||
- 400 if `state` is invalid
|
||||
- 500 on failure
|
||||
|
||||
**You can get the item id on the admin panel, from your system administrator.**
|
||||
|
||||
---
|
||||
|
||||
### 3. Set Return Date
|
||||
### 3) Get loan by code
|
||||
|
||||
**POST** `/apiV2/setReturnDate/:key/:loan_code`
|
||||
GET `/apiV2/getLoanByCode/:key/:loan_code`
|
||||
|
||||
Sets the `returned_date` of a loan to the current server time.
|
||||
Retrieves the details of a specific loan.
|
||||
|
||||
- `loan_code`: The unique code of the loan.
|
||||
|
||||
#### Example Request
|
||||
Example request:
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setReturnDate/your_admin_key/your_loan_code
|
||||
GET https://backend.insta.the1s.de/apiV2/getLoanByCode/12345/123456
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
_An empty object means, that the operation was successful and no further information is returned._
|
||||
|
||||
_You also get an http 200 status code._
|
||||
|
||||
---
|
||||
|
||||
### 4. Set Take Date
|
||||
|
||||
**POST** `/apiV2/setTakeDate/:key/:loan_code`
|
||||
|
||||
Sets the `take_date` of a loan to the current server time.
|
||||
|
||||
- `loan_code`: The unique code of the loan.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setTakeDate/your_admin_key/your_loan_code
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
_An empty object means, that the operation was successful and no further information is returned._
|
||||
|
||||
_You also get an http 2xx status code._
|
||||
|
||||
---
|
||||
|
||||
### 5. Get whole loan by loan code
|
||||
|
||||
**POST** `/getLoanByCode/:key/:loan_code`
|
||||
|
||||
Retrieves the details of a specific loan by its unique code.
|
||||
|
||||
- `loan_code`: The unique code of the loan.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
GET https://backend.insta.the1s.de/getLoanByCode/your_admin_key/your_loan_code
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
Example response:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -271,36 +133,78 @@ GET https://backend.insta.the1s.de/getLoanByCode/your_admin_key/your_loan_code
|
||||
"take_date": null,
|
||||
"returned_date": null,
|
||||
"created_at": "2025-08-20T11:23:40.000Z",
|
||||
"loaned_items_id": [
|
||||
8,
|
||||
9
|
||||
],
|
||||
"loaned_items_name": [
|
||||
"SD Karten",
|
||||
"Kameragimbal"
|
||||
]
|
||||
"loaned_items_id": [8, 9],
|
||||
"loaned_items_name": ["SD Karten", "Kameragimbal"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_You also get an http 200 status code._
|
||||
Status:
|
||||
|
||||
If the loan id does not exist, you will receive a 404 status code and an error message.
|
||||
|
||||
```
|
||||
{
|
||||
"message": "Loan not found"
|
||||
}
|
||||
```
|
||||
- 200 on success
|
||||
- 404 if not found
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
### 4) Set return date (now) by loan code
|
||||
|
||||
- `403 Forbidden`: Invalid or missing API key.
|
||||
- `400 Bad Request`: Invalid parameters (e.g., wrong state value).
|
||||
- `500 Internal Server Error`: Database or server error.
|
||||
POST `/apiV2/setReturnDate/:key/:loan_code`
|
||||
|
||||
Sets the `returned_date` to the current server time.
|
||||
|
||||
**Note:** I have updated this API route, so that everytime you return or take a loan, the state of the loaned items is automatically updated.
|
||||
|
||||
**DO NOT UPDATE THE STATE MANUALLY! (only if the item was taken with an admin key)**
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setReturnDate/12345/123456
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{ "data": { /* update result */ } }
|
||||
```
|
||||
|
||||
Status: 200 on success, 500 on failure.
|
||||
|
||||
---
|
||||
|
||||
If you have questions or want to collaborate, please reach out to me!
|
||||
### 5) Set take date (now) by loan code
|
||||
|
||||
POST `/apiV2/setTakeDate/:key/:loan_code`
|
||||
|
||||
Sets the `take_date` to the current server time.
|
||||
|
||||
**Note:** I have updated this API route, so that everytime you return or take a loan, the state of the loaned items is automatically updated.
|
||||
|
||||
**DO NOT UPDATE THE STATE MANUALLY! (only if the item was taken with an admin key)**
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setTakeDate/12345/123456
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{ "data": { /* update result */ } }
|
||||
```
|
||||
|
||||
Status: 200 on success, 500 on failure.
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
- 401 Unauthorized: Missing or invalid API key
|
||||
- 400 Bad Request: Invalid parameters (e.g., wrong state value)
|
||||
- 404 Not Found: Loan not found
|
||||
- 500 Internal Server Error: Database or server error
|
||||
|
||||
---
|
||||
|
||||
If you have questions or want to collaborate, please reach out!
|
||||
|
@@ -4,9 +4,7 @@ import Layout from "./Layout/Layout";
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<p></p>
|
||||
</Layout>
|
||||
<Layout />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Box, Heading, Text, Flex, Button } from "@chakra-ui/react";
|
||||
import Sidebar from "./Sidebar";
|
||||
import UserTable from "../components/UserTable";
|
||||
import ItemTable from "../components/ItemTable";
|
||||
import LoanTable from "../components/LoanTable";
|
||||
import APIKeyTable from "@/components/APIKeyTable";
|
||||
import { MoveLeft } from "lucide-react";
|
||||
|
||||
type DashboardProps = {
|
||||
@@ -16,6 +18,24 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
||||
|
||||
const [activeView, setActiveView] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const raw = window.location.pathname.slice(1);
|
||||
if (raw) {
|
||||
setActiveView(decodeURIComponent(raw));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sync URL when activeView changes, without reloading
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!activeView) return;
|
||||
const desired = `/${encodeURIComponent(activeView)}`;
|
||||
if (window.location.pathname !== desired) {
|
||||
window.history.replaceState(null, "", desired);
|
||||
}
|
||||
}, [activeView]);
|
||||
|
||||
return (
|
||||
<Flex h="100vh">
|
||||
<Sidebar
|
||||
@@ -23,6 +43,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
||||
viewGegenstaende={() => setActiveView("Gegenstände")}
|
||||
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
||||
viewUser={() => setActiveView("User")}
|
||||
viewAPI={() => setActiveView("API")}
|
||||
/>
|
||||
<Box flex="1" display="flex" flexDirection="column">
|
||||
<Flex
|
||||
@@ -66,6 +87,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
||||
{activeView === "User" && <UserTable />}
|
||||
{activeView === "Ausleihen" && <LoanTable />}
|
||||
{activeView === "Gegenstände" && <ItemTable />}
|
||||
{activeView === "API" && <APIKeyTable />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
@@ -3,18 +3,28 @@ import { useEffect } from "react";
|
||||
import Dashboard from "./Dashboard";
|
||||
import Login from "./Login";
|
||||
import Cookies from "js-cookie";
|
||||
import Landingpage from "@/components/API/Landingpage";
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const Layout: React.FC = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [showAPI, setShowAPI] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const path = window.location.pathname.replace(/\/+$/, ""); // remove trailing slash
|
||||
if (path === "/api") {
|
||||
setShowAPI(true);
|
||||
console.log("signal");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Cookies.get("token")) {
|
||||
const verifyToken = async () => {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/verifyToken", {
|
||||
const response = await fetch(`${API_BASE}/api/verifyToken`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
@@ -34,20 +44,26 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
|
||||
const handleLogout = () => {
|
||||
Cookies.remove("token");
|
||||
window.location.pathname = "/";
|
||||
setIsLoggedIn(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
if (showAPI) {
|
||||
return (
|
||||
<main>
|
||||
{isLoggedIn ? (
|
||||
<Dashboard onLogout={() => handleLogout()} />
|
||||
) : (
|
||||
<Login onSuccess={() => setIsLoggedIn(true)} />
|
||||
)}
|
||||
<Landingpage />
|
||||
</main>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
{isLoggedIn ? (
|
||||
<Dashboard onLogout={() => handleLogout()} />
|
||||
) : (
|
||||
<Login onSuccess={() => setIsLoggedIn(true)} />
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -24,41 +24,43 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<Card.Root maxW="sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Login</Card.Title>
|
||||
<Card.Description>
|
||||
Bitte unten Ihre Admin Zugangsdaten eingeben.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Stack gap="4" w="full">
|
||||
<Field.Root>
|
||||
<Field.Label>username</Field.Label>
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>password</Field.Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
{isError && (
|
||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
||||
)}
|
||||
<Button onClick={() => handleLogin()} variant="solid">
|
||||
Login
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Card.Root maxW="sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Login</Card.Title>
|
||||
<Card.Description>
|
||||
Bitte unten Ihre Admin Zugangsdaten eingeben.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Stack gap="4" w="full">
|
||||
<Field.Root>
|
||||
<Field.Label>username</Field.Label>
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>password</Field.Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
{isError && (
|
||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
||||
)}
|
||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
||||
Login
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -6,12 +6,14 @@ type SidebarProps = {
|
||||
viewGegenstaende: () => void;
|
||||
viewSchliessfaecher: () => void;
|
||||
viewUser: () => void;
|
||||
viewAPI: () => void;
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
viewAusleihen,
|
||||
viewGegenstaende,
|
||||
viewUser,
|
||||
viewAPI,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
@@ -58,6 +60,15 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
>
|
||||
Gegenstände
|
||||
</Link>
|
||||
<Link
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
_hover={{ bg: "gray.700", textDecoration: "none" }}
|
||||
onClick={viewAPI}
|
||||
>
|
||||
API Keys
|
||||
</Link>
|
||||
</VStack>
|
||||
|
||||
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
||||
|
238
admin/src/components/API/Landingpage.tsx
Normal file
238
admin/src/components/API/Landingpage.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Spinner,
|
||||
Text,
|
||||
VStack,
|
||||
Table,
|
||||
Heading,
|
||||
HStack,
|
||||
Card,
|
||||
SimpleGrid,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import { Lock, LockOpen } from "lucide-react";
|
||||
import MyAlert from "../myChakra/MyAlert";
|
||||
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";
|
||||
|
||||
type Loan = {
|
||||
id: number;
|
||||
username: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
returned_date: string | null;
|
||||
take_date: string | null;
|
||||
loaned_items_name: string[] | string;
|
||||
};
|
||||
|
||||
type Device = {
|
||||
id: number;
|
||||
item_name: string;
|
||||
can_borrow_role: string;
|
||||
inSafe: number;
|
||||
entry_created_at: string;
|
||||
};
|
||||
|
||||
const Landingpage: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loans, setLoans] = useState<Loan[]>([]);
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [errorDsc, setErrorDsc] = useState("");
|
||||
|
||||
const setError = (
|
||||
status: "error" | "success",
|
||||
message: string,
|
||||
description: string
|
||||
) => {
|
||||
setIsError(false);
|
||||
setErrorStatus(status);
|
||||
setErrorMessage(message);
|
||||
setErrorDsc(description);
|
||||
setIsError(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const loanRes = await fetch(`${API_BASE}/apiV2/allLoans`);
|
||||
const loanData = await loanRes.json();
|
||||
if (Array.isArray(loanData)) {
|
||||
setLoans(loanData);
|
||||
} else {
|
||||
setError(
|
||||
"error",
|
||||
"Fehler beim Laden",
|
||||
"Unerwartetes Datenformat erhalten. (Ausleihen)"
|
||||
);
|
||||
}
|
||||
|
||||
const deviceRes = await fetch(`${API_BASE}/apiV2/allItems`);
|
||||
const deviceData = await deviceRes.json();
|
||||
if (Array.isArray(deviceData)) {
|
||||
setDevices(deviceData);
|
||||
} else {
|
||||
setError(
|
||||
"error",
|
||||
"Fehler beim Laden",
|
||||
"Unerwartetes Datenformat erhalten. (Geräte)"
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(
|
||||
"error",
|
||||
"Fehler beim Laden",
|
||||
"Die Ausleihen konnten nicht geladen werden."
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading as="h1" size="lg" mb={2}>
|
||||
Matthias-Claudius-Schule Technik
|
||||
</Heading>
|
||||
|
||||
<Heading as="h2" size="md" mb={4}>
|
||||
Alle Ausleihen
|
||||
</Heading>
|
||||
|
||||
{isError && (
|
||||
<MyAlert
|
||||
status={errorStatus}
|
||||
description={errorDsc}
|
||||
title={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner color="colorPalette.600" />
|
||||
<Text color="colorPalette.600">Loading...</Text>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<Table.Root size="sm" striped>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeader>
|
||||
<strong>#</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Benutzername</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Startdatum</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Enddatum</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Ausgeliehene Artikel</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Rückgabedatum</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Ausleihdatum</strong>
|
||||
</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{loans.map((loan) => (
|
||||
<Table.Row key={loan.id}>
|
||||
<Table.Cell>{loan.id}</Table.Cell>
|
||||
<Table.Cell>{loan.username}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(loan.start_date)}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(loan.end_date)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{Array.isArray(loan.loaned_items_name)
|
||||
? loan.loaned_items_name.join(", ")
|
||||
: loan.loaned_items_name}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
)}
|
||||
|
||||
{!isLoading && loans.length === 0 && !isError && (
|
||||
<Text color="gray.500" mt={2}>
|
||||
Keine Ausleihen vorhanden.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Heading as="h2" size="md" mb={4}>
|
||||
Alle Geräte
|
||||
</Heading>
|
||||
|
||||
{/* Responsive Grid mit gleich hohen Karten */}
|
||||
<SimpleGrid minChildWidth="200px" gap={2} alignItems="stretch">
|
||||
{devices.map((device) => (
|
||||
<Card.Root
|
||||
key={device.id}
|
||||
size="sm"
|
||||
bg={device.inSafe ? "green" : "red"}
|
||||
h="full"
|
||||
minH="100px"
|
||||
>
|
||||
<Card.Header>
|
||||
{device.inSafe ? <LockOpen size={16} /> : <Lock size={16} />}
|
||||
<Heading size="md">{device.item_name}</Heading>
|
||||
</Card.Header>
|
||||
<Card.Body color="fg.muted">
|
||||
<Text>Ausleihrolle: {device.can_borrow_role}</Text>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<HStack mt={3} gap={3} align="center" role="group" aria-label="Legende">
|
||||
<Text fontWeight="medium" color="fg.muted">
|
||||
Legende:
|
||||
</Text>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
colorPalette="green"
|
||||
pointerEvents="none"
|
||||
cursor="default"
|
||||
borderRadius="full"
|
||||
>
|
||||
<HStack gap={2}>
|
||||
<LockOpen size={16} />
|
||||
<Text>Im Schließfach</Text>
|
||||
</HStack>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
colorPalette="red"
|
||||
pointerEvents="none"
|
||||
cursor="default"
|
||||
borderRadius="full"
|
||||
>
|
||||
<HStack gap={2}>
|
||||
<Lock size={16} />
|
||||
<Text>Nicht im Schließfach</Text>
|
||||
</HStack>
|
||||
</Button>
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landingpage;
|
208
admin/src/components/APIKeyTable.tsx
Normal file
208
admin/src/components/APIKeyTable.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
Spinner,
|
||||
Text,
|
||||
VStack,
|
||||
Button,
|
||||
HStack,
|
||||
IconButton,
|
||||
Heading,
|
||||
} from "@chakra-ui/react";
|
||||
import { Tooltip } from "@/components/ui/tooltip";
|
||||
import MyAlert from "./myChakra/MyAlert";
|
||||
import { Trash2, RefreshCcwDot, CirclePlus } from "lucide-react";
|
||||
import Cookies from "js-cookie";
|
||||
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";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
apiKey: string;
|
||||
user: string;
|
||||
entry_created_at: string;
|
||||
};
|
||||
|
||||
const APIKeyTable: React.FC = () => {
|
||||
const [items, setItems] = useState<Items[]>([]);
|
||||
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [errorDsc, setErrorDsc] = useState("");
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [reload, setReload] = useState(false);
|
||||
const [addAPIForm, setAddAPIForm] = useState(false);
|
||||
|
||||
const setError = (
|
||||
status: "error" | "success",
|
||||
message: string,
|
||||
description: string
|
||||
) => {
|
||||
setIsError(false);
|
||||
setErrorStatus(status);
|
||||
setErrorMessage(message);
|
||||
setErrorDsc(description);
|
||||
setIsError(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/apiKeys`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
setError("error", "Failed to fetch items", "There is an error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData().then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
setItems(data);
|
||||
}
|
||||
});
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Action toolbar */}
|
||||
<HStack
|
||||
mb={4}
|
||||
gap={3}
|
||||
justify="flex-start"
|
||||
align="center"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Tooltip content="API Keys neu laden" openDelay={300}>
|
||||
<IconButton
|
||||
aria-label="Refresh API Keys"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rounded="md"
|
||||
shadow="sm"
|
||||
_hover={{ shadow: "md", transform: "translateY(-2px)" }}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
onClick={() => setReload(!reload)}
|
||||
>
|
||||
<RefreshCcwDot size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Neuen API Key hinzufügen" openDelay={300}>
|
||||
<Button
|
||||
size="sm"
|
||||
colorPalette="teal"
|
||||
variant="solid"
|
||||
rounded="md"
|
||||
fontWeight="semibold"
|
||||
shadow="sm"
|
||||
_hover={{ shadow: "md", bg: "colorPalette.600" }}
|
||||
_active={{ bg: "colorPalette.700" }}
|
||||
onClick={() => {
|
||||
setAddAPIForm(true);
|
||||
}}
|
||||
>
|
||||
<CirclePlus size={18} style={{ marginRight: 6 }} />
|
||||
Neuen API Key hinzufügen
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
{/* End action toolbar */}
|
||||
|
||||
<Heading marginBottom={4} size="md">
|
||||
Gegenstände
|
||||
</Heading>
|
||||
{isError && (
|
||||
<MyAlert
|
||||
status={errorStatus}
|
||||
description={errorDsc}
|
||||
title={errorMessage}
|
||||
/>
|
||||
)}
|
||||
{isLoading && (
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner color="colorPalette.600" />
|
||||
<Text color="colorPalette.600">Loading...</Text>
|
||||
</VStack>
|
||||
)}
|
||||
{addAPIForm && (
|
||||
<AddAPIKey
|
||||
onClose={() => {
|
||||
setAddAPIForm(false);
|
||||
setReload(!reload);
|
||||
}}
|
||||
alert={setError}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Table.Root size="sm" striped>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeader>
|
||||
<strong>#</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>API Key</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Benutzer</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag erstellt 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>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
deleteAPKey(apiKey.id).then((response) => {
|
||||
if (response.success) {
|
||||
setItems(items.filter((i) => i.id !== apiKey.id));
|
||||
setError(
|
||||
"success",
|
||||
"Gegenstand gelöscht",
|
||||
"Der Gegenstand wurde erfolgreich gelöscht."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
colorPalette="red"
|
||||
size="sm"
|
||||
ml={2}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default APIKeyTable;
|
81
admin/src/components/AddAPIKey.tsx
Normal file
81
admin/src/components/AddAPIKey.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||
import { createAPIentry } from "@/utils/userActions";
|
||||
|
||||
type AddAPIKeyProps = {
|
||||
onClose: () => void;
|
||||
alert: (
|
||||
status: "success" | "error",
|
||||
message: string,
|
||||
description: string
|
||||
) => void;
|
||||
};
|
||||
|
||||
const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<Card.Root maxW="sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Neuen API Key erstellen</Card.Title>
|
||||
<Card.Description>
|
||||
Füllen Sie das folgende Formular aus, um einen API Key zu erstellen.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Stack gap="4" w="full">
|
||||
<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.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
onClick={async () => {
|
||||
const apiKey =
|
||||
(
|
||||
document.getElementById("apiKey") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const user =
|
||||
(
|
||||
document.getElementById("user") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
|
||||
if (!apiKey || !user) return;
|
||||
|
||||
const res = await createAPIentry(apiKey, user);
|
||||
if (res.success) {
|
||||
alert(
|
||||
"success",
|
||||
"API Key erstellt",
|
||||
"Der API Key wurde erfolgreich erstellt."
|
||||
);
|
||||
onClose();
|
||||
} else {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler beim Erstellen des API Keys",
|
||||
res.message ||
|
||||
"Beim Erstellen des API Keys ist ein Fehler aufgetreten. (frontend bug)"
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAPIKey;
|
@@ -33,7 +33,7 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
|
||||
<Input
|
||||
id="can_borrow_role"
|
||||
type="number"
|
||||
placeholder="Zahl (z.B. 2)"
|
||||
placeholder="Zahl (1 - 4)"
|
||||
/>
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
@@ -68,8 +68,10 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler",
|
||||
"Der Gegenstand konnte nicht erstellt werden."
|
||||
res.message ||
|
||||
"Der Gegenstand konnte nicht erstellt werden. (frontend bug)"
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@@ -55,57 +55,64 @@ const ChangePWform: React.FC<ChangePWformProps> = ({
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end" gap="2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
onClick={async () => {
|
||||
const newPassword =
|
||||
(
|
||||
document.getElementById("new_password") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const confirmNewPassword =
|
||||
(
|
||||
document.getElementById(
|
||||
"confirm_new_password"
|
||||
) as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
<Card.Footer gap="2">
|
||||
<Stack w="full" gap="3">
|
||||
<Stack direction="row" justify="flex-end" gap="2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
onClick={async () => {
|
||||
const newPassword =
|
||||
(
|
||||
document.getElementById(
|
||||
"new_password"
|
||||
) as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const confirmNewPassword =
|
||||
(
|
||||
document.getElementById(
|
||||
"confirm_new_password"
|
||||
) as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
|
||||
if (!newPassword || newPassword !== confirmNewPassword) {
|
||||
subAlert("Passwörter stimmen nicht überein!");
|
||||
return;
|
||||
}
|
||||
if (!newPassword || newPassword !== confirmNewPassword) {
|
||||
subAlert("Passwörter stimmen nicht überein!");
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await changePW(newPassword, username);
|
||||
if (res.success) {
|
||||
alert(
|
||||
"success",
|
||||
"Passwort geändert",
|
||||
"Das Passwort wurde erfolgreich geändert."
|
||||
);
|
||||
onClose();
|
||||
} else {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler",
|
||||
"Das Passwort konnte nicht geändert werden."
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Ändern
|
||||
</Button>
|
||||
{showSubAlert && (
|
||||
<Alert.Root status="error">
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Title>{subAlertMessage}</Alert.Title>
|
||||
</Alert.Content>
|
||||
</Alert.Root>
|
||||
)}
|
||||
const res = await changePW(newPassword, username);
|
||||
if (res.success) {
|
||||
alert(
|
||||
"success",
|
||||
"Passwort geändert",
|
||||
"Das Passwort wurde erfolgreich geändert."
|
||||
);
|
||||
onClose();
|
||||
} else {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler",
|
||||
"Das Passwort konnte nicht geändert werden."
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Ändern
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{showSubAlert && (
|
||||
<Alert.Root status="error">
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Title>{subAlertMessage}</Alert.Title>
|
||||
</Alert.Content>
|
||||
</Alert.Root>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
IconButton,
|
||||
Heading,
|
||||
Icon,
|
||||
Tag,
|
||||
Input,
|
||||
} from "@chakra-ui/react";
|
||||
import { Tooltip } from "@/components/ui/tooltip";
|
||||
import MyAlert from "./myChakra/MyAlert";
|
||||
@@ -19,13 +19,23 @@ import {
|
||||
CirclePlus,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Save,
|
||||
} from "lucide-react";
|
||||
import Cookies from "js-cookie";
|
||||
import { useState, useEffect } from "react";
|
||||
import { deleteItem } from "@/utils/userActions";
|
||||
import {
|
||||
deleteItem,
|
||||
handleEditItems,
|
||||
changeSafeState,
|
||||
} 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";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
item_name: string;
|
||||
@@ -44,6 +54,18 @@ const ItemTable: React.FC = () => {
|
||||
const [reload, setReload] = useState(false);
|
||||
const [addForm, setAddForm] = useState(false);
|
||||
|
||||
const handleItemNameChange = (id: number, value: string) => {
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? { ...it, item_name: value } : it))
|
||||
);
|
||||
};
|
||||
|
||||
const handleCanBorrowRoleChange = (id: number, value: string) => {
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? { ...it, can_borrow_role: value } : it))
|
||||
);
|
||||
};
|
||||
|
||||
const setError = (
|
||||
status: "error" | "success",
|
||||
message: string,
|
||||
@@ -60,7 +82,7 @@ const ItemTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/allItems", {
|
||||
const response = await fetch(`${API_BASE}/api/allItems`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
@@ -180,61 +202,85 @@ const ItemTable: React.FC = () => {
|
||||
{items.map((item) => (
|
||||
<Table.Row key={item.id}>
|
||||
<Table.Cell>{item.id}</Table.Cell>
|
||||
<Table.Cell>{item.item_name}</Table.Cell>
|
||||
<Table.Cell>{item.can_borrow_role}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{item.inSafe ? (
|
||||
<Tag.Root
|
||||
size="md"
|
||||
bg="green.500"
|
||||
color="white"
|
||||
px={4}
|
||||
py={1.5}
|
||||
rounded="full"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
shadow="sm"
|
||||
_hover={{ shadow: "md" }}
|
||||
>
|
||||
<Icon as={CheckCircle2} boxSize={4} />
|
||||
<Text
|
||||
as="span"
|
||||
fontSize="xs"
|
||||
letterSpacing="wide"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
Yes
|
||||
</Text>
|
||||
</Tag.Root>
|
||||
) : (
|
||||
<Tag.Root
|
||||
size="md"
|
||||
bg="red.500"
|
||||
color="white"
|
||||
px={4}
|
||||
py={1.5}
|
||||
rounded="full"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
shadow="sm"
|
||||
_hover={{ shadow: "md" }}
|
||||
>
|
||||
<Icon as={XCircle} boxSize={4} />
|
||||
<Text
|
||||
as="span"
|
||||
fontSize="xs"
|
||||
letterSpacing="wide"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
No
|
||||
</Text>
|
||||
</Tag.Root>
|
||||
)}
|
||||
<Input
|
||||
onChange={(e) =>
|
||||
handleItemNameChange(item.id, e.target.value)
|
||||
}
|
||||
value={item.item_name}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Input
|
||||
onChange={(e) =>
|
||||
handleCanBorrowRoleChange(item.id, e.target.value)
|
||||
}
|
||||
value={item.can_borrow_role}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
changeSafeState(item.id).then(() => setReload(!reload))
|
||||
}
|
||||
size="xs"
|
||||
rounded="full"
|
||||
px={3}
|
||||
py={1}
|
||||
gap={2}
|
||||
variant="ghost"
|
||||
color={item.inSafe ? "green.600" : "red.600"}
|
||||
borderWidth="1px"
|
||||
borderColor={item.inSafe ? "green.300" : "red.300"}
|
||||
_hover={{
|
||||
bg: item.inSafe ? "green.50" : "red.50",
|
||||
borderColor: item.inSafe ? "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"
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
as={item.inSafe ? CheckCircle2 : XCircle}
|
||||
boxSize={3.5}
|
||||
mr={2}
|
||||
/>
|
||||
<Text as="span" fontSize="xs" fontWeight="semibold">
|
||||
{item.inSafe ? "Yes" : "No"}
|
||||
</Text>
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleEditItems(
|
||||
item.id,
|
||||
item.item_name,
|
||||
item.can_borrow_role
|
||||
).then((response) => {
|
||||
if (response.success) {
|
||||
setError(
|
||||
"success",
|
||||
"Gegenstand erfolgreich bearbeitet!",
|
||||
"Gegenstand " +
|
||||
'"' +
|
||||
item.item_name +
|
||||
'" mit ID ' +
|
||||
item.id +
|
||||
" bearbeitet."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
colorPalette="teal"
|
||||
size="sm"
|
||||
>
|
||||
<Save />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
deleteItem(item.id).then((response) => {
|
||||
|
@@ -18,6 +18,11 @@ 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";
|
||||
|
||||
const LoanTable: React.FC = () => {
|
||||
const [items, setItems] = useState<Loan[]>([]);
|
||||
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
|
||||
@@ -55,7 +60,7 @@ const LoanTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/allLoans", {
|
||||
const response = await fetch(`${API_BASE}/api/allLoans`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
|
@@ -1,7 +1,12 @@
|
||||
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";
|
||||
|
||||
export const fetchUserData = async () => {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/allUsers", {
|
||||
const response = await fetch(`${API_BASE}/api/allUsers`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
|
@@ -1,5 +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";
|
||||
|
||||
export type LoginSuccess = { success: true };
|
||||
export type LoginFailure = {
|
||||
success: false;
|
||||
@@ -13,7 +18,7 @@ export const loginFunc = async (
|
||||
password: string
|
||||
): Promise<LoginResult> => {
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/loginAdmin", {
|
||||
const response = await fetch(`${API_BASE}/api/loginAdmin`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
|
@@ -1,9 +1,14 @@
|
||||
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";
|
||||
|
||||
export const handleDelete = async (userId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/deleteUser/${userId}`,
|
||||
`${API_BASE}/api/deleteUser/${userId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -28,7 +33,7 @@ export const handleEdit = async (
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/editUser/${userId}`,
|
||||
`${API_BASE}/api/editUser/${userId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -54,17 +59,14 @@ export const createUser = async (
|
||||
password: string
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/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/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");
|
||||
}
|
||||
@@ -77,17 +79,14 @@ export const createUser = async (
|
||||
|
||||
export const changePW = async (newPassword: string, username: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/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/changePWadmin`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ newPassword, username }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to change password");
|
||||
}
|
||||
@@ -101,7 +100,7 @@ export const changePW = async (newPassword: string, username: string) => {
|
||||
export const deleteLoan = async (loanId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/deleteLoan/${loanId}`,
|
||||
`${API_BASE}/api/deleteLoan/${loanId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -122,7 +121,7 @@ export const deleteLoan = async (loanId: number) => {
|
||||
export const deleteItem = async (itemId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/deleteItem/${itemId}`,
|
||||
`${API_BASE}/api/deleteItem/${itemId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -145,19 +144,20 @@ export const createItem = async (
|
||||
can_borrow_role: number
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/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/createItem`, {
|
||||
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 create item");
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes darf nicht mehrmals vergeben werden.",
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -165,3 +165,93 @@ export const createItem = async (
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const handleEditItems = async (
|
||||
itemId: number,
|
||||
item_name: string,
|
||||
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 }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to edit item");
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error editing item:", error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const changeSafeState = async (itemId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/changeSafeState/${itemId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to change safe state");
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error changing safe state:", error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const createAPIentry = async (apiKey: string, user: 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 }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Fehler beim Erstellen des API Keys. Achten Sie darauf, dass alle Felder ausgefüllt sind und der API Key nicht doppelt vergeben wird.",
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error creating API entry:", error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAPKey = async (apiKeyId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteAPKey/${apiKeyId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete API key");
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error deleting API key:", error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
@@ -29,7 +29,8 @@
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"ignoreDeprecations": "6.0"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
12
backend/package-lock.json
generated
12
backend/package-lock.json
generated
@@ -14,7 +14,8 @@
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3"
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -713,6 +714,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz",
|
||||
"integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
@@ -16,6 +16,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3"
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6"
|
||||
}
|
||||
}
|
||||
|
@@ -20,9 +20,167 @@ import {
|
||||
createItem,
|
||||
changeUserPassword,
|
||||
changeUserPasswordFRONTEND,
|
||||
changeInSafeStateV2,
|
||||
updateItemByID,
|
||||
getAllApiKeys,
|
||||
createAPIentry,
|
||||
deleteAPKey,
|
||||
getLoanInfoWithID,
|
||||
} from "../services/database.js";
|
||||
import { authenticate, generateToken } from "../services/tokenService.js";
|
||||
const router = express.Router();
|
||||
import nodemailer from "nodemailer";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
// Nice HTML + text templates for the loan email
|
||||
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
const itemsList =
|
||||
Array.isArray(items) && items.length
|
||||
? `<ul style="margin:8px 0 0 16px; padding:0;">${items
|
||||
.map((i) => `<li style="margin:4px 0;">${i}</li>`)
|
||||
.join("")}</ul>`
|
||||
: "<span>N/A</span>";
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</head>
|
||||
<body style="margin:0; padding:0; background:#f6f9fc; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; color:#111827;">
|
||||
<div style="padding:24px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px; margin:0 auto; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
|
||||
<tr>
|
||||
<td style="padding:20px 24px; background:${brand}; color:#ffffff;">
|
||||
<h1 style="margin:0; font-size:18px;">Neue Ausleihe erstellt</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:20px 24px;">
|
||||
<p style="margin:0 0 12px 0;">Es wurde eine neue Ausleihe angelegt. Hier sind die Details:</p>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:8px 0; color:#6b7280; width:180px;">Benutzer</td>
|
||||
<td style="padding:8px 0; font-weight:600;">${
|
||||
user || "N/A"
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0; color:#6b7280; vertical-align:top;">Ausgeliehene Gegenstände</td>
|
||||
<td style="padding:8px 0; font-weight:600;">${itemsList}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0; color:#6b7280;">Startdatum</td>
|
||||
<td style="padding:8px 0; font-weight:600;">${formatDateTime(
|
||||
startDate
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0; color:#6b7280;">Enddatum</td>
|
||||
<td style="padding:8px 0; font-weight:600;">${formatDateTime(
|
||||
endDate
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0; color:#6b7280;">Erstellt am</td>
|
||||
<td style="padding:8px 0; font-weight:600;">${formatDateTime(
|
||||
createdDate
|
||||
)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:20px 0 0 0; font-size:14px;">
|
||||
<a href="https://admin.insta.the1s.de/api" style="color:${brand}; text-decoration:underline;" target="_blank" rel="noopener noreferrer">
|
||||
Zur Übersicht aller Ausleihen
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin:16px 0 0 0; font-size:12px; color:#6b7280;">Diese E-Mail wurde automatisch vom Ausleihsystem gesendet. Bitte nicht antworten.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||
const itemsText =
|
||||
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
||||
return [
|
||||
"Neue Ausleihe erstellt",
|
||||
"",
|
||||
`Benutzer: ${user || "N/A"}`,
|
||||
`Gegenstände: ${itemsText}`,
|
||||
`Start: ${formatDateTime(startDate)}`,
|
||||
`Ende: ${formatDateTime(endDate)}`,
|
||||
`Erstellt am: ${formatDateTime(createdDate)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.MAIL_USER,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const info = await transporter.sendMail({
|
||||
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
|
||||
to: process.env.MAIL_SENDEES,
|
||||
subject: "Eine neue Ausleihe wurde erstellt!",
|
||||
text: buildLoanEmailText({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
}),
|
||||
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
||||
});
|
||||
|
||||
console.log("Message sent:", info.messageId);
|
||||
})();
|
||||
console.log("sendMailLoan called");
|
||||
}
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (value == null) return "N/A";
|
||||
|
||||
const toOut = (d) => {
|
||||
if (!(d instanceof Date) || isNaN(d.getTime())) return "N/A";
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = d.getFullYear();
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${dd}.${mm}.${yyyy} ${hh}:${mi} Uhr`;
|
||||
};
|
||||
|
||||
if (value instanceof Date) return toOut(value);
|
||||
if (typeof value === "number") return toOut(new Date(value));
|
||||
|
||||
const s = String(value).trim();
|
||||
|
||||
// Direct pattern: "YYYY-MM-DD[ T]HH:mm[:ss]"
|
||||
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::\d{2})?/);
|
||||
if (m) {
|
||||
const [, y, M, d, h, min] = m;
|
||||
return `${d}.${M}.${y} ${h}:${min} Uhr`;
|
||||
}
|
||||
|
||||
// ISO or other parseable formats
|
||||
const dObj = new Date(s);
|
||||
if (!isNaN(dObj.getTime())) return toOut(dObj);
|
||||
|
||||
return "N/A";
|
||||
};
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
const result = await loginFunc(req.body.username, req.body.password);
|
||||
@@ -153,6 +311,15 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const mailInfo = await getLoanInfoWithID(result.data.id);
|
||||
console.log(mailInfo);
|
||||
sendMailLoan(
|
||||
mailInfo.data.username,
|
||||
mailInfo.data.loaned_items_name,
|
||||
mailInfo.data.start_date,
|
||||
mailInfo.data.end_date,
|
||||
mailInfo.data.created_at
|
||||
);
|
||||
return res.status(201).json({
|
||||
message: "Loan created successfully",
|
||||
loanId: result.data.id,
|
||||
@@ -306,4 +473,88 @@ router.post("/changePWadmin", authenticate, async (req, res) => {
|
||||
return res.status(500).json({ message: "Failed to change password" });
|
||||
});
|
||||
|
||||
router.post("/updateItemByID", authenticate, async (req, res) => {
|
||||
const role = req.body.can_borrow_role;
|
||||
const itemId = req.body.itemId;
|
||||
const item_name = req.body.item_name;
|
||||
const result = await updateItemByID(itemId, item_name, role);
|
||||
if (result.success) {
|
||||
return res.status(200).json({ message: "Item updated successfully" });
|
||||
}
|
||||
return res.status(500).json({ message: "Failed to update item" });
|
||||
});
|
||||
|
||||
router.put("/changeSafeState/:itemId", authenticate, async (req, res) => {
|
||||
const itemId = req.params.itemId;
|
||||
const result = await changeInSafeStateV2(itemId);
|
||||
if (result.success) {
|
||||
return res
|
||||
.status(200)
|
||||
.json({ message: "Item safe state updated successfully" });
|
||||
}
|
||||
return res.status(500).json({ message: "Failed to update item safe state" });
|
||||
});
|
||||
|
||||
router.get("/apiKeys", authenticate, async (req, res) => {
|
||||
const result = await getAllApiKeys();
|
||||
if (result.success) {
|
||||
return res.status(200).json(result.data);
|
||||
}
|
||||
return res.status(500).json({ message: "Failed to fetch API keys" });
|
||||
});
|
||||
|
||||
router.delete("/deleteAPKey/:id", authenticate, async (req, res) => {
|
||||
const apiKeyId = req.params.id;
|
||||
const result = await deleteAPKey(apiKeyId);
|
||||
if (result.success) {
|
||||
return res.status(200).json({ message: "API key deleted successfully" });
|
||||
}
|
||||
return res.status(500).json({ message: "Failed to delete API key" });
|
||||
});
|
||||
|
||||
router.post("/createAPIentry", authenticate, async (req, res) => {
|
||||
const apiKey = req.body.apiKey;
|
||||
const user = req.body.user;
|
||||
if (!apiKey || !user) {
|
||||
return res.status(400).json({ message: "API key and user are required" });
|
||||
}
|
||||
|
||||
// Ensure apiKey is a number
|
||||
const apiKeyNum = Number(apiKey);
|
||||
if (!Number.isFinite(apiKeyNum)) {
|
||||
return res.status(400).json({ message: "API key must be a number" });
|
||||
}
|
||||
|
||||
const result = await createAPIentry(apiKeyNum, user);
|
||||
if (result.success) {
|
||||
return res.status(201).json({ message: "API key created successfully" });
|
||||
}
|
||||
if (result.code === "DUPLICATE") {
|
||||
return res.status(409).json({ message: "API key already exists" });
|
||||
}
|
||||
return res.status(500).json({ message: "Failed to create API key" });
|
||||
});
|
||||
|
||||
router.get("/apiKeys/validate/:key", async (req, res) => {
|
||||
try {
|
||||
const rawKey = req.params.key;
|
||||
const result = await getAllApiKeys();
|
||||
if (!result.success || !Array.isArray(result.data)) {
|
||||
return res.status(500).json({ valid: false });
|
||||
}
|
||||
|
||||
const isValid = result.data.some((entry) => {
|
||||
const val = String(
|
||||
entry?.key ?? entry?.apiKey ?? entry?.api_key ?? entry
|
||||
);
|
||||
return val === String(rawKey);
|
||||
});
|
||||
|
||||
return res.status(200).json({ valid: isValid });
|
||||
} catch (err) {
|
||||
console.error("validate api key error:", err);
|
||||
return res.status(500).json({ valid: false });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@@ -3,33 +3,68 @@ import dotenv from "dotenv";
|
||||
import {
|
||||
getItemsFromDatabaseV2,
|
||||
changeInSafeStateV2,
|
||||
setReturnDateV2,
|
||||
setTakeDateV2,
|
||||
setReturnDateV2,
|
||||
getLoanByCodeV2,
|
||||
getAllLoansV2,
|
||||
getAPIkey,
|
||||
} from "../services/database.js";
|
||||
|
||||
dotenv.config();
|
||||
const router = express.Router();
|
||||
|
||||
// Route for API to get ALL items from the database
|
||||
router.get("/items/:key", async (req, res) => {
|
||||
if (req.params.key === process.env.ADMIN_ID) {
|
||||
const result = await getItemsFromDatabaseV2();
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to fetch items" });
|
||||
async function validateAPIKey(apiKey) {
|
||||
try {
|
||||
if (!apiKey) return false;
|
||||
const result = await getAPIkey();
|
||||
if (!result?.success || !Array.isArray(result.data)) return false;
|
||||
return result.data.some((row) => String(row.apiKey) === String(apiKey));
|
||||
} catch (err) {
|
||||
console.error("validateAPIKey error:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a guard that returns Access Denied instead of hanging
|
||||
const apiKeyGuard = async (req, res, next) => {
|
||||
try {
|
||||
const key = req.params.key;
|
||||
if (!key) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "Access denied: missing API key" });
|
||||
}
|
||||
const ok = await validateAPIKey(key);
|
||||
if (!ok) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "Access denied: invalid API key" });
|
||||
}
|
||||
next();
|
||||
} catch (e) {
|
||||
console.error("apiKeyGuard error:", e);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
// Route for API to get ALL items from the database
|
||||
router.get("/items/:key", apiKeyGuard, async (req, res) => {
|
||||
const result = await getItemsFromDatabaseV2();
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(403).json({ message: "Access denied" });
|
||||
res.status(500).json({ message: "Failed to fetch items" });
|
||||
}
|
||||
});
|
||||
|
||||
// Route for API to control the position of an item
|
||||
router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => {
|
||||
if (req.params.key === process.env.ADMIN_ID) {
|
||||
router.post(
|
||||
"/controlInSafe/:key/:itemId/:state",
|
||||
apiKeyGuard,
|
||||
async (req, res) => {
|
||||
const itemId = req.params.itemId;
|
||||
const state = req.params.state;
|
||||
|
||||
if (state === "1" || state === "0") {
|
||||
const result = await changeInSafeStateV2(itemId, state);
|
||||
if (result.success) {
|
||||
@@ -40,53 +75,58 @@ router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => {
|
||||
} else {
|
||||
res.status(400).json({ message: "Invalid state value" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Route for API to get a loan by its code
|
||||
router.get("/getLoanByCode/:key/:loan_code", apiKeyGuard, async (req, res) => {
|
||||
const loan_code = req.params.loan_code;
|
||||
const result = await getLoanByCodeV2(loan_code);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(403).json({ message: "Access denied" });
|
||||
res.status(404).json({ message: "Loan not found" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/getLoanByCode/:key/:loan_code", async (req, res) => {
|
||||
if (req.params.key === process.env.ADMIN_ID) {
|
||||
const loan_code = req.params.loan_code;
|
||||
|
||||
const result = await getLoanByCodeV2(loan_code);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(404).json({ message: "Loan not found" });
|
||||
}
|
||||
// Route for API to set the return date by the loan code
|
||||
router.post("/setReturnDate/:key/:loan_code", apiKeyGuard, async (req, res) => {
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setReturnDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set return date" });
|
||||
}
|
||||
});
|
||||
|
||||
// Route for API to set the return date
|
||||
router.post("/setReturnDate/:key/:loan_code", async (req, res) => {
|
||||
if (req.params.key === process.env.ADMIN_ID) {
|
||||
const loanCode = req.params.loan_code;
|
||||
|
||||
const result = await setReturnDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set return date" });
|
||||
}
|
||||
// Route for API to set the take away date by the loan code
|
||||
router.post("/setTakeDate/:key/:loan_code", apiKeyGuard, async (req, res) => {
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setTakeDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(403).json({ message: "Access denied" });
|
||||
res.status(500).json({ message: "Failed to set take date" });
|
||||
}
|
||||
});
|
||||
|
||||
// Route for API to set the take away date
|
||||
router.post("/setTakeDate/:key/:loan_code", async (req, res) => {
|
||||
if (req.params.key === process.env.ADMIN_ID) {
|
||||
const loanCode = req.params.loan_code;
|
||||
// Route for API to get ALL loans from the database without sensitive info (only for landingpage)
|
||||
router.get("/allLoans", async (req, res) => {
|
||||
const result = await getAllLoansV2();
|
||||
if (result.success) {
|
||||
return res.status(200).json(result.data);
|
||||
}
|
||||
return res.status(500).json({ message: "Failed to fetch loans" });
|
||||
});
|
||||
|
||||
const result = await setTakeDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set take date" });
|
||||
}
|
||||
// Route for API to get ALL items from the database (only for landingpage)
|
||||
router.get("/allItems", async (req, res) => {
|
||||
const result = await getItemsFromDatabaseV2();
|
||||
if (result.success) {
|
||||
res.status(200).json(result.data);
|
||||
} else {
|
||||
res.status(403).json({ message: "Access denied" });
|
||||
res.status(500).json({ message: "Failed to fetch items" });
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -58,6 +58,14 @@ CREATE TABLE `lockers` (
|
||||
UNIQUE KEY `locker_number` (`locker_number`)
|
||||
);
|
||||
|
||||
CREATE TABLE `apiKeys` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`apiKey` int NOT NULL UNIQUE,
|
||||
`user` VARCHAR(255) NOT NULL,
|
||||
`entry_created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
INSERT INTO `items` (`item_name`, `can_borrow_role`, `inSafe`) VALUES
|
||||
('DJI 1er Mikro', 4, 1),
|
||||
('DJI 2er Mikro 1', 4, 1),
|
||||
|
@@ -40,10 +40,10 @@ export const getLoanByCodeV2 = async (loan_code) => {
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const changeInSafeStateV2 = async (itemId, state) => {
|
||||
export const changeInSafeStateV2 = async (itemId) => {
|
||||
const [result] = await pool.query(
|
||||
"UPDATE items SET inSafe = ? WHERE id = ?",
|
||||
[state, itemId]
|
||||
"UPDATE items SET inSafe = NOT inSafe WHERE id = ?",
|
||||
[itemId]
|
||||
);
|
||||
if (result.affectedRows > 0) {
|
||||
return { success: true };
|
||||
@@ -52,22 +52,56 @@ export const changeInSafeStateV2 = async (itemId, state) => {
|
||||
};
|
||||
|
||||
export const setReturnDateV2 = async (loanCode) => {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
|
||||
if (items.length === 0) return { success: false };
|
||||
|
||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||
? items[0].loaned_items_id
|
||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||
|
||||
const [setItemStates] = await pool.query(
|
||||
"UPDATE items SET inSafe = 1 WHERE id IN (?)",
|
||||
[itemIds]
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
if (result.affectedRows > 0) {
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const setTakeDateV2 = async (loanCode) => {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
|
||||
if (items.length === 0) return { success: false };
|
||||
|
||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||
? items[0].loaned_items_id
|
||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||
|
||||
const [setItemStates] = await pool.query(
|
||||
"UPDATE items SET inSafe = 0 WHERE id IN (?)",
|
||||
[itemIds]
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
if (result.affectedRows > 0) {
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
@@ -149,6 +183,16 @@ export const getBorrowableItemsFromDatabase = async (
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const getLoanInfoWithID = async (loanId) => {
|
||||
const [rows] = await pool.query("SELECT * FROM loans WHERE id = ?;", [
|
||||
loanId,
|
||||
]);
|
||||
if (rows.length > 0) {
|
||||
return { success: true, data: rows[0] };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const createLoanInDatabase = async (
|
||||
username,
|
||||
startDate,
|
||||
@@ -230,7 +274,7 @@ export const createLoanInDatabase = async (
|
||||
// Generate unique loan_code (retry a few times)
|
||||
let loanCode = null;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const candidate = Math.floor(1000 + Math.random() * 900000); // 4-6 digits
|
||||
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
||||
const [exists] = await conn.query(
|
||||
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
||||
[candidate]
|
||||
@@ -439,3 +483,55 @@ export const changeUserPasswordFRONTEND = async (
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const updateItemByID = async (itemId, item_name, can_borrow_role) => {
|
||||
const [result] = await pool.query(
|
||||
"UPDATE items SET item_name = ?, can_borrow_role = ? WHERE id = ?",
|
||||
[item_name, can_borrow_role, itemId]
|
||||
);
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const getAllLoansV2 = async () => {
|
||||
const [rows] = await pool.query(
|
||||
"SELECT id, username, start_date, end_date, loaned_items_name, returned_date, take_date FROM loans"
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
return { success: true, data: rows };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const getAllApiKeys = async () => {
|
||||
const [rows] = await pool.query("SELECT * FROM apiKeys");
|
||||
if (rows.length > 0) {
|
||||
return { success: true, data: rows };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const createAPIentry = async (apiKey, user) => {
|
||||
const [result] = await pool.query(
|
||||
"INSERT INTO apiKeys (apiKey, user) VALUES (?, ?)",
|
||||
[apiKey, user]
|
||||
);
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const deleteAPKey = async (apiKeyId) => {
|
||||
const [result] = await pool.query("DELETE FROM apiKeys WHERE id = ?", [
|
||||
apiKeyId,
|
||||
]);
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const getAPIkey = async () => {
|
||||
const [rows] = await pool.query("SELECT apiKey FROM apiKeys");
|
||||
if (rows.length > 0) {
|
||||
return { success: true, data: rows };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
@@ -19,6 +19,11 @@ type Loan = {
|
||||
loaned_items_name: string[];
|
||||
};
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
const formatDate = (iso: string | null) => {
|
||||
if (!iso) return "-";
|
||||
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
||||
@@ -28,7 +33,7 @@ const formatDate = (iso: string | null) => {
|
||||
};
|
||||
|
||||
async function fetchUserLoans(): Promise<Loan[]> {
|
||||
const res = await fetch("https://backend.insta.the1s.de/api/userLoans", {
|
||||
const res = await fetch(`${API_BASE}/api/userLoans`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${Cookies.get("token") || ""}` },
|
||||
});
|
||||
|
@@ -6,6 +6,11 @@ export const ALL_ITEMS_UPDATED_EVENT = "allItemsUpdated";
|
||||
export const BORROWABLE_ITEMS_UPDATED_EVENT = "borrowableItemsUpdated";
|
||||
export const AUTH_LOGOUT_EVENT = "authLogout";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
let sendError = false;
|
||||
|
||||
function logout() {
|
||||
@@ -25,7 +30,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
if (!token) return;
|
||||
// First we fetch all items that are potentially available for borrowing
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/items", {
|
||||
const response = await fetch(`${API_BASE}/api/items`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -57,7 +62,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
|
||||
// get all loans
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/loans", {
|
||||
const response = await fetch(`${API_BASE}/api/loans`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -89,7 +94,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
|
||||
// get user loans
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/userLoans", {
|
||||
const response = await fetch(`${API_BASE}/api/userLoans`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -122,7 +127,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
|
||||
export const loginUser = async (username: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/login", {
|
||||
const response = await fetch(`${API_BASE}/api/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -158,7 +163,7 @@ export const getBorrowableItems = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/borrowableItems", {
|
||||
const response = await fetch(`${API_BASE}/api/borrowableItems`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
|
@@ -2,10 +2,15 @@ import { myToast } from "./toastify";
|
||||
import Cookies from "js-cookie";
|
||||
import { queryClient } from "./queryClient";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export const handleDeleteLoan = async (loanID: number): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/deleteLoan/${loanID}`,
|
||||
`${API_BASE}/api/deleteLoan/${loanID}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -75,17 +80,14 @@ export const rmFromRemove = (itemID: number) => {
|
||||
|
||||
export const createLoan = async (startDate: string, endDate: string) => {
|
||||
const items = removeArr;
|
||||
const response = await fetch(
|
||||
"https://backend.insta.the1s.de/api/createLoan",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ items, startDate, endDate }),
|
||||
}
|
||||
);
|
||||
const response = await fetch(`${API_BASE}/api/createLoan`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ items, startDate, endDate }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Erstellen der Ausleihe", "error");
|
||||
@@ -106,7 +108,7 @@ export const createLoan = async (startDate: string, endDate: string) => {
|
||||
|
||||
export const onReturn = async (loanID: number) => {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/returnLoan/${loanID}`,
|
||||
`${API_BASE}/api/returnLoan/${loanID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -125,15 +127,12 @@ export const onReturn = async (loanID: number) => {
|
||||
};
|
||||
|
||||
export const onTake = async (loanID: number) => {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/takeLoan/${loanID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await fetch(`${API_BASE}/api/takeLoan/${loanID}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Ausleihen der Ausleihe", "error");
|
||||
@@ -145,17 +144,14 @@ export const onTake = async (loanID: number) => {
|
||||
};
|
||||
|
||||
export const changePW = async (oldPassword: string, newPassword: string) => {
|
||||
const response = await fetch(
|
||||
"https://backend.insta.the1s.de/api/changePassword",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ oldPassword, newPassword }),
|
||||
}
|
||||
);
|
||||
const response = await fetch(`${API_BASE}/api/changePassword`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ oldPassword, newPassword }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Ändern des Passworts", "error");
|
||||
|
Reference in New Issue
Block a user