Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a3c948386 | |||
| 6fb03530df | |||
| d2c36e71be | |||
| 40d784ab36 | |||
| 60c85efd37 | |||
| 747932cf03 | |||
| 0964109c4b | |||
| 5442f2f1f3 | |||
| 56e073244f | |||
| d2dc74971f | |||
| 25709ea0d9 | |||
| f8ab2490fe | |||
| 3de877dd2b | |||
| 5d0134017a | |||
| 07503ec079 | |||
| 4155982aac | |||
| 59e05e5c4c | |||
| 2facdab011 | |||
| 04db0bd7e0 | |||
| 40dd34bcac | |||
| 939d3e89c5 | |||
| a355c42964 | |||
| c8a230979f | |||
| 581cd4a1fd | |||
| c53e6e095a | |||
| eaa325668c |
@@ -117,3 +117,8 @@ ToDo.txt
|
|||||||
|
|
||||||
# only in development branch
|
# only in development branch
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# psd files from footage
|
||||||
|
footage/*.psd
|
||||||
|
|
||||||
|
icon/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "no-as-a-service"]
|
||||||
|
path = no-as-a-service
|
||||||
|
url = https://github.com/hotheadhacker/no-as-a-service.git
|
||||||
@@ -1,366 +1,177 @@
|
|||||||
# Borrow System API Documentation
|
# Borrow System API Documentation
|
||||||
|
|
||||||
**Frontend:** https://insta.the1s.de
|
## Overview
|
||||||
**Backend base URL:** `https://insta.the1s.de/backend/api`
|
|
||||||
|
|
||||||
---
|
The Borrow System API provides endpoints for managing items, loans, and door access for a borrowing/locker system. All endpoints require authentication via an 8-digit API key passed as a URL parameter.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
All API endpoints require **either**:
|
All requests must include a valid API key in the URL path as the `:key` parameter. API keys are 8-digit numeric strings.
|
||||||
|
|
||||||
### 1. Bearer Token (JWT)
|
|
||||||
|
|
||||||
Send an `Authorization` header:
|
|
||||||
|
|
||||||
```http
|
|
||||||
Authorization: Bearer <JWT_TOKEN>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Used for user-based access.
|
|
||||||
- Token must be valid and not expired.
|
|
||||||
|
|
||||||
### 2. API Key (for devices / machine-to-machine)
|
|
||||||
|
|
||||||
Include an API key in the route as `:key` parameter:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/api/.../:key/...
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/items/12345678
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `12345678` is your API key.
|
|
||||||
The API key is validated server-side.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Response Codes
|
|
||||||
|
|
||||||
- `200 OK` – Request was successful.
|
|
||||||
- `401 Unauthorized` – Missing or malformed credentials.
|
|
||||||
- `403 Forbidden` – Credentials invalid or not allowed to access this resource.
|
|
||||||
- `404 Not Found` – Resource (e.g., loan) not found.
|
|
||||||
- `500 Internal Server Error` – Unexpected server error.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
### 1. Get All Items
|
The Base URL for all endpoints is: `https://insta.the1s.de/backend/api`
|
||||||
|
|
||||||
**GET** `/api/items/:key`
|
### Get All Items
|
||||||
|
|
||||||
Returns a list of all items.
|
`GET /items/:key`
|
||||||
|
|
||||||
#### Path Parameters
|
Returns all items in the system.
|
||||||
|
|
||||||
- `:key` – API key (8-digit number)
|
**Response 200:**
|
||||||
|
|
||||||
#### Authentication
|
|
||||||
|
|
||||||
- Either:
|
|
||||||
- Valid `Authorization: Bearer <token>`
|
|
||||||
- Or valid `:key` path parameter
|
|
||||||
|
|
||||||
#### Request Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/items/12345678 HTTP/1.1
|
|
||||||
Host: backend.insta.the1s.de
|
|
||||||
Authorization: Bearer <JWT_TOKEN>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful Response (200)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"item_name": "DJI 1er Mikro",
|
"item_name": "Laptop",
|
||||||
"can_borrow_role": 4,
|
"can_borrow_role": 1,
|
||||||
"inSafe": 1,
|
"in_safe": true,
|
||||||
"safe_nr": 3,
|
"safe_nr": 3,
|
||||||
"door_key": "123",
|
"door_key": 101,
|
||||||
"entry_created_at": "2025-08-19T22:02:16.000Z",
|
"last_borrowed_person": "jdoe",
|
||||||
"entry_updated_at": "2025-08-19T22:02:16.000Z",
|
|
||||||
"last_borrowed_person": "alice",
|
|
||||||
"currently_borrowing": null
|
"currently_borrowing": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error Response (500)
|
**Response 500:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "message": "Failed to fetch items" }
|
||||||
"message": "Failed to fetch items"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Toggle Item Safe State
|
### Change Item Safe State
|
||||||
|
|
||||||
Toggles `in_safe` between `0` and `1` for a given item.
|
`POST /change-state/:key/:itemId`
|
||||||
|
|
||||||
**Keep in mind that when you return a loan by code, the item states are automatically updated.**
|
Toggles the `in_safe` boolean state of an item.
|
||||||
|
|
||||||
**POST** `/api/change-state/:key/:itemId`
|
**URL Parameters:**
|
||||||
|
|
||||||
#### Path Parameters
|
- **key** - API key
|
||||||
|
- **itemId** - The item's ID
|
||||||
|
|
||||||
- `:key` – API key (8-digit number)
|
**Response 200:** Returns on successful toggle.
|
||||||
- `:itemId` – Item ID (integer)
|
|
||||||
|
|
||||||
#### Authentication
|
**Response 500:**
|
||||||
|
|
||||||
- Either Bearer token or `:key` API key.
|
|
||||||
|
|
||||||
#### Request Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/change-state/12345678/42 HTTP/1.1
|
|
||||||
Host: backend.insta.the1s.de
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful Response (200)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "message": "Failed to update item state" }
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
_(Implementation currently only returns `{ success: true }`, so `data` may be empty.)_
|
|
||||||
|
|
||||||
#### Error Response (500)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Failed to update item state"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Get Loan by Code
|
### Get Loan by Code
|
||||||
|
|
||||||
Fetch loan information by `loan_code`.
|
`GET /get-loan-by-code/:key/:loan_code`
|
||||||
|
|
||||||
**GET** `/api/get-loan-by-code/:key/:loan_code`
|
Retrieves loan details by its 6-digit loan code.
|
||||||
|
|
||||||
#### Path Parameters
|
**URL Parameters:**
|
||||||
|
|
||||||
- `:key` – API key (8-digit number)
|
- **key** - API key
|
||||||
- `:loan_code` – Loan code (string)
|
- **loan_code** - A 6-digit numeric loan code
|
||||||
|
|
||||||
#### Authentication
|
**Response 200:**
|
||||||
|
|
||||||
- Either Bearer token or `:key` API key.
|
|
||||||
|
|
||||||
#### Request Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/get-loan-by-code/12345678/12345 HTTP/1.1
|
|
||||||
Host: backend.insta.the1s.de
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful Response (200)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"username": "john",
|
"username": "jdoe",
|
||||||
"returned_date": null,
|
"returned_date": null,
|
||||||
"take_date": "2025-01-01T10:00:00.000Z",
|
"take_date": "2024-01-15T10:30:00.000Z",
|
||||||
"lockers": "[1, 2, 3]"
|
"lockers": [1, 3]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error Response (404)
|
**Response 404:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "message": "Loan not found" }
|
||||||
"message": "Loan not found"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Set Loan Return Date
|
### Set Take Date
|
||||||
|
|
||||||
Sets `returned_date = NOW()` on a loan and updates related items:
|
`POST /set-take-date/:key/:loan_code`
|
||||||
|
|
||||||
- `in_safe = 1`
|
Records when items are physically taken by setting `take_date` to the current timestamp. Updates associated items to `in_safe = false` and sets `currently_borrowing` to the loan's username.
|
||||||
- `currently_borrowing = NULL`
|
|
||||||
- `last_borrowed_person = username`
|
|
||||||
|
|
||||||
**POST** `/api/set-return-date/:key/:loan_code`
|
**URL Parameters:**
|
||||||
|
|
||||||
#### Path Parameters
|
- **key** - API key
|
||||||
|
- **loan_code** - A 6-digit numeric loan code
|
||||||
|
|
||||||
- `:key` – API key (8-digit number)
|
**Response 200:** Empty JSON object on success.
|
||||||
- `:loan_code` – Loan code (string)
|
|
||||||
|
|
||||||
#### Authentication
|
**Response 500:**
|
||||||
|
|
||||||
- Either Bearer token or `:key` API key.
|
|
||||||
|
|
||||||
#### Request Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/set-return-date/12345678/12345 HTTP/1.1
|
|
||||||
Host: backend.insta.the1s.de
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful Response (200)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "message": "Loan not found or already taken" }
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error Response (500)
|
> **Note:** This endpoint will fail if the loan has already been taken or does not exist.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Failed to set return date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Set Loan Take Date
|
### Set Return Date
|
||||||
|
|
||||||
Sets `take_date = NOW()` on a loan and updates related items:
|
`POST /set-return-date/:key/:loan_code`
|
||||||
|
|
||||||
- `in_safe = 0`
|
Marks a loan as returned by setting `returned_date` to the current timestamp. Also updates all associated items to `in_safe = true`, clears `currently_borrowing`, and sets `last_borrowed_person`. Therefore, keep in mind that you must not call other endpoints that will change the safe state of an item after or before calling this endpoint, otherwise the state of the items will be inconsistent.
|
||||||
- `currently_borrowing = username`
|
|
||||||
|
|
||||||
**POST** `/api/set-take-date/:key/:loan_code`
|
**URL Parameters:**
|
||||||
|
|
||||||
#### Path Parameters
|
- **key** - API key
|
||||||
|
- **loan_code** - A 6-digit numeric loan code
|
||||||
|
|
||||||
- `:key` – API key (8-digit number)
|
**Response 200:** Empty JSON object on success.
|
||||||
- `:loan_code` – Loan code (string)
|
|
||||||
|
|
||||||
#### Authentication
|
**Response 500:**
|
||||||
|
|
||||||
- Either Bearer token or `:key` API key.
|
|
||||||
|
|
||||||
#### Request Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/set-take-date/12345678/LOAN-12345 HTTP/1.1
|
|
||||||
Host: backend.insta.the1s.de
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful Response (200)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "message": "Failed to set return date" }
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error Response (500)
|
> **Note:** This endpoint will fail if the loan has already been returned (i.e., `returned_date` is not `NULL`).
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Failed to set take date"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Open Door by Door Key
|
### Open Door
|
||||||
|
|
||||||
Looks up an item by its `door_key`, toggles `in_safe`, and returns safe information.
|
`GET /open-door/:key/:doorKey`
|
||||||
|
|
||||||
**GET** `/api/open-door/:key/:doorKey`
|
Toggles the safe state of an item identified by its door key and returns the associated safe number.
|
||||||
|
|
||||||
#### Path Parameters
|
**URL Parameters:**
|
||||||
|
|
||||||
- `:key` – API key (8-digit number)
|
- **key** - API key
|
||||||
- `:doorKey` – Door key/token (string) used by hardware to identify the locker.
|
- **doorKey** - The door key identifier assigned to an item
|
||||||
|
|
||||||
#### Authentication
|
**Response 200:**
|
||||||
|
|
||||||
- Either Bearer token or `:key` API key.
|
|
||||||
|
|
||||||
#### Request Example
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/open-door/12345678/123 HTTP/1.1
|
|
||||||
Host: backend.insta.the1s.de
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful Response (200)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"safe_nr": 5,
|
"safe_nr": 3,
|
||||||
"id": 42
|
"id": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error Response (500)
|
**Response 500:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "message": "Failed to open door" }
|
||||||
"message": "Failed to open door"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Error Handling
|
||||||
|
|
||||||
## Authentication Error Messages
|
All endpoints return a `500` status code for server-side failures and a JSON body with a `message` field, except for **Get Loan by Code** which returns `404` when no matching loan is found.
|
||||||
|
|
||||||
### Missing credentials
|
|
||||||
|
|
||||||
Status: `401`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Unauthorized"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invalid JWT
|
|
||||||
|
|
||||||
Status: `403`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Present token invalid"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invalid API Key
|
|
||||||
|
|
||||||
Status: `403`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "API Key invalid"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All responses are JSON.
|
|
||||||
- Time fields like `take_date` and `returned_date` are in the format returned by MySQL (usually ISO-like strings).
|
|
||||||
- `loaned_items_id` in the database is stored as a JSON array string (e.g. `"[1,2,3]"`) and is parsed internally; clients do not interact with this field directly via current endpoints.
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 416 KiB |
@@ -2,7 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/icon_borrow-system-frontend_dark.png"
|
||||||
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Ausleihsystem</title>
|
<title>Ausleihsystem</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /backend/ {
|
location /backend/ {
|
||||||
proxy_pass http://demo_borrow_system-backend_v2:8102/;
|
proxy_pass http://borrow_system-backend_v2:8004/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "v2.1.2 (dev)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^3.28.0",
|
"@chakra-ui/react": "^3.28.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@lottiefiles/dotlottie-react": "^0.19.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"i18next": "^25.6.0",
|
"i18next": "^25.6.0",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -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-shapes-icon lucide-shapes"><path d="M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"/><rect x="3" y="14" width="7" height="7" rx="1"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 420 B |
@@ -16,6 +16,7 @@ import { Flex } from "@chakra-ui/react";
|
|||||||
import { Footer } from "./components/footer/Footer";
|
import { Footer } from "./components/footer/Footer";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import { ContactPage } from "./pages/ContactPage";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ function App() {
|
|||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/my-loans" element={<MyLoansPage />} />
|
<Route path="/my-loans" element={<MyLoansPage />} />
|
||||||
<Route path="/landingpage" element={<Landingpage />} />
|
<Route path="/landingpage" element={<Landingpage />} />
|
||||||
|
<Route path="/contact" element={<ContactPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Alert, Stack, VStack, Spinner, Text, Heading } from "@chakra-ui/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const DeactivatedServices = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [deactivatedServices, setDeactivatedServices] = useState<
|
||||||
|
{ function_name: string }[]
|
||||||
|
>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDeactivatedServices = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/api/users/deactivated-services`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setDeactivatedServices(data);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch deactivated services");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching deactivated services:", error);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDeactivatedServices();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{deactivatedServices.length >= 1 && (
|
||||||
|
<Stack gap="2">
|
||||||
|
<Heading size={"xl"}>{t("deactivated-services")}</Heading>
|
||||||
|
{isLoading && (
|
||||||
|
<VStack colorPalette="teal">
|
||||||
|
<Spinner color="colorPalette.600" />
|
||||||
|
<Text color="colorPalette.600">{t("loading")}</Text>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
{deactivatedServices.length >= 1 &&
|
||||||
|
deactivatedServices.map((item) => (
|
||||||
|
<Alert.Root key={item.function_name} status="warning">
|
||||||
|
<Alert.Indicator />
|
||||||
|
<Alert.Title>
|
||||||
|
{item.function_name} {t("is-deactivated")}
|
||||||
|
</Alert.Title>
|
||||||
|
</Alert.Root>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
|
Image,
|
||||||
Heading,
|
Heading,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
MoreVertical,
|
MoreVertical,
|
||||||
Languages,
|
Languages,
|
||||||
Table,
|
Table,
|
||||||
|
ContactRound,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useUserContext } from "@/states/Context";
|
import { useUserContext } from "@/states/Context";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -67,6 +69,7 @@ export const Header = () => {
|
|||||||
className="mb-6"
|
className="mb-6"
|
||||||
position="relative"
|
position="relative"
|
||||||
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
|
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
|
||||||
|
marginBottom={1}
|
||||||
>
|
>
|
||||||
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
|
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
|
||||||
<Box
|
<Box
|
||||||
@@ -141,7 +144,7 @@ export const Header = () => {
|
|||||||
value="help"
|
value="help"
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
window.open(
|
window.open(
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
|
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages",
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener,noreferrer",
|
"noopener,noreferrer",
|
||||||
)
|
)
|
||||||
@@ -153,6 +156,16 @@ export const Header = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
value="contact"
|
||||||
|
onSelect={() => navigate("/contact", { replace: true })}
|
||||||
|
children={
|
||||||
|
<HStack gap={3}>
|
||||||
|
<ContactRound size={16} />
|
||||||
|
<Text as="span">{t("contact")}</Text>
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Menu.Separator />
|
<Menu.Separator />
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
value="logout"
|
value="logout"
|
||||||
@@ -179,6 +192,13 @@ export const Header = () => {
|
|||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
|
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
|
||||||
<Flex align="center" justify="space-between" gap={2}>
|
<Flex align="center" justify="space-between" gap={2}>
|
||||||
|
<Image
|
||||||
|
src="/icon_borrow-system-frontend_dark.png"
|
||||||
|
alt="borrow-system logo"
|
||||||
|
boxSize="10"
|
||||||
|
objectFit="contain"
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
<Heading
|
<Heading
|
||||||
size="2xl"
|
size="2xl"
|
||||||
className="tracking-tight text-slate-900 dark:text-slate-100"
|
className="tracking-tight text-slate-900 dark:text-slate-100"
|
||||||
@@ -268,7 +288,7 @@ export const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
|
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
@@ -278,6 +298,17 @@ export const Header = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
onClick={() => navigate("/contact", { replace: true })}
|
||||||
|
>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<ContactRound size={18} />
|
||||||
|
<Text as="span">{t("contact")}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button onClick={logout} variant="outline" colorScheme="red">
|
<Button onClick={logout} variant="outline" colorScheme="red">
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<LogOut size={18} />
|
<LogOut size={18} />
|
||||||
|
|||||||
@@ -36,12 +36,43 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
|||||||
const [msgTitle, setMsgTitle] = useState("");
|
const [msgTitle, setMsgTitle] = useState("");
|
||||||
const [msgDescription, setMsgDescription] = useState("");
|
const [msgDescription, setMsgDescription] = useState("");
|
||||||
|
|
||||||
|
const [isMsgNAAS, setIsMsgNAAS] = useState(false);
|
||||||
|
const [msgStatusNAAS, setMsgStatusNAAS] = useState<"error" | "success">(
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
const [msgTitleNAAS, setMsgTitleNAAS] = useState("");
|
||||||
|
const [msgDescriptionNAAS, setMsgDescriptionNAAS] = useState("");
|
||||||
|
|
||||||
const [oldPassword, setOldPassword] = useState("");
|
const [oldPassword, setOldPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
|
||||||
// Dialog control
|
// Dialog control
|
||||||
const [isPwOpen, setPwOpen] = useState(false);
|
const [isPwOpen, setPwOpen] = useState(false);
|
||||||
|
const [naasDialog, setNaasDialog] = useState(false);
|
||||||
|
const [naas, setNaas] = useState("");
|
||||||
|
|
||||||
|
const openNAAS = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/no`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setNaas(data.reason);
|
||||||
|
setNaasDialog(true);
|
||||||
|
} catch (error) {
|
||||||
|
setMsgStatusNAAS("error");
|
||||||
|
setMsgTitleNAAS(t("naas-error"));
|
||||||
|
setMsgDescriptionNAAS(t("naas-error-desc"));
|
||||||
|
setIsMsgNAAS(true);
|
||||||
|
|
||||||
|
console.log(msgStatusNAAS, msgTitleNAAS, msgDescriptionNAAS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
@@ -147,14 +178,31 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Card.Footer justifyContent="flex-end">
|
<Card.Footer>
|
||||||
<Button variant="outline" onClick={() => props.setUserDialog(false)}>
|
<Stack w="100%" gap={3}>
|
||||||
|
{isMsgNAAS && (
|
||||||
|
<MyAlert
|
||||||
|
status={msgStatusNAAS}
|
||||||
|
title={msgTitleNAAS}
|
||||||
|
description={msgDescriptionNAAS}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<HStack justify="flex-end" gap={2} wrap="wrap">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => props.setUserDialog(false)}
|
||||||
|
>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => openNAAS()}>
|
||||||
|
{t("try-naas")}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
{/* Passwort-Dialog (kontrolliert) */}
|
{/* Passwort-Dialog */}
|
||||||
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
|
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
|
||||||
<Portal>
|
<Portal>
|
||||||
<Dialog.Backdrop />
|
<Dialog.Backdrop />
|
||||||
@@ -215,6 +263,31 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
|||||||
</Dialog.Positioner>
|
</Dialog.Positioner>
|
||||||
</Portal>
|
</Portal>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<HStack wrap="wrap" gap="4">
|
||||||
|
<Dialog.Root
|
||||||
|
placement={"center"}
|
||||||
|
open={naasDialog}
|
||||||
|
motionPreset="slide-in-bottom"
|
||||||
|
>
|
||||||
|
<Portal>
|
||||||
|
<Dialog.Backdrop />
|
||||||
|
<Dialog.Positioner>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>{t("naas-header")}</Dialog.Title>
|
||||||
|
</Dialog.Header>
|
||||||
|
<Dialog.Body>
|
||||||
|
<p>{naas}</p>
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.CloseTrigger asChild>
|
||||||
|
<CloseButton onClick={() => setNaasDialog(false)} size="sm" />
|
||||||
|
</Dialog.CloseTrigger>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||||
|
|
||||||
|
export const unlockAnimation = () => {
|
||||||
|
return (
|
||||||
|
<DotLottieReact
|
||||||
|
src="https://lottie.host/f839baa1-9c64-44c4-9386-f0e4c87ab208/2Iw1m4k86d.lottie"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const approvalAnimation = () => {
|
||||||
|
return (
|
||||||
|
<DotLottieReact
|
||||||
|
src="https://lottie.host/b7257009-9e3f-43e2-8112-a176f4696e4c/iQxxqAVOGX.lottie"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutAnimation = () => {
|
||||||
|
return (
|
||||||
|
<DotLottieReact
|
||||||
|
src="https://lottie.host/4975758c-de38-4d15-9f74-927709751d32/v8FtKpnD1y.lottie"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
|
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||||
import * as React from "react";
|
import {
|
||||||
import type { ReactNode } from "react";
|
ColorModeProvider,
|
||||||
import { ColorModeProvider as ThemeColorModeProvider } from "./color-mode";
|
type ColorModeProviderProps,
|
||||||
|
} from "./color-mode"
|
||||||
|
|
||||||
export interface ColorModeProviderProps {
|
export function Provider(props: ColorModeProviderProps) {
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ColorModeProvider({ children }: ColorModeProviderProps) {
|
|
||||||
// Wrap children with the real color-mode provider
|
|
||||||
return <ThemeColorModeProvider>{children}</ThemeColorModeProvider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Provider({ children }: { children: ReactNode }) {
|
|
||||||
return (
|
return (
|
||||||
<ChakraProvider value={defaultSystem}>
|
<ChakraProvider value={defaultSystem}>
|
||||||
<ColorModeProvider>{children}</ColorModeProvider>
|
<ColorModeProvider {...props} />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Field,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Container,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { Header } from "@/components/Header";
|
||||||
|
|
||||||
|
interface Alert {
|
||||||
|
type: "info" | "warning" | "success" | "error" | "neutral";
|
||||||
|
headline: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContactPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [alert, setAlert] = useState<Alert | null>(null);
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
// Logic to send the message
|
||||||
|
const result = await fetch(`${API_BASE}/api/users/contact`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
setAlert({
|
||||||
|
type: "success",
|
||||||
|
headline: t("contactPage_successHeadline"),
|
||||||
|
text: t("contactPage_successText"),
|
||||||
|
});
|
||||||
|
setMessage("");
|
||||||
|
} else if (result.status === 503) {
|
||||||
|
setAlert({
|
||||||
|
type: "error",
|
||||||
|
headline: t("serviceDeactivatedHeadline"),
|
||||||
|
text: t("contactPage_serviceDeactivatedText"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAlert({
|
||||||
|
type: "error",
|
||||||
|
headline: t("contactPage_errorHeadline"),
|
||||||
|
text: t("contactPage_errorText"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
|
<Header />
|
||||||
|
<Field.Root invalid={message === ""}>
|
||||||
|
<Field.Label>
|
||||||
|
<Text>{t("contactPage_messageDescription")}</Text>
|
||||||
|
<Field.RequiredIndicator />
|
||||||
|
</Field.Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t("contactPage_messagePlaceholder")}
|
||||||
|
variant="subtle"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
{message === "" && (
|
||||||
|
<Field.ErrorText>{t("contactPage_messageErrorText")}</Field.ErrorText>
|
||||||
|
)}
|
||||||
|
</Field.Root>
|
||||||
|
{alert && (
|
||||||
|
<Alert.Root status={alert.type}>
|
||||||
|
<Alert.Indicator />
|
||||||
|
<Alert.Content>
|
||||||
|
<Alert.Title>{alert.headline}</Alert.Title>
|
||||||
|
<Alert.Description>{alert.text}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
|
</Alert.Root>
|
||||||
|
)}
|
||||||
|
<Button onClick={sendMessage}>{t("contactPage_sendButton")}</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,8 @@ import { borrowAbleItemsAtom } from "@/states/Atoms";
|
|||||||
import { createLoan } from "@/utils/Fetcher";
|
import { createLoan } from "@/utils/Fetcher";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { approvalAnimation } from "@/components/dotLottie";
|
||||||
|
import { DeactivatedServices } from "@/components/DeactivatedServices";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -27,6 +29,8 @@ export interface User {
|
|||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [showAnimation, setShowAnimation] = useState(false);
|
||||||
|
|
||||||
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
||||||
const [startDate, setStartDate] = useState("");
|
const [startDate, setStartDate] = useState("");
|
||||||
const [endDate, setEndDate] = useState("");
|
const [endDate, setEndDate] = useState("");
|
||||||
@@ -46,13 +50,29 @@ export const HomePage = () => {
|
|||||||
setSelectedItems((prevSelected) =>
|
setSelectedItems((prevSelected) =>
|
||||||
prevSelected.includes(itemId)
|
prevSelected.includes(itemId)
|
||||||
? prevSelected.filter((id) => id !== itemId)
|
? prevSelected.filter((id) => id !== itemId)
|
||||||
: [...prevSelected, itemId]
|
: [...prevSelected, itemId],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showApprovalAnimation = (seconds: number) => {
|
||||||
|
const milliseconds = seconds * 1000;
|
||||||
|
|
||||||
|
setShowAnimation(true);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setShowAnimation(false);
|
||||||
|
}, milliseconds);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{showAnimation && (
|
||||||
|
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||||
|
<div>{approvalAnimation()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
<Header />
|
<Header />
|
||||||
|
<DeactivatedServices />
|
||||||
{isMsg && (
|
{isMsg && (
|
||||||
<MyAlert
|
<MyAlert
|
||||||
status={msgStatus}
|
status={msgStatus}
|
||||||
@@ -158,7 +178,7 @@ export const HomePage = () => {
|
|||||||
maxLength={MAX_CHARACTERS}
|
maxLength={MAX_CHARACTERS}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setNote(
|
setNote(
|
||||||
e.currentTarget.value.slice(0, MAX_CHARACTERS)
|
e.currentTarget.value.slice(0, MAX_CHARACTERS),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -178,16 +198,17 @@ export const HomePage = () => {
|
|||||||
setMsgStatus("error");
|
setMsgStatus("error");
|
||||||
setMsgTitle(response.title || t("error"));
|
setMsgTitle(response.title || t("error"));
|
||||||
setMsgDescription(
|
setMsgDescription(
|
||||||
response.description || t("unknown-error")
|
response.description || t("unknown-error"),
|
||||||
);
|
);
|
||||||
setIsMsg(true);
|
setIsMsg(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showApprovalAnimation(3);
|
||||||
setMsgStatus("success");
|
setMsgStatus("success");
|
||||||
setMsgTitle(t("success"));
|
setMsgTitle(t("success"));
|
||||||
setMsgDescription(t("loan-success"));
|
setMsgDescription(t("loan-success"));
|
||||||
setIsMsg(true);
|
setIsMsg(true);
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -196,5 +217,6 @@ export const HomePage = () => {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Button,
|
Button,
|
||||||
|
Container,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
import MyAlert from "@/components/myChakra/MyAlert";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Header } from "@/components/Header";
|
||||||
|
|
||||||
export const formatDateTime = (value: string | null | undefined) => {
|
export const formatDateTime = (value: string | null | undefined) => {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
@@ -32,6 +33,7 @@ type Loan = {
|
|||||||
returned_date: string | null;
|
returned_date: string | null;
|
||||||
take_date: string | null;
|
take_date: string | null;
|
||||||
loaned_items_name: string[] | string;
|
loaned_items_name: string[] | string;
|
||||||
|
note: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Device = {
|
type Device = {
|
||||||
@@ -46,7 +48,6 @@ type Device = {
|
|||||||
|
|
||||||
const Landingpage: React.FC = () => {
|
const Landingpage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loans, setLoans] = useState<Loan[]>([]);
|
const [loans, setLoans] = useState<Loan[]>([]);
|
||||||
@@ -59,7 +60,7 @@ const Landingpage: React.FC = () => {
|
|||||||
const setError = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
message: string,
|
message: string,
|
||||||
description: string
|
description: string,
|
||||||
) => {
|
) => {
|
||||||
setIsError(false);
|
setIsError(false);
|
||||||
setErrorStatus(status);
|
setErrorStatus(status);
|
||||||
@@ -78,6 +79,16 @@ const Landingpage: React.FC = () => {
|
|||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (loanRes.status === 503) {
|
||||||
|
setError(
|
||||||
|
"error",
|
||||||
|
t("serviceDeactivatedHeadline"),
|
||||||
|
t("loan_page_serviceDeactivatedText"),
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loanData = await loanRes.json();
|
const loanData = await loanRes.json();
|
||||||
if (Array.isArray(loanData)) {
|
if (Array.isArray(loanData)) {
|
||||||
setLoans(loanData);
|
setLoans(loanData);
|
||||||
@@ -85,7 +96,7 @@ const Landingpage: React.FC = () => {
|
|||||||
setError(
|
setError(
|
||||||
"error",
|
"error",
|
||||||
t("error-by-loading"),
|
t("error-by-loading"),
|
||||||
t("unexpected-date-format_loan")
|
t("unexpected-date-format_loan"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +113,7 @@ const Landingpage: React.FC = () => {
|
|||||||
setError(
|
setError(
|
||||||
"error",
|
"error",
|
||||||
t("error-by-loading"),
|
t("error-by-loading"),
|
||||||
t("unexpected-date-format_device")
|
t("unexpected-date-format_device"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -115,14 +126,8 @@ const Landingpage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
<Heading as="h1" size="lg" mb={2}>
|
<Header />
|
||||||
Matthias-Claudius-Schule Technik
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Button onClick={() => navigate("/", { replace: true })}>
|
|
||||||
{t("back")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Heading as="h2" size="md" mb={4}>
|
<Heading as="h2" size="md" mb={4}>
|
||||||
{t("all-loans")}
|
{t("all-loans")}
|
||||||
@@ -168,6 +173,9 @@ const Landingpage: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>{t("return-date")}</strong>
|
<strong>{t("return-date")}</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>{t("note")}</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
@@ -184,6 +192,7 @@ const Landingpage: React.FC = () => {
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
||||||
|
<Table.Cell>{loan.note}</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
))}
|
))}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
@@ -260,7 +269,7 @@ const Landingpage: React.FC = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,47 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
|||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Navigate, useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import { unlockAnimation } from "@/components/dotLottie";
|
||||||
|
import { logoutAnimation } from "@/components/dotLottie";
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
||||||
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
||||||
|
const [showAnimation, setShowAnimation] = useState(false);
|
||||||
|
const [showLogout, setShowLogout] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const from = location.state?.from?.pathname || "/";
|
const from = location.state?.from?.pathname || "/";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn) {
|
if (triggerLogout) {
|
||||||
navigate(from, { replace: true });
|
setShowLogout(true);
|
||||||
window.location.reload(); // if deleted, the user context is not updated in time
|
window.setTimeout(() => {
|
||||||
|
setShowLogout(false);
|
||||||
|
}, 4500);
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, navigate, from]);
|
|
||||||
|
if (!isLoggedIn) return;
|
||||||
|
|
||||||
|
// Existing sessions should redirect immediately, fresh logins wait for animation.
|
||||||
|
if (!showAnimation) {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
window.location.reload(); // keeps user context in sync after login
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, [isLoggedIn, showAnimation, navigate, from]);
|
||||||
|
|
||||||
const loginFnc = async (username: string, password: string) => {
|
const loginFnc = async (username: string, password: string) => {
|
||||||
const response = await fetch(`${API_BASE}/api/users/login`, {
|
const response = await fetch(`${API_BASE}/api/users/login`, {
|
||||||
@@ -42,6 +63,8 @@ export const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowAnimation(true);
|
||||||
|
|
||||||
Cookies.set("token", data.token);
|
Cookies.set("token", data.token);
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -62,14 +85,22 @@ export const LoginPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTriggerLogout(false);
|
setTriggerLogout(false);
|
||||||
navigate(from, { replace: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
return <Navigate to={from} replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{showAnimation && (
|
||||||
|
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||||
|
<div>{unlockAnimation()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showLogout && (
|
||||||
|
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||||
|
<div>{logoutAnimation()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center p-4">
|
<div className="flex flex-1 items-center justify-center p-4">
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<Card.Root maxW="sm">
|
<Card.Root maxW="sm">
|
||||||
@@ -97,9 +128,17 @@ export const LoginPage = () => {
|
|||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Card.Footer justifyContent="flex-end">
|
<Card.Footer justifyContent="flex-end">
|
||||||
{isError && (
|
{isError && (
|
||||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
<MyAlert
|
||||||
|
status="error"
|
||||||
|
title={errorMsg}
|
||||||
|
description={errorDsc}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => handleLogin()}
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
@@ -115,5 +154,6 @@ export const LoginPage = () => {
|
|||||||
</Card.Root>
|
</Card.Root>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ export const MyLoansPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 503) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("serviceDeactivatedHeadline"));
|
||||||
|
setMsgDescription(t("loan_page_serviceDeactivatedText"));
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setMsgStatus("error");
|
setMsgStatus("error");
|
||||||
setMsgTitle(t("error"));
|
setMsgTitle(t("error"));
|
||||||
setMsgDescription(t("error-fetching-loans"));
|
setMsgDescription(t("error-fetching-loans"));
|
||||||
@@ -84,6 +91,14 @@ export const MyLoansPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 507) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("error"));
|
||||||
|
setMsgDescription(t("error-deleting-loan-507"));
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setMsgStatus("error");
|
setMsgStatus("error");
|
||||||
setMsgTitle(t("error"));
|
setMsgTitle(t("error"));
|
||||||
setMsgDescription(t("error-deleting-loan"));
|
setMsgDescription(t("error-deleting-loan"));
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export const getBorrowableItems = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 503) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
status: "error",
|
||||||
|
title: "Service deactivated",
|
||||||
|
description:
|
||||||
|
"The loan service is currently deactivated. Please try again later.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
status: "error",
|
status: "error",
|
||||||
@@ -60,6 +70,16 @@ export const createLoan = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 503) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
status: "error",
|
||||||
|
title: "Service deactivated",
|
||||||
|
description:
|
||||||
|
"The loan service is currently deactivated. Please try again later.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
status: "error",
|
status: "error",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
"admin-status": "Admin-Status",
|
"admin-status": "Admin-Status",
|
||||||
"first-name": "Vorname",
|
"first-name": "Vorname",
|
||||||
"last-name": "Nachname",
|
"last-name": "Nachname",
|
||||||
"app-title": "Ausleihsystem (demo)",
|
"app-title": "Ausleihsystem",
|
||||||
"last-borrowed-person": "Zuletzt ausgeliehen von",
|
"last-borrowed-person": "Zuletzt ausgeliehen von",
|
||||||
"currently-borrowed-by": "Derzeit ausgeliehen von",
|
"currently-borrowed-by": "Derzeit ausgeliehen von",
|
||||||
"back": "Zurückgehen",
|
"back": "Zurückgehen",
|
||||||
@@ -88,5 +88,16 @@
|
|||||||
"take-loan-success": "Ausleihe erfolgreich abgeholt",
|
"take-loan-success": "Ausleihe erfolgreich abgeholt",
|
||||||
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
|
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
|
||||||
"network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.",
|
"network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.",
|
||||||
"contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden."
|
"contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden.",
|
||||||
|
"naas": "No-as-a-service",
|
||||||
|
"try-naas": "Klick mich",
|
||||||
|
"naas-error": "Fehler mit no-as-a-service",
|
||||||
|
"naas-error-desc": "Ein Fehler ist beim Kommunizieren mit no-as-a-service aufgetreten.",
|
||||||
|
"naas-header": "Eine gute Möglichkeit, nein zu sagen...",
|
||||||
|
"error-deleting-loan-507": "Die Ausleihe kann nicht gelöscht werden, da sie noch nicht zurückgegeben wurde.",
|
||||||
|
"serviceDeactivatedHeadline": "Service deaktiviert",
|
||||||
|
"contactPage_serviceDeactivatedText": "Der Kontaktservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.",
|
||||||
|
"loan_page_serviceDeactivatedText": "Der Ausleihservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.",
|
||||||
|
"is-deactivated": "ist deaktiviert.",
|
||||||
|
"deactivated-services": "Deaktivierte Services"
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"admin-status": "Admin status",
|
"admin-status": "Admin status",
|
||||||
"first-name": "First name",
|
"first-name": "First name",
|
||||||
"last-name": "Last name",
|
"last-name": "Last name",
|
||||||
"app-title": "Borrow System (demo)",
|
"app-title": "Borrow System",
|
||||||
"last-borrowed-person": "Last borrowed by",
|
"last-borrowed-person": "Last borrowed by",
|
||||||
"currently-borrowed-by": "Currently borrowed by",
|
"currently-borrowed-by": "Currently borrowed by",
|
||||||
"back": "Go back",
|
"back": "Go back",
|
||||||
@@ -88,5 +88,16 @@
|
|||||||
"take-loan-success": "Loan taken successfully",
|
"take-loan-success": "Loan taken successfully",
|
||||||
"return-loan-success": "Loan returned successfully",
|
"return-loan-success": "Loan returned successfully",
|
||||||
"network-error": "Network error. Please contact the administrator.",
|
"network-error": "Network error. Please contact the administrator.",
|
||||||
"contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible."
|
"contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible.",
|
||||||
|
"naas": "No-as-a-service",
|
||||||
|
"try-naas": "Click me",
|
||||||
|
"naas-error": "Error with no-as-a-service",
|
||||||
|
"naas-error-desc": "An error occurred while communicating with no-as-a-service.",
|
||||||
|
"naas-header": "A good way to say no...",
|
||||||
|
"error-deleting-loan-507": "The loan cannot be deleted because it has not been returned yet.",
|
||||||
|
"serviceDeactivatedHeadline": "Service deactivated",
|
||||||
|
"contactPage_serviceDeactivatedText": "The contact service is currently deactivated. Please try again later.",
|
||||||
|
"loan_page_serviceDeactivatedText": "The loan service is currently deactivated. Please try again later.",
|
||||||
|
"is-deactivated": "is deactivated.",
|
||||||
|
"deactivated-services": "Deactivated services"
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
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";
|
||||||
import path from "node:path";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss()],
|
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "src"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
allowedHosts: ["insta.the1s.de"],
|
port: 8001,
|
||||||
port: 8101,
|
watch: {
|
||||||
watch: { usePolling: true },
|
usePolling: true,
|
||||||
hmr: {
|
|
||||||
host: "insta.the1s.de",
|
|
||||||
port: 8101,
|
|
||||||
protocol: "wss",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Copyright (c) 2026 Theis Gaedigk
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This source code is not to be copied, modified, or distributed in any form
|
||||||
|
without explicit written permission from the author.
|
||||||
@@ -14,7 +14,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /backend/ {
|
location /backend/ {
|
||||||
proxy_pass http://demo_borrow_system-backend_v2:8102/;
|
proxy_pass http://borrow_system-backend_v2:8004/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "v1.3.2 (dev)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 APIKeyTable from "@/components/APIKeyTable";
|
||||||
|
import ServerConfig from "@/components/ServerConfig";
|
||||||
import { MoveLeft } from "lucide-react";
|
import { MoveLeft } from "lucide-react";
|
||||||
|
|
||||||
type DashboardProps = {
|
type DashboardProps = {
|
||||||
@@ -44,6 +45,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
||||||
viewUser={() => setActiveView("User")}
|
viewUser={() => setActiveView("User")}
|
||||||
viewAPI={() => setActiveView("API")}
|
viewAPI={() => setActiveView("API")}
|
||||||
|
viewConfig={() => setActiveView("Server Konfiguration")}
|
||||||
/>
|
/>
|
||||||
<Box flex="1" display="flex" flexDirection="column">
|
<Box flex="1" display="flex" flexDirection="column">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -88,6 +90,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
{activeView === "Ausleihen" && <LoanTable />}
|
{activeView === "Ausleihen" && <LoanTable />}
|
||||||
{activeView === "Gegenstände" && <ItemTable />}
|
{activeView === "Gegenstände" && <ItemTable />}
|
||||||
{activeView === "API" && <APIKeyTable />}
|
{activeView === "API" && <APIKeyTable />}
|
||||||
|
{activeView === "Server Konfiguration" && <ServerConfig />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type SidebarProps = {
|
|||||||
viewSchliessfaecher: () => void;
|
viewSchliessfaecher: () => void;
|
||||||
viewUser: () => void;
|
viewUser: () => void;
|
||||||
viewAPI: () => void;
|
viewAPI: () => void;
|
||||||
|
viewConfig: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
@@ -16,6 +17,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
viewGegenstaende,
|
viewGegenstaende,
|
||||||
viewUser,
|
viewUser,
|
||||||
viewAPI,
|
viewAPI,
|
||||||
|
viewConfig
|
||||||
}) => {
|
}) => {
|
||||||
const [info, setInfo] = useState<any>(null);
|
const [info, setInfo] = useState<any>(null);
|
||||||
|
|
||||||
@@ -83,6 +85,15 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
>
|
>
|
||||||
API Keys
|
API Keys
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
rounded="md"
|
||||||
|
_hover={{ bg: "gray.700", textDecoration: "none" }}
|
||||||
|
onClick={viewConfig}
|
||||||
|
>
|
||||||
|
Server Konfiguration
|
||||||
|
</Link>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
Heading,
|
||||||
|
Switch,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import MyAlert from "./myChakra/MyAlert";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { formatDateTime } from "@/utils/userFuncs";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
|
||||||
|
type Items = {
|
||||||
|
id: number;
|
||||||
|
function_name: string;
|
||||||
|
active: boolean;
|
||||||
|
entry_created_at: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerConfig: 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 handleSwitchChange = async (id: number, newState: boolean) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/api/admin/server-config/update?functionName=${encodeURIComponent(
|
||||||
|
items.find((item) => item.id === id)?.function_name || "",
|
||||||
|
)}&active=${newState}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
setReload((prev) => !prev);
|
||||||
|
setError(
|
||||||
|
"success",
|
||||||
|
"Status updated",
|
||||||
|
"The function status was updated successfully.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setError(
|
||||||
|
"error",
|
||||||
|
"Failed to update status",
|
||||||
|
"There is an error updating the function status.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError(
|
||||||
|
"error",
|
||||||
|
"Failed to update status",
|
||||||
|
"There is an error updating the function status.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (
|
||||||
|
status: "error" | "success",
|
||||||
|
message: string,
|
||||||
|
description: string,
|
||||||
|
) => {
|
||||||
|
setIsError(false);
|
||||||
|
setErrorStatus(status);
|
||||||
|
setErrorMessage(message);
|
||||||
|
setErrorDsc(description);
|
||||||
|
setIsError(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/api/admin/server-config/all`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.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 (
|
||||||
|
<>
|
||||||
|
<Heading marginBottom={4} size="2xl">
|
||||||
|
Server Konfiguration
|
||||||
|
</Heading>
|
||||||
|
{isError && (
|
||||||
|
<MyAlert
|
||||||
|
status={errorStatus}
|
||||||
|
description={errorDsc}
|
||||||
|
title={errorMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<VStack colorPalette="teal">
|
||||||
|
<Spinner color="colorPalette.600" />
|
||||||
|
<Text color="colorPalette.600">Loading...</Text>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Table.Root size="sm" striped w="100%" style={{ tableLayout: "auto" }}>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
|
<strong>#</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Service Name</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Toggle</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Eintrag erstellt am</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Table.Row key={item.id}>
|
||||||
|
<Table.Cell whiteSpace="nowrap">{item.id}</Table.Cell>
|
||||||
|
<Table.Cell fontFamily="mono">{item.function_name}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Switch.Root
|
||||||
|
checked={item.active}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleSwitchChange(item.id, !item.active)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch.HiddenInput />
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
<Switch.Label />
|
||||||
|
</Switch.Root>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell whiteSpace="nowrap">
|
||||||
|
{formatDateTime(item.entry_created_at)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerConfig;
|
||||||
@@ -8,13 +8,9 @@ export default defineConfig({
|
|||||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
allowedHosts: ["admin.insta.the1s.de"],
|
port: 8003,
|
||||||
port: 8103,
|
watch: {
|
||||||
watch: { usePolling: true },
|
usePolling: true,
|
||||||
hmr: {
|
|
||||||
host: "admin.insta.the1s.de",
|
|
||||||
port: 8103,
|
|
||||||
protocol: "wss",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"backend-info": {
|
"backend-info": {
|
||||||
"version": "v2.1 (demo)"
|
"version": "v2.2 (dev)"
|
||||||
},
|
},
|
||||||
"frontend-info": {
|
"frontend-info": {
|
||||||
"version": "v2.1 (demo)"
|
"version": "v2.2 (dev)"
|
||||||
},
|
},
|
||||||
"admin-panel-info": {
|
"admin-panel-info": {
|
||||||
"version": "v1.3.2 (demo)"
|
"version": "v1.3.2 (dev)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "1.0.0",
|
"version": "v2.1.1 (dev)",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "1.0.0",
|
"version": "v2.1.1 (dev)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.4.1",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"nodemailer": "^7.0.6"
|
"nodemailer": "^7.0.6"
|
||||||
@@ -349,6 +350,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||||
|
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
@@ -527,6 +546,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "1.0.0",
|
"version": "v2.1.1 (dev)",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.4.1",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"nodemailer": "^7.0.6"
|
"nodemailer": "^7.0.6"
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import mysql from "mysql2";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql
|
||||||
|
.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
export const getAllFunctions = async () => {
|
||||||
|
const [rows] = await pool.query("SELECT * FROM functions");
|
||||||
|
return { success: true, data: rows };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateFunctionStatus = async (functionName, active) => {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"UPDATE functions SET active = ? WHERE function_name = ?",
|
||||||
|
[active, functionName],
|
||||||
|
);
|
||||||
|
if (result.affectedRows > 0) return { success: true };
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
@@ -29,14 +29,14 @@ export const createUser = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deleteUserById = async (userId) => {
|
export const deleteUserById = async (userId) => {
|
||||||
const [result] = await pool.query("DELETE FROM users WHERE id = ? AND secret_user = false", [userId]);
|
const [result] = await pool.query("DELETE FROM users WHERE id = ?", [userId]);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePassword = async (username, newPassword) => {
|
export const changePassword = async (username, newPassword) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ? AND secret_user = false",
|
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?",
|
||||||
[newPassword, username],
|
[newPassword, username],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
@@ -52,7 +52,7 @@ export const editUserById = async (
|
|||||||
is_admin,
|
is_admin,
|
||||||
) => {
|
) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ? AND secret_user = false",
|
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?",
|
||||||
[first_name, last_name, role, email, is_admin, userId],
|
[first_name, last_name, role, email, is_admin, userId],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
@@ -61,7 +61,7 @@ export const editUserById = async (
|
|||||||
|
|
||||||
export const getAllUsers = async () => {
|
export const getAllUsers = async () => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users WHERE secret_user = false",
|
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users",
|
||||||
);
|
);
|
||||||
if (result.length > 0) return { success: true, data: result };
|
if (result.length > 0) return { success: true, data: result };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -69,7 +69,7 @@ export const getAllUsers = async () => {
|
|||||||
|
|
||||||
export const getUserById = async (userId) => {
|
export const getUserById = async (userId) => {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ? AND secret_user = false",
|
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?",
|
||||||
[userId],
|
[userId],
|
||||||
);
|
);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { authenticateAdmin } from "../../services/authentication.js";
|
||||||
|
const router = express.Router();
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// database funcs import
|
||||||
|
import {
|
||||||
|
getAllFunctions,
|
||||||
|
updateFunctionStatus,
|
||||||
|
} from "./database/serverConfMgmt.database.js";
|
||||||
|
|
||||||
|
// Route to get all functions and their statuses
|
||||||
|
router.get("/all", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await getAllFunctions();
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ data: result.data });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to fetch functions" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ message: "An error occurred", error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to update the status of a function
|
||||||
|
router.post("/update", async (req, res) => {
|
||||||
|
const functionName = req.query.functionName;
|
||||||
|
let active = req.query.active;
|
||||||
|
|
||||||
|
if (active === "false") {
|
||||||
|
active = 0;
|
||||||
|
} else if (active === "true") {
|
||||||
|
active = 1;
|
||||||
|
} else {
|
||||||
|
res.status(406).json({ message: "Got unexpected format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateFunctionStatus(functionName, active);
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ message: "Function status updated successfully" });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to update function status" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { authenticate } from "../../services/authentication.js";
|
import { authenticate } from "../../services/authentication.js";
|
||||||
|
import { checkIfServiceIsActive } from "../../services/functions.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
const loan_service = "Loan Service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getItemsFromDatabaseV2,
|
getItemsFromDatabaseV2,
|
||||||
changeInSafeStateV2,
|
changeInSafeStateV2,
|
||||||
@@ -39,6 +42,7 @@ router.post("/change-state/:key/:itemId", authenticate, async (req, res) => {
|
|||||||
router.get(
|
router.get(
|
||||||
"/get-loan-by-code/:key/:loan_code",
|
"/get-loan-by-code/:key/:loan_code",
|
||||||
authenticate,
|
authenticate,
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const loan_code = req.params.loan_code;
|
const loan_code = req.params.loan_code;
|
||||||
const result = await getLoanByCodeV2(loan_code);
|
const result = await getLoanByCodeV2(loan_code);
|
||||||
@@ -54,6 +58,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/set-return-date/:key/:loan_code",
|
"/set-return-date/:key/:loan_code",
|
||||||
authenticate,
|
authenticate,
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const loanCode = req.params.loan_code;
|
const loanCode = req.params.loan_code;
|
||||||
const result = await setReturnDateV2(loanCode);
|
const result = await setReturnDateV2(loanCode);
|
||||||
@@ -69,6 +74,7 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
"/set-take-date/:key/:loan_code",
|
"/set-take-date/:key/:loan_code",
|
||||||
authenticate,
|
authenticate,
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const loanCode = req.params.loan_code;
|
const loanCode = req.params.loan_code;
|
||||||
const result = await setTakeDateV2(loanCode);
|
const result = await setTakeDateV2(loanCode);
|
||||||
|
|||||||
@@ -234,6 +234,23 @@ export const getBorrowableItemsFromDatabase = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SETdeleteLoanFromDatabase = async (loanId) => {
|
export const SETdeleteLoanFromDatabase = async (loanId) => {
|
||||||
|
const [checkIfdatesReturned] = await pool.query(
|
||||||
|
"SELECT take_date, returned_date FROM loans WHERE id = ? AND deleted = 0",
|
||||||
|
[loanId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (checkIfdatesReturned.length === 0) {
|
||||||
|
return { success: false, code: "LOAN_NOT_FOUND" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { take_date, returned_date } = checkIfdatesReturned[0];
|
||||||
|
const bothNull = take_date === null && returned_date === null;
|
||||||
|
const bothSet = take_date !== null && returned_date !== null;
|
||||||
|
|
||||||
|
if (!(bothNull || bothSet)) {
|
||||||
|
return { success: false, code: "LOAN_NOT_RETURNED" };
|
||||||
|
}
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET deleted = 1 WHERE id = ?;",
|
"UPDATE loans SET deleted = 1 WHERE id = ?;",
|
||||||
[loanId],
|
[loanId],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const pool = mysql
|
|||||||
export const loginFunc = async (username, password) => {
|
export const loginFunc = async (username, password) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT * FROM users WHERE username = ? AND password = ?",
|
"SELECT * FROM users WHERE username = ? AND password = ?",
|
||||||
[username, password]
|
[username, password],
|
||||||
);
|
);
|
||||||
if (result.length > 0) return { success: true, data: result[0] };
|
if (result.length > 0) return { success: true, data: result[0] };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -40,7 +40,7 @@ export const changePassword = async (username, oldPassword, newPassword) => {
|
|||||||
// get user current password
|
// get user current password
|
||||||
const [user] = await pool.query(
|
const [user] = await pool.query(
|
||||||
"SELECT * FROM users WHERE username = ? AND password = ?",
|
"SELECT * FROM users WHERE username = ? AND password = ?",
|
||||||
[username, oldPassword]
|
[username, oldPassword],
|
||||||
);
|
);
|
||||||
if (user.length === 0) return { success: false };
|
if (user.length === 0) return { success: false };
|
||||||
|
|
||||||
@@ -48,8 +48,16 @@ export const changePassword = async (username, oldPassword, newPassword) => {
|
|||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET password = ? WHERE username = ?",
|
"UPDATE users SET password = ? WHERE username = ?",
|
||||||
[newPassword, username]
|
[newPassword, username],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDeactivatedServices = async () => {
|
||||||
|
const [rows] = await pool.query("SELECT function_name FROM functions WHERE active = 0;");
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return { success: true, data: rows };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { authenticate, generateToken } from "../../services/authentication.js";
|
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||||
|
import {
|
||||||
|
checkIfServiceIsActive,
|
||||||
|
checkIfServiceIsActive2,
|
||||||
|
} from "../../services/functions.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
const loan_service = "Loan Service";
|
||||||
|
const loan_mailer_service = "Loan Mailer";
|
||||||
|
|
||||||
// database funcs import
|
// database funcs import
|
||||||
import {
|
import {
|
||||||
createLoanInDatabase,
|
createLoanInDatabase,
|
||||||
@@ -18,7 +25,11 @@ import {
|
|||||||
} from "./database/loansMgmt.database.js";
|
} from "./database/loansMgmt.database.js";
|
||||||
import { sendMailLoan } from "./services/mailer.js";
|
import { sendMailLoan } from "./services/mailer.js";
|
||||||
|
|
||||||
router.post("/createLoan", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/createLoan",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { items, startDate, endDate, note } = req.body || {};
|
const { items, startDate, endDate, note } = req.body || {};
|
||||||
|
|
||||||
@@ -54,6 +65,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
if (await checkIfServiceIsActive2(loan_mailer_service)) {
|
||||||
const mailInfo = await getLoanInfoWithID(result.data.id);
|
const mailInfo = await getLoanInfoWithID(result.data.id);
|
||||||
console.log(mailInfo);
|
console.log(mailInfo);
|
||||||
sendMailLoan(
|
sendMailLoan(
|
||||||
@@ -64,6 +76,8 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
mailInfo.data.created_at,
|
mailInfo.data.created_at,
|
||||||
mailInfo.data.note,
|
mailInfo.data.note,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
message: "Loan created successfully",
|
message: "Loan created successfully",
|
||||||
loanId: result.data.id,
|
loanId: result.data.id,
|
||||||
@@ -86,9 +100,14 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
console.error("createLoan error:", err);
|
console.error("createLoan error:", err);
|
||||||
return res.status(500).json({ message: "Failed to create loan" });
|
return res.status(500).json({ message: "Failed to create loan" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.get("/loans", authenticate, async (req, res) => {
|
router.get(
|
||||||
|
"/loans",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const result = await getLoansFromDatabase(req.user.username);
|
const result = await getLoansFromDatabase(req.user.username);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json(result.data);
|
res.status(200).json(result.data);
|
||||||
@@ -97,9 +116,14 @@ router.get("/loans", authenticate, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch loans" });
|
res.status(500).json({ message: "Failed to fetch loans" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/set-return-date/:loan_code",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const loanCode = req.params.loan_code;
|
const loanCode = req.params.loan_code;
|
||||||
const result = await setReturnDate(loanCode);
|
const result = await setReturnDate(loanCode);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -107,9 +131,14 @@ router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set return date" });
|
res.status(500).json({ message: "Failed to set return date" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/set-take-date/:loan_code",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const loanCode = req.params.loan_code;
|
const loanCode = req.params.loan_code;
|
||||||
const result = await setTakeDate(loanCode);
|
const result = await setTakeDate(loanCode);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -117,7 +146,8 @@ router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set take date" });
|
res.status(500).json({ message: "Failed to set take date" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.get("/all-items", authenticate, async (req, res) => {
|
router.get("/all-items", authenticate, async (req, res) => {
|
||||||
const result = await getItems();
|
const result = await getItems();
|
||||||
@@ -128,26 +158,50 @@ router.get("/all-items", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/delete-loan/:id", authenticate, async (req, res) => {
|
router.delete(
|
||||||
|
"/delete-loan/:id",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const loanId = req.params.id;
|
const loanId = req.params.id;
|
||||||
const result = await SETdeleteLoanFromDatabase(loanId);
|
const result = await SETdeleteLoanFromDatabase(loanId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json({ message: "Loan deleted successfully" });
|
res.status(200).json({ message: "Loan deleted successfully" });
|
||||||
} else {
|
} else {
|
||||||
|
if (result.code === "LOAN_NOT_FOUND") {
|
||||||
|
res.status(404).json({ message: "Loan not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "LOAN_NOT_RETURNED") {
|
||||||
|
res.status(507).json({
|
||||||
|
message: "Cannot delete loan that has not been returned",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(500).json({ message: "Failed to delete loan" });
|
res.status(500).json({ message: "Failed to delete loan" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.get("/all-loans", authenticate, async (req, res) => {
|
router.get(
|
||||||
|
"/all-loans",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const result = await getALLLoans();
|
const result = await getALLLoans();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json(result.data);
|
res.status(200).json(result.data);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch loans" });
|
res.status(500).json({ message: "Failed to fetch loans" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/borrowable-items", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/borrowable-items",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const { startDate, endDate } = req.body || {};
|
const { startDate, endDate } = req.body || {};
|
||||||
if (!startDate || !endDate) {
|
if (!startDate || !endDate) {
|
||||||
return res
|
return res
|
||||||
@@ -168,6 +222,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.json({ message: "Failed to fetch borrowable items" });
|
.json({ message: "Failed to fetch borrowable items" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { authenticate, generateToken } from "../../services/authentication.js";
|
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||||
|
import { checkIfServiceIsActive } from "../../services/functions.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
const user_frontend_service = "User Frontend";
|
||||||
|
const contact_form_service = "Contact Form Service";
|
||||||
|
|
||||||
// database funcs import
|
// database funcs import
|
||||||
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
import {
|
||||||
|
loginFunc,
|
||||||
|
changePassword,
|
||||||
|
getDeactivatedServices,
|
||||||
|
} from "./database/userMgmt.database.js";
|
||||||
import { sendMail } from "./services/mailer_v2.js";
|
import { sendMail } from "./services/mailer_v2.js";
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
router.post(
|
||||||
|
"/login",
|
||||||
|
checkIfServiceIsActive(user_frontend_service),
|
||||||
|
async (req, res) => {
|
||||||
const result = await loginFunc(req.body.username, req.body.password);
|
const result = await loginFunc(req.body.username, req.body.password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const token = await generateToken({
|
const token = await generateToken({
|
||||||
@@ -22,9 +33,14 @@ router.post("/login", async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(401).json({ message: "Invalid credentials" });
|
res.status(401).json({ message: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/change-password", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/change-password",
|
||||||
|
checkIfServiceIsActive(user_frontend_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const oldPassword = req.body.oldPassword;
|
const oldPassword = req.body.oldPassword;
|
||||||
const newPassword = req.body.newPassword;
|
const newPassword = req.body.newPassword;
|
||||||
const username = req.user.username;
|
const username = req.user.username;
|
||||||
@@ -34,15 +50,30 @@ router.post("/change-password", authenticate, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to change password" });
|
res.status(500).json({ message: "Failed to change password" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/contact", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/contact",
|
||||||
|
checkIfServiceIsActive(contact_form_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const message = req.body.message;
|
const message = req.body.message;
|
||||||
const username = req.user.username;
|
const username = req.user.username;
|
||||||
|
|
||||||
sendMail(username, message);
|
sendMail(username, message);
|
||||||
|
|
||||||
res.status(200).json({ message: "Contact message sent successfully" });
|
res.status(200).json({ message: "Contact message sent successfully" });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get("/deactivated-services", authenticate, async (req, res) => {
|
||||||
|
const result = await getDeactivatedServices();
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json(result.data);
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to fetch deactivated services" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
USE borrow_system_new;
|
|
||||||
|
|
||||||
-- USERS
|
|
||||||
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin)
|
|
||||||
VALUES
|
|
||||||
('user1', 'passwordhash1', 'user1@example.com', 'First1', 'Last1', 1, false),
|
|
||||||
('user2', 'passwordhash2', 'user2@example.com', 'First2', 'Last2', 1, false),
|
|
||||||
('user3', 'passwordhash3', 'user3@example.com', 'First3', 'Last3', 2, false),
|
|
||||||
('admin1', 'passwordhash4', 'admin1@example.com', 'Admin', 'One', 9, true),
|
|
||||||
('admin2', 'passwordhash5', 'admin2@example.com', 'Admin', 'Two', 9, true);
|
|
||||||
|
|
||||||
-- ITEMS
|
|
||||||
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, door_key, last_borrowed_person, currently_borrowing)
|
|
||||||
VALUES
|
|
||||||
('Item1', 1, true, 1, 101, NULL, NULL),
|
|
||||||
('Item2', 1, true, 2, 102, 'user1', 'user1'),
|
|
||||||
('Item3', 2, true, 3, 103, 'user2', NULL),
|
|
||||||
('Item4', 1, false, NULL, NULL, NULL, NULL),
|
|
||||||
('Item5', 2, false, NULL, NULL, 'user3', 'user3');
|
|
||||||
|
|
||||||
-- LOANS
|
|
||||||
INSERT INTO loans (
|
|
||||||
username,
|
|
||||||
lockers,
|
|
||||||
loan_code,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
take_date,
|
|
||||||
returned_date,
|
|
||||||
created_at,
|
|
||||||
loaned_items_id,
|
|
||||||
loaned_items_name,
|
|
||||||
deleted,
|
|
||||||
note
|
|
||||||
)
|
|
||||||
VALUES
|
|
||||||
(
|
|
||||||
'user1',
|
|
||||||
JSON_ARRAY('Locker1', 'Locker2'),
|
|
||||||
'123456',
|
|
||||||
'2026-02-01 09:00:00',
|
|
||||||
'2026-02-10 17:00:00',
|
|
||||||
'2026-02-01 09:15:00',
|
|
||||||
NULL,
|
|
||||||
'2026-02-01 09:00:00',
|
|
||||||
JSON_ARRAY(1, 2),
|
|
||||||
JSON_ARRAY('Item1', 'Item2'),
|
|
||||||
false,
|
|
||||||
'Erste allgemeine Ausleihe'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'user2',
|
|
||||||
JSON_ARRAY('Locker3'),
|
|
||||||
'234567',
|
|
||||||
'2026-02-02 10:00:00',
|
|
||||||
'2026-02-05 16:00:00',
|
|
||||||
'2026-02-02 10:05:00',
|
|
||||||
'2026-02-05 15:30:00',
|
|
||||||
'2026-02-02 10:00:00',
|
|
||||||
JSON_ARRAY(3),
|
|
||||||
JSON_ARRAY('Item3'),
|
|
||||||
false,
|
|
||||||
'Zurückgegeben vor Enddatum'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'user3',
|
|
||||||
JSON_ARRAY(),
|
|
||||||
'345678',
|
|
||||||
'2026-02-03 08:30:00',
|
|
||||||
'2026-02-15 18:00:00',
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
'2026-02-03 08:30:00',
|
|
||||||
JSON_ARRAY(5),
|
|
||||||
JSON_ARRAY('Item5'),
|
|
||||||
false,
|
|
||||||
'Noch ausgeliehen'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'user1',
|
|
||||||
JSON_ARRAY('Locker4'),
|
|
||||||
'456789',
|
|
||||||
'2025-12-01 09:00:00',
|
|
||||||
'2025-12-03 17:00:00',
|
|
||||||
'2025-12-01 09:10:00',
|
|
||||||
'2025-12-03 16:45:00',
|
|
||||||
'2025-12-01 09:00:00',
|
|
||||||
JSON_ARRAY(1),
|
|
||||||
JSON_ARRAY('Item1'),
|
|
||||||
true,
|
|
||||||
'Alte, gelöschte Ausleihe'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- API KEYS
|
|
||||||
INSERT INTO apiKeys (api_key, entry_name)
|
|
||||||
VALUES
|
|
||||||
('10000001', 'Entry1'),
|
|
||||||
('10000002', 'Entry2'),
|
|
||||||
('10000003', 'Entry3'),
|
|
||||||
('10000004', 'Entry4');
|
|
||||||
@@ -11,7 +11,6 @@ CREATE TABLE users (
|
|||||||
is_admin bool NOT NULL DEFAULT false,
|
is_admin bool NOT NULL DEFAULT false,
|
||||||
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
secret_user bool NOT NULL DEFAULT false,
|
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
@@ -56,3 +55,14 @@ CREATE TABLE apiKeys (
|
|||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (api_key REGEXP '^[0-9]{8}$')
|
CHECK (api_key REGEXP '^[0-9]{8}$')
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE functions (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
function_name VARCHAR(500) NOT NULL UNIQUE,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
INSERT INTO functions (function_name) VALUES ("Loan Mailer"), ("Loan Service"), ("Contact Form Service"), ("User Frontend"), ("API")
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import env from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import info from "./info.json" assert { type: "json" };
|
import info from "./info.json" assert { type: "json" };
|
||||||
import { authenticate } from "./services/authentication.js";
|
import { authenticate } from "./services/authentication.js";
|
||||||
|
import { rateLimit } from "express-rate-limit";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const app = express();
|
||||||
|
const port = 8004;
|
||||||
|
const naasURL = process.env.NAAS_URL;
|
||||||
|
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 1 * 60 * 1000, // 1 minute
|
||||||
|
limit: 50, // Limit each IP to 50 requests per `window` (here, per 1 minute).
|
||||||
|
standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
|
||||||
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
|
||||||
|
ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive
|
||||||
|
// store: ... , // Redis, Memcached, etc. See below.
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(limiter);
|
||||||
|
|
||||||
// frontend routes
|
// frontend routes
|
||||||
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
|
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
|
||||||
@@ -14,14 +31,11 @@ import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js";
|
|||||||
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
|
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
|
||||||
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
|
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
|
||||||
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
|
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
|
||||||
|
import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js";
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
import apiRouter from "./routes/api/api.route.js";
|
import apiRouter from "./routes/api/api.route.js";
|
||||||
|
|
||||||
env.config();
|
|
||||||
const app = express();
|
|
||||||
const port = 8102;
|
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
// Body-Parser VOR den Routen registrieren
|
// Body-Parser VOR den Routen registrieren
|
||||||
app.use(express.json({ limit: "10mb" }));
|
app.use(express.json({ limit: "10mb" }));
|
||||||
@@ -37,6 +51,7 @@ app.use("/api/admin/user-data", userDataMgmtRouter);
|
|||||||
app.use("/api/admin/item-data", itemDataMgmtRouter);
|
app.use("/api/admin/item-data", itemDataMgmtRouter);
|
||||||
app.use("/api/admin/api-data", apiDataMgmtRouter);
|
app.use("/api/admin/api-data", apiDataMgmtRouter);
|
||||||
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
|
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
|
||||||
|
app.use("/api/admin/server-config", serverConfMgmtRouter);
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
app.use("/api", apiRouter);
|
app.use("/api", apiRouter);
|
||||||
@@ -47,6 +62,20 @@ app.listen(port, () => {
|
|||||||
console.log(`Server is running on port: ${port}`);
|
console.log(`Server is running on port: ${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/no", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(naasURL);
|
||||||
|
if (!response.ok) {
|
||||||
|
res.status(500).send("Request to no-as-a-service went wrong.");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error communicating with no-as-a-service:", error);
|
||||||
|
res.status(500).send("Error communicating with no-as-a-service.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/verify", authenticate, async (req, res) => {
|
app.get("/verify", authenticate, async (req, res) => {
|
||||||
res.status(200).json({ message: "Token is valid", user: req.user });
|
res.status(200).json({ message: "Token is valid", user: req.user });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { SignJWT, jwtVerify } from "jose";
|
import { SignJWT, jwtVerify } from "jose";
|
||||||
import env from "dotenv";
|
import env from "dotenv";
|
||||||
import { verifyAPIKeyDB } from "./database.js";
|
import { verifyAPIKeyDB } from "./database.js";
|
||||||
|
import { checkIfServiceIsActive2 } from "./functions.js";
|
||||||
env.config();
|
env.config();
|
||||||
|
|
||||||
|
const api_service = "API";
|
||||||
|
const user_frontend_service = "User Frontend";
|
||||||
|
|
||||||
const secretKey = process.env.SECRET_KEY;
|
const secretKey = process.env.SECRET_KEY;
|
||||||
if (!secretKey) {
|
if (!secretKey) {
|
||||||
throw new Error("Missing SECRET_KEY environment variable");
|
throw new Error("Missing SECRET_KEY environment variable");
|
||||||
@@ -45,6 +49,13 @@ export async function authenticate(req, res, next) {
|
|||||||
const apiKey = req.params.key;
|
const apiKey = req.params.key;
|
||||||
|
|
||||||
if (authHeader) {
|
if (authHeader) {
|
||||||
|
const serviceActive = await checkIfServiceIsActive2(user_frontend_service);
|
||||||
|
if (!serviceActive) {
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: "User Frontend is currently unavailable." });
|
||||||
|
}
|
||||||
|
|
||||||
const parts = authHeader.split(" ");
|
const parts = authHeader.split(" ");
|
||||||
const scheme = parts[0];
|
const scheme = parts[0];
|
||||||
const token = parts[1];
|
const token = parts[1];
|
||||||
@@ -61,6 +72,13 @@ export async function authenticate(req, res, next) {
|
|||||||
return res.status(403).json({ message: "Present token invalid" }); // present token invalid
|
return res.status(403).json({ message: "Present token invalid" }); // present token invalid
|
||||||
}
|
}
|
||||||
} else if (apiKey) {
|
} else if (apiKey) {
|
||||||
|
const serviceActive = await checkIfServiceIsActive2(api_service);
|
||||||
|
if (!serviceActive) {
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: "API Service is currently unavailable." });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await verifyAPIKey(apiKey);
|
await verifyAPIKey(apiKey);
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import mysql from "mysql2";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql
|
||||||
|
.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
export function checkIfServiceIsActive(service) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
|
||||||
|
[service],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: `-${service}- is currently unavailable.` });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkIfServiceIsActive2(service) {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
|
||||||
|
[service],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog for upcoming version: v2.2
|
||||||
|
|
||||||
|
This update provides some new features for the design. It also contains some improvements and I have also fixed some bugs.
|
||||||
|
|
||||||
|
## New features
|
||||||
|
|
||||||
|
- The overview page now has the note column and is overall better organised
|
||||||
|
- I also addded the regular header to the page
|
||||||
|
- I have added three animations to the Borrow System
|
||||||
|
- I have added a new icon for the frontend, which is now also used in the header and the favicon. It is a dark version of the old icon, which fits better to the overall design. I have made it with Icon Composer. The old icon is still used for the admin panel, which has a light design. (Maybe I will change the admin panel design in the future...)
|
||||||
|
- When you go to your user card (over the user icon in the header) you have a new button "Click me". If you click it, you will get an message... _I am just saying: I have implemented the no-as-a-service code in to my Backend._
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
- I have the error logging for the API route wehre you can take loans improved.
|
||||||
|
- If you try to delete a loan that has not been returned yet, you will get an 507 error code.
|
||||||
|
|
||||||
|
## Fixed bugs
|
||||||
|
|
||||||
|
- Fixed bug: #13
|
||||||
|
- Fixed bug for messaging when server has an error
|
||||||
|
- Fixed footer height
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New version numbers
|
||||||
|
|
||||||
|
**Backend:** v2.2
|
||||||
|
|
||||||
|
**Frontend:** v2.2
|
||||||
|
|
||||||
|
**Admin panel:** v1.3.2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
-[Theis](https://portfolio-theis.de)
|
||||||
@@ -1,51 +1,56 @@
|
|||||||
services:
|
services:
|
||||||
demo_usr_frontend:
|
# usr-frontend_v2:
|
||||||
container_name: demo_borrow_system-usr-frontend
|
# container_name: borrow_system-usr-frontend
|
||||||
networks:
|
# build: ./FrontendV2
|
||||||
- proxynet
|
# ports:
|
||||||
build: ./FrontendV2
|
# - "8001:80"
|
||||||
restart: unless-stopped
|
# restart: always
|
||||||
|
|
||||||
demo_admin_frontend:
|
# admin-frontend:
|
||||||
container_name: demo_borrow_system-admin-frontend
|
# container_name: borrow_system-admin-frontend
|
||||||
networks:
|
# build: ./admin
|
||||||
- proxynet
|
# ports:
|
||||||
build: ./admin
|
# - "8003:80"
|
||||||
restart: unless-stopped
|
# restart: always
|
||||||
|
|
||||||
demo_backend_v2:
|
backend_v2:
|
||||||
container_name: demo_borrow_system-backend_v2
|
container_name: borrow_system-backend_v2
|
||||||
networks:
|
|
||||||
- proxynet
|
|
||||||
build: ./backendV2
|
build: ./backendV2
|
||||||
|
ports:
|
||||||
|
- "8004:8004"
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DB_HOST: demo_mysql_v2
|
DB_HOST: mysql_v2
|
||||||
DB_USER: root
|
DB_USER: root
|
||||||
DB_PASSWORD: ${DB_PASSWORD_V2}
|
DB_PASSWORD: ${DB_PASSWORD_V2}
|
||||||
DB_NAME: borrow_system_new
|
DB_NAME: borrow_system_new
|
||||||
depends_on:
|
depends_on:
|
||||||
- demo_mysql_v2
|
- mysql_v2
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
|
|
||||||
demo_mysql_v2:
|
mysql_v2:
|
||||||
container_name: demo_borrow_system-mysql-v2
|
container_name: borrow_system-mysql-v2
|
||||||
networks:
|
|
||||||
- proxynet
|
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
|
||||||
MYSQL_DATABASE: borrow_system_new
|
MYSQL_DATABASE: borrow_system_new
|
||||||
TZ: Europe/Berlin
|
TZ: Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
- demo_mysql-v2-data:/var/lib/mysql
|
- mysql-v2-data:/var/lib/mysql
|
||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||||
|
ports:
|
||||||
|
- "3310:3306"
|
||||||
|
|
||||||
|
no-as-a-service:
|
||||||
|
container_name: borrow_system-naas
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
build:
|
||||||
|
context: ./no-as-a-service
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
demo_mysql-v2-data:
|
mysql-v2-data:
|
||||||
|
|
||||||
networks:
|
|
||||||
proxynet:
|
|
||||||
external: true
|
|
||||||
|
|||||||