Compare commits
16 Commits
dev
...
ba0f06e104
Author | SHA1 | Date | |
---|---|---|---|
ba0f06e104 | |||
a932144e94 | |||
36ad60b782 | |||
e4467dba32 | |||
410923af92 | |||
24c405386b | |||
d5296bd3fa | |||
3ee2f6b670 | |||
09af4c760c | |||
3fd0fd9584 | |||
27984ebac8 | |||
3d4aab74d5 | |||
4076630eec | |||
6025212e93 | |||
de554048eb | |||
e1d79d2c79 |
@@ -1,4 +1,4 @@
|
|||||||
# Backend API docs (apiV2)
|
# Backend API docs
|
||||||
|
|
||||||
If you want to cooperate with me, or build something new with my backend API, feel free to reach out!
|
If you want to cooperate with me, or build something new with my backend API, feel free to reach out!
|
||||||
|
|
||||||
@@ -6,51 +6,49 @@ On this page you will learn how my API works.
|
|||||||
|
|
||||||
## General information
|
## 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` which 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` 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`.**
|
||||||
|
|
||||||
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.
|
This is the file that you can use to build an API.
|
||||||
|
|
||||||
---
|
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
|
## Authentication
|
||||||
|
|
||||||
All endpoints require an API key as a path parameter named `:key`.
|
All endpoints require the Admin API key (`ADMIN_ID`) as a URL parameter.
|
||||||
|
|
||||||
Example: `/apiV2/items/:key`
|
Example: `/apiV2/items/{ADMIN_ID}`
|
||||||
|
|
||||||
If the key is missing or invalid, the API responds with `401 Unauthorized`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Endpoints
|
## URL
|
||||||
|
|
||||||
### 1) Get all items
|
- The frontend is currently running on `https://insta.the1s.de`.
|
||||||
|
|
||||||
GET `/apiV2/items/:key`
|
- The backend is currently running on `https://backend.insta.the1s.de`.
|
||||||
|
|
||||||
Returns a list of all items wrapped in a `data` object.
|
You can see the status of this and all my other services at `https://status.the1s.de`.
|
||||||
|
|
||||||
Example request:
|
---
|
||||||
|
|
||||||
|
## Current endpoints
|
||||||
|
|
||||||
|
### 1. Get All Items
|
||||||
|
|
||||||
|
**GET** `/apiV2/items/:key`
|
||||||
|
|
||||||
|
Returns a list of all items and their details.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
```
|
```
|
||||||
GET https://backend.insta.the1s.de/apiV2/items/12345
|
GET https://backend.insta.the1s.de/apiV2/items/your_admin_key
|
||||||
```
|
```
|
||||||
|
|
||||||
Example response:
|
#### Example Response
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -61,66 +59,206 @@ Example response:
|
|||||||
"can_borrow_role": 4,
|
"can_borrow_role": 4,
|
||||||
"inSafe": 1,
|
"inSafe": 1,
|
||||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Fields:
|
Each item has the following properties:
|
||||||
|
|
||||||
- `id`: Unique identifier
|
- `id`: The unique identifier for the item.
|
||||||
- `item_name`: Item name
|
- `item_name`: The name of the item.
|
||||||
- `can_borrow_role`: Role allowed to borrow
|
- `can_borrow_role`: The role ID that is allowed to borrow the item.
|
||||||
- `inSafe`: 1 if in locker, 0 otherwise
|
- `inSafe`: Indicates whether the item is currently in the locker (1) or not (0). This variable/state can change over time.
|
||||||
- `entry_created_at`: Creation timestamp
|
|
||||||
|
|
||||||
Status: 200 on success, 500 on failure.
|
_You also get an http 200 status code._
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2) Change item safe state
|
### 2. Change Item Safe State
|
||||||
|
|
||||||
POST `/apiV2/controlInSafe/:key/:itemId/:state`
|
**POST** `/apiV2/controlInSafe/:key/:itemId/:state`
|
||||||
|
|
||||||
Updates `inSafe` (locker) state of an item.
|
Updates the `inSafe` state of an item (whether it is in the locker).
|
||||||
|
|
||||||
- `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/12345/123/1
|
POST https://backend.insta.the1s.de/apiV2/controlInSafe/your_admin_key/item_id/new_item_state
|
||||||
```
|
```
|
||||||
|
|
||||||
Example response (shape depends on database service):
|
#### Example Response
|
||||||
|
|
||||||
```
|
```
|
||||||
{ "data": { /* update result */ } }
|
{}
|
||||||
```
|
```
|
||||||
|
|
||||||
Status:
|
_An empty object means, that the operation was successful and no further information is returned._
|
||||||
|
|
||||||
- 200 on success
|
_You also get an http 200 status code._
|
||||||
- 400 if `state` is invalid
|
|
||||||
- 500 on failure
|
|
||||||
|
|
||||||
**You can get the item id on the admin panel, from your system administrator.**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3) Get loan by code
|
### 3. Set Return Date
|
||||||
|
|
||||||
GET `/apiV2/getLoanByCode/:key/:loan_code`
|
**POST** `/apiV2/setReturnDate/:key/:loan_code`
|
||||||
|
|
||||||
Retrieves the details of a specific loan.
|
Sets the `returned_date` of a loan to the current server time.
|
||||||
|
|
||||||
Example request:
|
- `loan_code`: The unique code of the loan.
|
||||||
|
|
||||||
|
#### Example Request
|
||||||
|
|
||||||
```
|
```
|
||||||
GET https://backend.insta.the1s.de/apiV2/getLoanByCode/12345/123456
|
POST https://backend.insta.the1s.de/apiV2/setReturnDate/your_admin_key/your_loan_code
|
||||||
```
|
```
|
||||||
|
|
||||||
Example response:
|
#### 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
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
@@ -133,70 +271,36 @@ Example response:
|
|||||||
"take_date": null,
|
"take_date": null,
|
||||||
"returned_date": null,
|
"returned_date": null,
|
||||||
"created_at": "2025-08-20T11:23:40.000Z",
|
"created_at": "2025-08-20T11:23:40.000Z",
|
||||||
"loaned_items_id": [8, 9],
|
"loaned_items_id": [
|
||||||
"loaned_items_name": ["SD Karten", "Kameragimbal"]
|
8,
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"loaned_items_name": [
|
||||||
|
"SD Karten",
|
||||||
|
"Kameragimbal"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Status:
|
_You also get an http 200 status code._
|
||||||
|
|
||||||
- 200 on success
|
If the loan id does not exist, you will receive a 404 status code and an error message.
|
||||||
- 404 if not found
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"message": "Loan not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4) Set return date (now) by loan code
|
## Error Handling
|
||||||
|
|
||||||
POST `/apiV2/setReturnDate/:key/:loan_code`
|
- `403 Forbidden`: Invalid or missing API key.
|
||||||
|
- `400 Bad Request`: Invalid parameters (e.g., wrong state value).
|
||||||
Sets the `returned_date` to the current server time.
|
- `500 Internal Server Error`: Database or server error.
|
||||||
|
|
||||||
Example request:
|
|
||||||
|
|
||||||
```
|
|
||||||
POST https://backend.insta.the1s.de/apiV2/setReturnDate/12345/123456
|
|
||||||
```
|
|
||||||
|
|
||||||
Example response:
|
|
||||||
|
|
||||||
```
|
|
||||||
{ "data": { /* update result */ } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Status: 200 on success, 500 on failure.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5) Set take date (now) by loan code
|
If you have questions or want to collaborate, please reach out to me!
|
||||||
|
|
||||||
POST `/apiV2/setTakeDate/:key/:loan_code`
|
|
||||||
|
|
||||||
Sets the `take_date` to the current server time.
|
|
||||||
|
|
||||||
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!
|
|
||||||
|
72
README.md
72
README.md
@@ -1,73 +1,7 @@
|
|||||||
# Borrow System
|
# Borrow System
|
||||||
|
|
||||||

|
**You have reached the `debian12` branch.**
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
A small full‑stack system to log in, view available items, reserve them for a time window, and manage personal loans.
|
Here you will find the source code of exactly the application that I have hosted.
|
||||||
|
|
||||||
- Frontend: React + TypeScript + Vite + Tailwind CSS
|
The main branch or the branch that I am developing on, is the `dev` branch.
|
||||||
- Backend: Node.js + Express + MySQL + JWT (jose)
|
|
||||||
- Orchestration: Docker Compose (backend + MySQL)
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
|
|
||||||
- Frontend: [frontend/](frontend)
|
|
||||||
- Vite/Tailwind config: [frontend/vite.config.ts](frontend/vite.config.ts), [frontend/tailwind.config.js](frontend/tailwind.config.js)
|
|
||||||
- App entry: [frontend/src/main.tsx](frontend/src/main.tsx), [frontend/src/App.tsx](frontend/src/App.tsx)
|
|
||||||
- UI: [frontend/src/layout/Layout.tsx](frontend/src/layout/Layout.tsx), [frontend/src/components](frontend/src/components)
|
|
||||||
- Data/utilities: [frontend/src/utils/fetchData.ts](frontend/src/utils/fetchData.ts), [frontend/src/utils/userHandler.ts](frontend/src/utils/userHandler.ts), [frontend/src/utils/toastify.ts](frontend/src/utils/toastify.ts)
|
|
||||||
- Backend: [backend/](backend)
|
|
||||||
- Server: [backend/server.js](backend/server.js)
|
|
||||||
- Routes: [backend/routes/api.js](backend/routes/api.js), [backend/routes/apiV2.js](backend/routes/apiV2.js)
|
|
||||||
- DB + services: [backend/services/database.js](backend/services/database.js), [backend/services/tokenService.js](backend/services/tokenService.js)
|
|
||||||
- Schema/seed: [backend/scheme.sql](backend/scheme.sql)
|
|
||||||
- Docs: [docs/](docs)
|
|
||||||
- API docs (see below): [docs/backend_API_docs/README.md](docs/backend_API_docs/README.md)
|
|
||||||
|
|
||||||
## Features (high‑level)
|
|
||||||
|
|
||||||
- Auth via JWT (login -> token cookie) using the backend route in [backend/routes/api.js](backend/routes/api.js).
|
|
||||||
- After login, the app loads items, loans, and user loans and keeps them in localStorage.
|
|
||||||
- Choose a date range to fetch borrowable items, select items, and create a loan.
|
|
||||||
- Manage personal loans list (and delete a loan).
|
|
||||||
|
|
||||||
Key frontend utilities:
|
|
||||||
|
|
||||||
- [`utils.fetchData.fetchAllData`](frontend/src/utils/fetchData.ts): loads items, loans, and user loans after login.
|
|
||||||
- [`utils.fetchData.getBorrowableItems`](frontend/src/utils/fetchData.ts): fetches borrowable items for the selected time range.
|
|
||||||
- [`utils.userHandler.createLoan`](frontend/src/utils/userHandler.ts): creates a new loan for selected items.
|
|
||||||
- [`utils.userHandler.handleDeleteLoan`](frontend/src/utils/userHandler.ts): deletes a loan and syncs local state.
|
|
||||||
- [`utils.toastify.myToast`](frontend/src/utils/toastify.ts): toast notifications.
|
|
||||||
|
|
||||||
UI flow (main screens):
|
|
||||||
|
|
||||||
- Period selection: [frontend/src/components/Form1.tsx](frontend/src/components/Form1.tsx)
|
|
||||||
- Borrowable items + selection: [frontend/src/components/Form2.tsx](frontend/src/components/Form2.tsx)
|
|
||||||
- User loans table: [frontend/src/components/Form4.tsx](frontend/src/components/Form4.tsx)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
- Scripts: see [frontend/package.json](frontend/package.json) and [backend/package.json](backend/package.json)
|
|
||||||
- Frontend: `npm run dev`, `npm run build`, `npm run preview`, `npm run lint`
|
|
||||||
- Backend: `npm start`
|
|
||||||
- Linting: ESLint configured via [frontend/eslint.config.js](frontend/eslint.config.js)
|
|
||||||
- TypeScript configs: [frontend/tsconfig.app.json](frontend/tsconfig.app.json), [frontend/tsconfig.node.json](frontend/tsconfig.node.json)
|
|
||||||
|
|
||||||
## Configuration notes
|
|
||||||
|
|
||||||
- Vite/Tailwind integration via [frontend/vite.config.ts](frontend/vite.config.ts) and `@tailwindcss/vite`; CSS entry uses `@import "tailwindcss"` in [frontend/src/index.css](frontend/src/index.css).
|
|
||||||
- Toasts wired in [frontend/src/main.tsx](frontend/src/main.tsx) with `react-toastify`.
|
|
||||||
- Local state is stored in `localStorage` keys: `allItems`, `allLoans`, `userLoans`, `borrowableItems`. Cross‑component updates are signaled via window events from [`utils.fetchData`](frontend/src/utils/fetchData.ts).
|
|
||||||
|
|
||||||
## API documentation
|
|
||||||
|
|
||||||
Refer to the dedicated API docs:
|
|
||||||
|
|
||||||
`docs/backend_API_docs/README.md`
|
|
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/user-star.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Admin panel</title>
|
<title>Admin panel</title>
|
||||||
</head>
|
</head>
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-star-icon lucide-user-star"><path d="M16.051 12.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"/><path d="M8 15H7a4 4 0 0 0-4 4v2"/><circle cx="10" cy="7" r="4"/></svg>
|
|
Before Width: | Height: | Size: 635 B |
1
admin/public/vite.svg
Normal file
1
admin/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@@ -4,7 +4,9 @@ import Layout from "./Layout/Layout";
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout />
|
<Layout>
|
||||||
|
<p></p>
|
||||||
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,6 @@ import Sidebar from "./Sidebar";
|
|||||||
import UserTable from "../components/UserTable";
|
import UserTable from "../components/UserTable";
|
||||||
import ItemTable from "../components/ItemTable";
|
import ItemTable from "../components/ItemTable";
|
||||||
import LoanTable from "../components/LoanTable";
|
import LoanTable from "../components/LoanTable";
|
||||||
import APIKeyTable from "@/components/APIKeyTable";
|
|
||||||
import { MoveLeft } from "lucide-react";
|
import { MoveLeft } from "lucide-react";
|
||||||
|
|
||||||
type DashboardProps = {
|
type DashboardProps = {
|
||||||
@@ -24,7 +23,6 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
viewGegenstaende={() => setActiveView("Gegenstände")}
|
viewGegenstaende={() => setActiveView("Gegenstände")}
|
||||||
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
||||||
viewUser={() => setActiveView("User")}
|
viewUser={() => setActiveView("User")}
|
||||||
viewAPI={() => setActiveView("API")}
|
|
||||||
/>
|
/>
|
||||||
<Box flex="1" display="flex" flexDirection="column">
|
<Box flex="1" display="flex" flexDirection="column">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -68,7 +66,6 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
{activeView === "User" && <UserTable />}
|
{activeView === "User" && <UserTable />}
|
||||||
{activeView === "Ausleihen" && <LoanTable />}
|
{activeView === "Ausleihen" && <LoanTable />}
|
||||||
{activeView === "Gegenstände" && <ItemTable />}
|
{activeView === "Gegenstände" && <ItemTable />}
|
||||||
{activeView === "API" && <APIKeyTable />}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@@ -3,20 +3,15 @@ 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 Landingpage from "@/components/API/Landingpage";
|
|
||||||
|
|
||||||
const Layout: React.FC = () => {
|
type LayoutProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [showAPI, setShowAPI] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const path = window.location.pathname.replace(/\/+$/, ""); // remove trailing slash
|
|
||||||
if (path === "/api") {
|
|
||||||
setShowAPI(true);
|
|
||||||
console.log("signal");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Cookies.get("token")) {
|
if (Cookies.get("token")) {
|
||||||
const verifyToken = async () => {
|
const verifyToken = async () => {
|
||||||
const response = await fetch("http://localhost:8002/api/verifyToken", {
|
const response = await fetch("http://localhost:8002/api/verifyToken", {
|
||||||
@@ -42,22 +37,17 @@ const Layout: React.FC = () => {
|
|||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showAPI) {
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<Landingpage />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<>
|
||||||
{isLoggedIn ? (
|
<main>
|
||||||
<Dashboard onLogout={() => handleLogout()} />
|
{isLoggedIn ? (
|
||||||
) : (
|
<Dashboard onLogout={() => handleLogout()} />
|
||||||
<Login onSuccess={() => setIsLoggedIn(true)} />
|
) : (
|
||||||
)}
|
<Login onSuccess={() => setIsLoggedIn(true)} />
|
||||||
</main>
|
)}
|
||||||
|
</main>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -23,43 +23,39 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-4">
|
<Card.Root maxW="sm">
|
||||||
<Card.Root maxW="sm">
|
<Card.Header>
|
||||||
<Card.Header>
|
<Card.Title>Login</Card.Title>
|
||||||
<Card.Title>Login</Card.Title>
|
<Card.Description>
|
||||||
<Card.Description>
|
Bitte unten Ihre Admin Zugangsdaten eingeben.
|
||||||
Bitte unten Ihre Admin Zugangsdaten eingeben.
|
</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>username</Field.Label>
|
<Input
|
||||||
<Input
|
value={username}
|
||||||
value={username}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
/>
|
||||||
/>
|
</Field.Root>
|
||||||
</Field.Root>
|
<Field.Root>
|
||||||
<Field.Root>
|
<Field.Label>password</Field.Label>
|
||||||
<Field.Label>password</Field.Label>
|
<Input
|
||||||
<Input
|
type="password"
|
||||||
type="password"
|
value={password}
|
||||||
value={password}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
/>
|
||||||
/>
|
</Field.Root>
|
||||||
</Field.Root>
|
</Stack>
|
||||||
</Stack>
|
</Card.Body>
|
||||||
</Card.Body>
|
<Card.Footer justifyContent="flex-end">
|
||||||
<Card.Footer justifyContent="flex-end">
|
{isError && <MyAlert status="error" title={errorMsg} description={errorDsc} />}
|
||||||
{isError && (
|
<Button onClick={() => handleLogin()} variant="solid">
|
||||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
Login
|
||||||
)}
|
</Button>
|
||||||
<Button onClick={() => handleLogin()} variant="solid">
|
</Card.Footer>
|
||||||
Login
|
</Card.Root>
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
|
||||||
</Card.Root>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -6,14 +6,13 @@ type SidebarProps = {
|
|||||||
viewGegenstaende: () => void;
|
viewGegenstaende: () => void;
|
||||||
viewSchliessfaecher: () => void;
|
viewSchliessfaecher: () => void;
|
||||||
viewUser: () => void;
|
viewUser: () => void;
|
||||||
viewAPI: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
viewAusleihen,
|
viewAusleihen,
|
||||||
viewGegenstaende,
|
viewGegenstaende,
|
||||||
|
viewSchliessfaecher,
|
||||||
viewUser,
|
viewUser,
|
||||||
viewAPI,
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -60,15 +59,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
>
|
>
|
||||||
Gegenstände
|
Gegenstände
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
px={3}
|
|
||||||
py={2}
|
|
||||||
rounded="md"
|
|
||||||
_hover={{ bg: "gray.700", textDecoration: "none" }}
|
|
||||||
onClick={viewAPI}
|
|
||||||
>
|
|
||||||
API Keys
|
|
||||||
</Link>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
||||||
|
@@ -1,233 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
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("http://localhost:8002/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("http://localhost:8002/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}>
|
|
||||||
<Lock 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}>
|
|
||||||
<LockOpen size={16} />
|
|
||||||
<Text>Nicht im Schließfach</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Landingpage;
|
|
@@ -1,203 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
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("http://localhost:8002/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;
|
|
@@ -1,73 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Erstellen
|
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
|
||||||
</Card.Root>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddAPIKey;
|
|
@@ -65,13 +65,6 @@ const AddForm: React.FC<AddFormProps> = ({ onClose, alert }) => {
|
|||||||
"Der Nutzer wurde erfolgreich erstellt."
|
"Der Nutzer wurde erfolgreich erstellt."
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
|
||||||
alert(
|
|
||||||
"error",
|
|
||||||
"Fehler beim Erstellen des Nutzers",
|
|
||||||
"Es gab einen Fehler beim Erstellen des Nutzers. Vielleicht gibt es bereits einen Nutzer mit diesem Benutzernamen."
|
|
||||||
);
|
|
||||||
onClose();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@@ -1,115 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Button, Card, Field, Input, Stack, Alert } from "@chakra-ui/react";
|
|
||||||
import { changePW } from "@/utils/userActions";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
type ChangePWformProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
alert: (
|
|
||||||
status: "success" | "error",
|
|
||||||
message: string,
|
|
||||||
description: string
|
|
||||||
) => void;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChangePWform: React.FC<ChangePWformProps> = ({
|
|
||||||
onClose,
|
|
||||||
alert,
|
|
||||||
username,
|
|
||||||
}) => {
|
|
||||||
const [showSubAlert, setShowSubAlert] = useState(false);
|
|
||||||
const [subAlertMessage, setSubAlertMessage] = useState("");
|
|
||||||
|
|
||||||
const subAlert = (message: string) => {
|
|
||||||
setSubAlertMessage(message);
|
|
||||||
setShowSubAlert(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
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>Passwort ändern</Card.Title>
|
|
||||||
<Card.Description>
|
|
||||||
Füllen Sie das folgende Formular aus, um das Passwort zu ändern.
|
|
||||||
</Card.Description>
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body>
|
|
||||||
<Stack gap="4" w="full">
|
|
||||||
<Field.Root>
|
|
||||||
<Field.Label>Neues Passwort</Field.Label>
|
|
||||||
<Input
|
|
||||||
id="new_password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Neues Passwort"
|
|
||||||
/>
|
|
||||||
</Field.Root>
|
|
||||||
<Field.Root>
|
|
||||||
<Field.Label>Neues Passwort widerholen</Field.Label>
|
|
||||||
<Input
|
|
||||||
id="confirm_new_password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Wiederholen Sie das neue Passwort"
|
|
||||||
/>
|
|
||||||
</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() || "";
|
|
||||||
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Card.Footer>
|
|
||||||
</Card.Root>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChangePWform;
|
|
@@ -9,7 +9,7 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Heading,
|
Heading,
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Tag,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Tooltip } from "@/components/ui/tooltip";
|
import { Tooltip } from "@/components/ui/tooltip";
|
||||||
import MyAlert from "./myChakra/MyAlert";
|
import MyAlert from "./myChakra/MyAlert";
|
||||||
@@ -19,17 +19,11 @@ import {
|
|||||||
CirclePlus,
|
CirclePlus,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Save,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import { deleteItem } from "@/utils/userActions";
|
||||||
deleteItem,
|
|
||||||
handleEditItems,
|
|
||||||
changeSafeState,
|
|
||||||
} from "@/utils/userActions";
|
|
||||||
import AddItemForm from "./AddItemForm";
|
import AddItemForm from "./AddItemForm";
|
||||||
import { formatDateTime } from "@/utils/userFuncs";
|
|
||||||
|
|
||||||
type Items = {
|
type Items = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -49,18 +43,6 @@ const ItemTable: React.FC = () => {
|
|||||||
const [reload, setReload] = useState(false);
|
const [reload, setReload] = useState(false);
|
||||||
const [addForm, setAddForm] = 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 = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
message: string,
|
message: string,
|
||||||
@@ -197,85 +179,61 @@ const ItemTable: React.FC = () => {
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Table.Row key={item.id}>
|
<Table.Row key={item.id}>
|
||||||
<Table.Cell>{item.id}</Table.Cell>
|
<Table.Cell>{item.id}</Table.Cell>
|
||||||
|
<Table.Cell>{item.item_name}</Table.Cell>
|
||||||
|
<Table.Cell>{item.can_borrow_role}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Input
|
{item.inSafe ? (
|
||||||
onChange={(e) =>
|
<Tag.Root
|
||||||
handleItemNameChange(item.id, e.target.value)
|
size="md"
|
||||||
}
|
bg="green.500"
|
||||||
value={item.item_name}
|
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>
|
||||||
|
)}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
<Table.Cell>{item.entry_created_at}</Table.Cell>
|
||||||
<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
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
deleteItem(item.id).then((response) => {
|
deleteItem(item.id).then((response) => {
|
||||||
|
@@ -78,6 +78,10 @@ const LoanTable: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Heading marginBottom={4} size="md">
|
||||||
|
Ausleihen
|
||||||
|
</Heading>
|
||||||
|
|
||||||
{/* Action toolbar */}
|
{/* Action toolbar */}
|
||||||
<HStack
|
<HStack
|
||||||
mb={4}
|
mb={4}
|
||||||
@@ -103,10 +107,6 @@ const LoanTable: React.FC = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
{/* End action toolbar */}
|
{/* End action toolbar */}
|
||||||
|
|
||||||
<Heading marginBottom={4} size="md">
|
|
||||||
Ausleihen
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<MyAlert
|
<MyAlert
|
||||||
status={errorStatus}
|
status={errorStatus}
|
||||||
|
@@ -18,7 +18,6 @@ import { handleDelete, handleEdit } from "@/utils/userActions";
|
|||||||
import MyAlert from "./myChakra/MyAlert";
|
import MyAlert from "./myChakra/MyAlert";
|
||||||
import AddForm from "./AddForm";
|
import AddForm from "./AddForm";
|
||||||
import { formatDateTime } from "@/utils/userFuncs";
|
import { formatDateTime } from "@/utils/userFuncs";
|
||||||
import ChangePWform from "./ChangePWform";
|
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -37,8 +36,6 @@ const UserTable: React.FC = () => {
|
|||||||
const [errorDsc, setErrorDsc] = useState("");
|
const [errorDsc, setErrorDsc] = useState("");
|
||||||
const [reload, setReload] = useState(false);
|
const [reload, setReload] = useState(false);
|
||||||
const [addForm, setAddForm] = useState(false);
|
const [addForm, setAddForm] = useState(false);
|
||||||
const [changePWform, setChangePWform] = useState(false);
|
|
||||||
const [changeUsr, setChangeUsr] = useState("");
|
|
||||||
|
|
||||||
const setError = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
@@ -60,11 +57,6 @@ const UserTable: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordChange = (username: string) => {
|
|
||||||
setChangeUsr(username);
|
|
||||||
setChangePWform(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -147,16 +139,6 @@ const UserTable: React.FC = () => {
|
|||||||
<Heading marginBottom={4} size="md">
|
<Heading marginBottom={4} size="md">
|
||||||
Benutzer
|
Benutzer
|
||||||
</Heading>
|
</Heading>
|
||||||
{changePWform && (
|
|
||||||
<ChangePWform
|
|
||||||
onClose={() => {
|
|
||||||
setChangePWform(false);
|
|
||||||
setReload(!reload);
|
|
||||||
}}
|
|
||||||
alert={setError}
|
|
||||||
username={changeUsr}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<MyAlert
|
<MyAlert
|
||||||
status={errorStatus}
|
status={errorStatus}
|
||||||
@@ -190,7 +172,7 @@ const UserTable: React.FC = () => {
|
|||||||
<strong>Benutzername</strong>
|
<strong>Benutzername</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Passwort ändern</strong>
|
<strong>Passwort</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Rolle</strong>
|
<strong>Rolle</strong>
|
||||||
@@ -216,9 +198,12 @@ const UserTable: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Button onClick={() => handlePasswordChange(user.username)}>
|
<Input
|
||||||
Passwort ändern
|
onChange={(e) =>
|
||||||
</Button>
|
handleInputChange(user.id, "password", e.target.value)
|
||||||
|
}
|
||||||
|
value={user.password}
|
||||||
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Input
|
<Input
|
||||||
@@ -237,6 +222,7 @@ const UserTable: React.FC = () => {
|
|||||||
user.id,
|
user.id,
|
||||||
user.username,
|
user.username,
|
||||||
user.role,
|
user.role,
|
||||||
|
user.password
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setError(
|
setError(
|
||||||
|
@@ -24,18 +24,19 @@ export const handleDelete = async (userId: number) => {
|
|||||||
export const handleEdit = async (
|
export const handleEdit = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
username: string,
|
username: string,
|
||||||
role: string
|
role: string,
|
||||||
|
password: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://localhost:8002/api/editUser/${userId}`,
|
`http://localhost:8002/api/editUser/${userId}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "PUT",
|
||||||
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({ username, role, password }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -72,26 +73,6 @@ export const createUser = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePW = async (newPassword: string, username: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:8002/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");
|
|
||||||
}
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error changing password:", error);
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteLoan = async (loanId: number) => {
|
export const deleteLoan = async (loanId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -156,89 +137,3 @@ export const createItem = async (
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleEditItems = async (
|
|
||||||
itemId: number,
|
|
||||||
item_name: string,
|
|
||||||
can_borrow_role: string
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("http://localhost:8002/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(
|
|
||||||
`http://localhost:8002/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(`http://localhost:8002/api/createAPIentry`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ apiKey, user }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to create API entry");
|
|
||||||
}
|
|
||||||
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(
|
|
||||||
`http://localhost:8002/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 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@@ -1,7 +1,14 @@
|
|||||||
export const formatDateTime = (value: string | null | undefined) => {
|
export const formatDateTime = (value: string | null | undefined) => {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
const inpDate = new Date(value);
|
||||||
if (!m) return "N/A";
|
if (isNaN(inpDate.getTime())) return "N/A";
|
||||||
const [, y, M, d, h, min] = m;
|
return (
|
||||||
return `${d}.${M}.${y} ${h}:${min} Uhr`;
|
inpDate.toLocaleString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}) + " Uhr"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@@ -7,6 +7,6 @@ RUN npm install
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8002
|
EXPOSE 8102
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
@@ -18,13 +18,6 @@ import {
|
|||||||
getAllItems,
|
getAllItems,
|
||||||
deleteItemID,
|
deleteItemID,
|
||||||
createItem,
|
createItem,
|
||||||
changeUserPassword,
|
|
||||||
changeUserPasswordFRONTEND,
|
|
||||||
changeInSafeStateV2,
|
|
||||||
updateItemByID,
|
|
||||||
getAllApiKeys,
|
|
||||||
createAPIentry,
|
|
||||||
deleteAPKey,
|
|
||||||
} from "../services/database.js";
|
} from "../services/database.js";
|
||||||
import { authenticate, generateToken } from "../services/tokenService.js";
|
import { authenticate, generateToken } from "../services/tokenService.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -182,21 +175,6 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/changePassword", authenticate, async (req, res) => {
|
|
||||||
const { oldPassword, newPassword } = req.body || {};
|
|
||||||
const username = req.user.username;
|
|
||||||
const result = await changeUserPasswordFRONTEND(
|
|
||||||
username,
|
|
||||||
oldPassword,
|
|
||||||
newPassword
|
|
||||||
);
|
|
||||||
if (result.success) {
|
|
||||||
res.status(200).json({ message: "Password changed successfully" });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ message: "Failed to change password" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin panel functions
|
// Admin panel functions
|
||||||
|
|
||||||
router.post("/loginAdmin", async (req, res) => {
|
router.post("/loginAdmin", async (req, res) => {
|
||||||
@@ -245,10 +223,10 @@ router.get("/verifyToken", authenticate, async (req, res) => {
|
|||||||
res.status(200).json({ message: "Token is valid" });
|
res.status(200).json({ message: "Token is valid" });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/editUser/:id", authenticate, async (req, res) => {
|
router.put("/editUser/:id", authenticate, async (req, res) => {
|
||||||
const userId = req.params.id;
|
const userId = req.params.id;
|
||||||
const { username, role } = req.body || {};
|
const { username, role, password } = req.body || {};
|
||||||
const result = await handleEdit(userId, username, role);
|
const result = await handleEdit(userId, username, role, password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.status(200).json({ message: "User edited successfully" });
|
return res.status(200).json({ message: "User edited successfully" });
|
||||||
}
|
}
|
||||||
@@ -298,101 +276,4 @@ router.post("/createItem", authenticate, async (req, res) => {
|
|||||||
return res.status(500).json({ message: "Failed to create item" });
|
return res.status(500).json({ message: "Failed to create item" });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/changePWadmin", authenticate, async (req, res) => {
|
|
||||||
const newPassword = req.body.newPassword;
|
|
||||||
if (!newPassword) {
|
|
||||||
return res.status(400).json({ message: "New password is required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await changeUserPassword(req.body.username, newPassword);
|
|
||||||
if (result.success) {
|
|
||||||
return res.status(200).json({ message: "Password changed successfully" });
|
|
||||||
}
|
|
||||||
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;
|
export default router;
|
||||||
|
@@ -6,65 +6,30 @@ import {
|
|||||||
setReturnDateV2,
|
setReturnDateV2,
|
||||||
setTakeDateV2,
|
setTakeDateV2,
|
||||||
getLoanByCodeV2,
|
getLoanByCodeV2,
|
||||||
getAllLoansV2,
|
|
||||||
getAPIkey,
|
|
||||||
} from "../services/database.js";
|
} from "../services/database.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
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
|
// Route for API to get ALL items from the database
|
||||||
router.get("/items/:key", apiKeyGuard, async (req, res) => {
|
router.get("/items/:key", async (req, res) => {
|
||||||
const result = await getItemsFromDatabaseV2();
|
if (req.params.key === process.env.ADMIN_ID) {
|
||||||
if (result.success) {
|
const result = await getItemsFromDatabaseV2();
|
||||||
res.status(200).json({ data: result.data });
|
if (result.success) {
|
||||||
|
res.status(200).json({ data: result.data });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to fetch items" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch items" });
|
res.status(403).json({ message: "Access denied" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route for API to control the position of an item
|
// Route for API to control the position of an item
|
||||||
router.post(
|
router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => {
|
||||||
"/controlInSafe/:key/:itemId/:state",
|
if (req.params.key === process.env.ADMIN_ID) {
|
||||||
apiKeyGuard,
|
|
||||||
async (req, res) => {
|
|
||||||
const itemId = req.params.itemId;
|
const itemId = req.params.itemId;
|
||||||
const state = req.params.state;
|
const state = req.params.state;
|
||||||
|
|
||||||
if (state === "1" || state === "0") {
|
if (state === "1" || state === "0") {
|
||||||
const result = await changeInSafeStateV2(itemId, state);
|
const result = await changeInSafeStateV2(itemId, state);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -75,58 +40,53 @@ router.post(
|
|||||||
} else {
|
} else {
|
||||||
res.status(400).json({ message: "Invalid state value" });
|
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 {
|
} else {
|
||||||
res.status(404).json({ message: "Loan not found" });
|
res.status(403).json({ message: "Access denied" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route for API to set the return date by the loan code
|
router.get("/getLoanByCode/:key/:loan_code", async (req, res) => {
|
||||||
router.post("/setReturnDate/:key/:loan_code", apiKeyGuard, async (req, res) => {
|
if (req.params.key === process.env.ADMIN_ID) {
|
||||||
const loanCode = req.params.loan_code;
|
const loan_code = req.params.loan_code;
|
||||||
const result = await setReturnDateV2(loanCode);
|
|
||||||
if (result.success) {
|
const result = await getLoanByCodeV2(loan_code);
|
||||||
res.status(200).json({ data: result.data });
|
if (result.success) {
|
||||||
} else {
|
res.status(200).json({ data: result.data });
|
||||||
res.status(500).json({ message: "Failed to set return date" });
|
} else {
|
||||||
|
res.status(404).json({ message: "Loan not found" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route for API to set the take away date by the loan code
|
// Route for API to set the return date
|
||||||
router.post("/setTakeDate/:key/:loan_code", apiKeyGuard, async (req, res) => {
|
router.post("/setReturnDate/:key/:loan_code", async (req, res) => {
|
||||||
const loanCode = req.params.loan_code;
|
if (req.params.key === process.env.ADMIN_ID) {
|
||||||
const result = await setTakeDateV2(loanCode);
|
const loanCode = req.params.loan_code;
|
||||||
if (result.success) {
|
|
||||||
res.status(200).json({ data: result.data });
|
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" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set take date" });
|
res.status(403).json({ message: "Access denied" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route for API to get ALL loans from the database without sensitive info (only for landingpage)
|
// Route for API to set the take away date
|
||||||
router.get("/allLoans", async (req, res) => {
|
router.post("/setTakeDate/:key/:loan_code", async (req, res) => {
|
||||||
const result = await getAllLoansV2();
|
if (req.params.key === process.env.ADMIN_ID) {
|
||||||
if (result.success) {
|
const loanCode = req.params.loan_code;
|
||||||
return res.status(200).json(result.data);
|
|
||||||
}
|
|
||||||
return res.status(500).json({ message: "Failed to fetch loans" });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route for API to get ALL items from the database (only for landingpage)
|
const result = await setTakeDateV2(loanCode);
|
||||||
router.get("/allItems", async (req, res) => {
|
if (result.success) {
|
||||||
const result = await getItemsFromDatabaseV2();
|
res.status(200).json({ data: result.data });
|
||||||
if (result.success) {
|
} else {
|
||||||
res.status(200).json(result.data);
|
res.status(500).json({ message: "Failed to set take date" });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch items" });
|
res.status(403).json({ message: "Access denied" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -58,14 +58,6 @@ CREATE TABLE `lockers` (
|
|||||||
UNIQUE KEY `locker_number` (`locker_number`)
|
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
|
INSERT INTO `items` (`item_name`, `can_borrow_role`, `inSafe`) VALUES
|
||||||
('DJI 1er Mikro', 4, 1),
|
('DJI 1er Mikro', 4, 1),
|
||||||
('DJI 2er Mikro 1', 4, 1),
|
('DJI 2er Mikro 1', 4, 1),
|
||||||
|
@@ -5,7 +5,7 @@ import apiRouter from "./routes/api.js";
|
|||||||
import apiRouterV2 from "./routes/apiV2.js";
|
import apiRouterV2 from "./routes/apiV2.js";
|
||||||
env.config();
|
env.config();
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 8002;
|
const port = 8102;
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
// Increase body size limits to support large CSV JSON payloads
|
// Increase body size limits to support large CSV JSON payloads
|
||||||
|
@@ -8,6 +8,7 @@ const pool = mysql
|
|||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
})
|
})
|
||||||
.promise();
|
.promise();
|
||||||
|
|
||||||
@@ -39,10 +40,10 @@ export const getLoanByCodeV2 = async (loan_code) => {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeInSafeStateV2 = async (itemId) => {
|
export const changeInSafeStateV2 = async (itemId, state) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE items SET inSafe = NOT inSafe WHERE id = ?",
|
"UPDATE items SET inSafe = ? WHERE id = ?",
|
||||||
[itemId]
|
[state, itemId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -87,8 +88,11 @@ export const getItemsFromDatabase = async (role) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getLoansFromDatabase = async () => {
|
export const getLoansFromDatabase = async () => {
|
||||||
const [rows] = await pool.query("SELECT * FROM loans;");
|
const [result] = await pool.query("SELECT * FROM loans;");
|
||||||
return { success: true, data: rows.length > 0 ? rows : null };
|
if (result.length > 0) {
|
||||||
|
return { success: true, data: result };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserLoansFromDatabase = async (username) => {
|
export const getUserLoansFromDatabase = async (username) => {
|
||||||
@@ -229,7 +233,7 @@ export const createLoanInDatabase = async (
|
|||||||
// Generate unique loan_code (retry a few times)
|
// Generate unique loan_code (retry a few times)
|
||||||
let loanCode = null;
|
let loanCode = null;
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
const candidate = Math.floor(1000 + Math.random() * 900000); // 4-6 digits
|
||||||
const [exists] = await conn.query(
|
const [exists] = await conn.query(
|
||||||
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
||||||
[candidate]
|
[candidate]
|
||||||
@@ -294,56 +298,24 @@ export const createLoanInDatabase = async (
|
|||||||
// These functions are only temporary, and will be deleted when the full bin is set up.
|
// These functions are only temporary, and will be deleted when the full bin is set up.
|
||||||
|
|
||||||
export const onTake = async (loanId) => {
|
export const onTake = async (loanId) => {
|
||||||
const [items] = await pool.query(
|
|
||||||
"SELECT loaned_items_id FROM loans WHERE id = ?",
|
|
||||||
[loanId]
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET take_date = NOW() WHERE id = ?",
|
"UPDATE loans SET take_date = NOW() WHERE id = ?",
|
||||||
[loanId]
|
[loanId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onReturn = async (loanId) => {
|
export const onReturn = async (loanId) => {
|
||||||
const [items] = await pool.query(
|
|
||||||
"SELECT loaned_items_id FROM loans WHERE id = ?",
|
|
||||||
[loanId]
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET returned_date = NOW() WHERE id = ?",
|
"UPDATE loans SET returned_date = NOW() WHERE id = ?",
|
||||||
[loanId]
|
[loanId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -359,9 +331,7 @@ export const loginAdmin = async (username, password) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getAllUsers = async () => {
|
export const getAllUsers = async () => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query("SELECT * FROM users");
|
||||||
"SELECT id, username, role, entry_created_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 };
|
||||||
};
|
};
|
||||||
@@ -372,10 +342,10 @@ export const deleteUserID = async (userId) => {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleEdit = async (userId, username, role) => {
|
export const handleEdit = async (userId, username, role, password) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET username = ?, role = ? WHERE id = ?",
|
"UPDATE users SET username = ?, role = ?, password = ? WHERE id = ?",
|
||||||
[username, role, userId]
|
[username, role, password, userId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -416,77 +386,3 @@ export const createItem = async (item_name, can_borrow_role) => {
|
|||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeUserPassword = async (username, newPassword) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE users SET password = ? WHERE username = ?",
|
|
||||||
[newPassword, username]
|
|
||||||
);
|
|
||||||
if (result.affectedRows > 0) return { success: true };
|
|
||||||
return { success: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changeUserPasswordFRONTEND = async (
|
|
||||||
username,
|
|
||||||
oldPassword,
|
|
||||||
newPassword
|
|
||||||
) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE users SET password = ? WHERE username = ? AND password = ?",
|
|
||||||
[newPassword, username, oldPassword]
|
|
||||||
);
|
|
||||||
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 };
|
|
||||||
};
|
|
||||||
|
@@ -1,15 +1,18 @@
|
|||||||
services:
|
services:
|
||||||
# borrow_system-frontend:
|
borrow_system-frontend:
|
||||||
# container_name: borrow_system-frontend
|
container_name: borrow_system-frontend
|
||||||
# build: ./frontend
|
build: ./frontend
|
||||||
# ports:
|
ports:
|
||||||
# - "8001:8001"
|
- "8101:8101"
|
||||||
# environment:
|
networks:
|
||||||
# - CHOKIDAR_USEPOLLING=true
|
- proxynet
|
||||||
# volumes:
|
- borrow_system-internal
|
||||||
# - ./frontend:/app
|
environment:
|
||||||
# - /app/node_modules
|
- CHOKIDAR_USEPOLLING=true
|
||||||
# restart: unless-stopped
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
# admin-frontend:
|
# admin-frontend:
|
||||||
# container_name: admin-frontend
|
# container_name: admin-frontend
|
||||||
@@ -27,9 +30,13 @@ services:
|
|||||||
container_name: borrow_system-backend
|
container_name: borrow_system-backend
|
||||||
build: ./backend
|
build: ./backend
|
||||||
ports:
|
ports:
|
||||||
- "8002:8002"
|
- "8102:8102"
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
- borrow_system-internal
|
||||||
environment:
|
environment:
|
||||||
DB_HOST: mysql
|
DB_HOST: mysql
|
||||||
|
DB_PORT: 3306
|
||||||
DB_USER: root
|
DB_USER: root
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
DB_NAME: borrow_system
|
DB_NAME: borrow_system
|
||||||
@@ -46,12 +53,18 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||||
MYSQL_DATABASE: borrow_system
|
MYSQL_DATABASE: borrow_system
|
||||||
TZ: Europe/Berlin
|
|
||||||
volumes:
|
volumes:
|
||||||
- mysql-data:/var/lib/mysql
|
- mysql-data:/var/lib/mysql
|
||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
|
||||||
ports:
|
ports:
|
||||||
- "3309:3306"
|
- "3309:3306"
|
||||||
|
networks:
|
||||||
|
- borrow_system-internal
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
borrow_system-internal:
|
||||||
|
external: false
|
||||||
|
@@ -7,6 +7,6 @@ RUN npm install
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8001
|
EXPOSE 8101
|
||||||
|
|
||||||
CMD ["npm", "run", "dev"]
|
CMD ["npm", "run", "dev"]
|
@@ -21,14 +21,13 @@ type Loan = {
|
|||||||
|
|
||||||
const formatDate = (iso: string | null) => {
|
const formatDate = (iso: string | null) => {
|
||||||
if (!iso) return "-";
|
if (!iso) return "-";
|
||||||
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
const d = new Date(iso);
|
||||||
if (!m) return iso;
|
if (Number.isNaN(d.getTime())) return iso;
|
||||||
const [, y, M, d, h, min] = m;
|
return d.toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" });
|
||||||
return `${d}.${M}.${y} ${h}:${min}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchUserLoans(): Promise<Loan[]> {
|
async function fetchUserLoans(): Promise<Loan[]> {
|
||||||
const res = await fetch("http://localhost:8002/api/userLoans", {
|
const res = await fetch("https://backend.insta.the1s.de/api/userLoans", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { Authorization: `Bearer ${Cookies.get("token") || ""}` },
|
headers: { Authorization: `Bearer ${Cookies.get("token") || ""}` },
|
||||||
});
|
});
|
||||||
|
@@ -1,33 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { changePW } from "../utils/userHandler";
|
|
||||||
import { myToast } from "../utils/toastify";
|
|
||||||
|
|
||||||
type HeaderProps = {
|
type HeaderProps = {
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({ onLogout }) => {
|
const Header: React.FC<HeaderProps> = ({ onLogout }) => {
|
||||||
const passwordForm = () => {
|
|
||||||
const oldPW = window.prompt("Altes Passwort");
|
|
||||||
const newPW = window.prompt("Neues Passwort");
|
|
||||||
const repeatNewPW = window.prompt("Neues Passwort wiederholen");
|
|
||||||
if (oldPW && newPW && repeatNewPW) {
|
|
||||||
if (newPW === repeatNewPW) {
|
|
||||||
changePW(oldPW, newPW);
|
|
||||||
} else {
|
|
||||||
myToast("Die neuen Passwörter stimmen nicht überein.", "error");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
myToast("Bitte alle Felder ausfüllen.", "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const btn =
|
|
||||||
"inline-flex items-center h-9 px-3 rounded-md text-sm font-medium border border-slate-300 bg-white text-slate-700 hover:bg-slate-100 active:bg-slate-200 transition focus:outline-none focus:ring-2 focus:ring-slate-400/50";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="mb-4 sm:mb-6">
|
<header className="mb-4 sm:mb-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">
|
<h1 className="text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">
|
||||||
Gegenstand ausleihen
|
Gegenstand ausleihen
|
||||||
@@ -36,38 +16,23 @@ const Header: React.FC<HeaderProps> = ({ onLogout }) => {
|
|||||||
Schnell und unkompliziert Equipment reservieren
|
Schnell und unkompliziert Equipment reservieren
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
<nav
|
type="button"
|
||||||
aria-label="Aktionen"
|
onClick={onLogout}
|
||||||
className="flex flex-wrap items-center gap-2"
|
className="h-9 px-3 rounded-md border border-slate-300 text-slate-700 hover:bg-slate-100 transition"
|
||||||
>
|
>
|
||||||
<a
|
Logout
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/src/branch/dev/Docs/HELP.md"
|
</button>
|
||||||
target="_blank"
|
<a href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/src/branch/dev/Docs/HELP.md">
|
||||||
rel="noreferrer"
|
<button className="h-9 px-3 rounded-md border border-slate-300 text-slate-700 hover:bg-slate-100 transition">
|
||||||
className={btn}
|
|
||||||
>
|
|
||||||
Hilfe
|
Hilfe
|
||||||
</a>
|
</button>
|
||||||
<a
|
</a>
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
|
<a href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system">
|
||||||
target="_blank"
|
<button className="h-9 px-3 rounded-md border border-slate-300 text-slate-700 hover:bg-slate-100 transition">
|
||||||
rel="noreferrer"
|
|
||||||
className={btn}
|
|
||||||
>
|
|
||||||
Source Code
|
Source Code
|
||||||
</a>
|
|
||||||
<button type="button" onClick={passwordForm} className={btn}>
|
|
||||||
Passwort ändern
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
</a>
|
||||||
type="button"
|
|
||||||
onClick={onLogout}
|
|
||||||
className={`${btn} border-rose-300 hover:bg-rose-50`}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@@ -25,7 +25,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
// First we fetch all items that are potentially available for borrowing
|
// First we fetch all items that are potentially available for borrowing
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8002/api/items", {
|
const response = await fetch("https://backend.insta.the1s.de/api/items", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -57,7 +57,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
|||||||
|
|
||||||
// get all loans
|
// get all loans
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8002/api/loans", {
|
const response = await fetch("https://backend.insta.the1s.de/api/loans", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -89,7 +89,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
|||||||
|
|
||||||
// get user loans
|
// get user loans
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8002/api/userLoans", {
|
const response = await fetch("https://backend.insta.the1s.de/api/userLoans", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -122,7 +122,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
|||||||
|
|
||||||
export const loginUser = async (username: string, password: string) => {
|
export const loginUser = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8002/api/login", {
|
const response = await fetch("https://backend.insta.the1s.de/api/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -158,7 +158,7 @@ export const getBorrowableItems = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8002/api/borrowableItems", {
|
const response = await fetch("https://backend.insta.the1s.de/api/borrowableItems", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||||
|
@@ -5,7 +5,7 @@ import { queryClient } from "./queryClient";
|
|||||||
export const handleDeleteLoan = async (loanID: number): Promise<boolean> => {
|
export const handleDeleteLoan = async (loanID: number): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://localhost:8002/api/deleteLoan/${loanID}`,
|
`https://backend.insta.the1s.de/api/deleteLoan/${loanID}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -75,7 +75,7 @@ export const rmFromRemove = (itemID: number) => {
|
|||||||
|
|
||||||
export const createLoan = async (startDate: string, endDate: string) => {
|
export const createLoan = async (startDate: string, endDate: string) => {
|
||||||
const items = removeArr;
|
const items = removeArr;
|
||||||
const response = await fetch("http://localhost:8002/api/createLoan", {
|
const response = await fetch("https://backend.insta.the1s.de/api/createLoan", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -137,22 +137,3 @@ export const onTake = async (loanID: number) => {
|
|||||||
myToast("Ausleihe erfolgreich ausgeliehen!", "success");
|
myToast("Ausleihe erfolgreich ausgeliehen!", "success");
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePW = async (oldPassword: string, newPassword: string) => {
|
|
||||||
const response = await fetch("http://localhost:8002/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");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
myToast("Passwort erfolgreich geändert!", "success");
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import svgr from "vite-plugin-svgr";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), svgr(), tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 8001,
|
allowedHosts: ["insta.the1s.de"],
|
||||||
watch: {
|
port: 8101,
|
||||||
usePolling: true,
|
watch: { usePolling: true },
|
||||||
|
hmr: {
|
||||||
|
host: "insta.the1s.de",
|
||||||
|
port: 8101,
|
||||||
|
protocol: "wss",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user