Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06cb298a38 | |||
| 8589971dc8 | |||
| 6ec8e19737 | |||
| d29c793b6b | |||
| 9f44a4796d | |||
| c97cc8b538 | |||
| dc0a68f7f1 | |||
| fe3a06e5ce | |||
| 776fab749d | |||
| 179f5686d1 | |||
| 83b43f4c83 | |||
| 5d9cee63ab | |||
| 0b203d838c | |||
| ae1888fe90 | |||
| f1c02910e6 | |||
| d33b288956 | |||
| 5e2a426401 | |||
| 022aa669e8 | |||
| 28373e0231 | |||
| 2f3583ccd0 | |||
| 9da72cc5bf | |||
| c633627b7c | |||
| 5259c41b13 | |||
| 3d9e3814fe | |||
| b44edb2b1d | |||
| a72fabc0a0 | |||
| 1406f28f86 | |||
| 38d1091e9b | |||
| f82efecb8c | |||
| 1f12bc8839 | |||
| f19750f6f3 | |||
| 808b3fd5c4 | |||
| 0891598eb9 | |||
| 39ff02f2e7 | |||
| cc67fb4f85 | |||
| 75ff4aadc1 | |||
| 6f998d07c1 | |||
| f2bb326040 | |||
| 8c701db900 | |||
| d1664338a6 | |||
| 1a2624cd9e | |||
| a138190cc6 | |||
| 993e0cd74b | |||
| dab004a7b6 | |||
| d039336f39 | |||
| 4c781e9325 | |||
| 451e6b3646 |
@@ -117,8 +117,3 @@ 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/
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "no-as-a-service"]
|
|
||||||
path = no-as-a-service
|
|
||||||
url = https://github.com/hotheadhacker/no-as-a-service.git
|
|
||||||
@@ -1,177 +1,366 @@
|
|||||||
# Borrow System API Documentation
|
# Borrow System API Documentation
|
||||||
|
|
||||||
## Overview
|
**Frontend:** https://insta.the1s.de
|
||||||
|
**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 requests must include a valid API key in the URL path as the `:key` parameter. API keys are 8-digit numeric strings.
|
All API endpoints require **either**:
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
The Base URL for all endpoints is: `https://insta.the1s.de/backend/api`
|
### 1. Get All Items
|
||||||
|
|
||||||
### Get All Items
|
**GET** `/api/items/:key`
|
||||||
|
|
||||||
`GET /items/:key`
|
Returns a list of all items.
|
||||||
|
|
||||||
Returns all items in the system.
|
#### Path Parameters
|
||||||
|
|
||||||
**Response 200:**
|
- `:key` – API key (8-digit number)
|
||||||
|
|
||||||
|
#### 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": "Laptop",
|
"item_name": "DJI 1er Mikro",
|
||||||
"can_borrow_role": 1,
|
"can_borrow_role": 4,
|
||||||
"in_safe": true,
|
"inSafe": 1,
|
||||||
"safe_nr": 3,
|
"safe_nr": 3,
|
||||||
"door_key": 101,
|
"door_key": "123",
|
||||||
"last_borrowed_person": "jdoe",
|
"entry_created_at": "2025-08-19T22:02:16.000Z",
|
||||||
|
"entry_updated_at": "2025-08-19T22:02:16.000Z",
|
||||||
|
"last_borrowed_person": "alice",
|
||||||
"currently_borrowing": null
|
"currently_borrowing": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response 500:**
|
#### Error Response (500)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to fetch items" }
|
{
|
||||||
|
"message": "Failed to fetch items"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Change Item Safe State
|
### 2. Toggle Item Safe State
|
||||||
|
|
||||||
`POST /change-state/:key/:itemId`
|
Toggles `in_safe` between `0` and `1` for a given item.
|
||||||
|
|
||||||
Toggles the `in_safe` boolean state of an item.
|
**Keep in mind that when you return a loan by code, the item states are automatically updated.**
|
||||||
|
|
||||||
**URL Parameters:**
|
**POST** `/api/change-state/:key/:itemId`
|
||||||
|
|
||||||
- **key** - API key
|
#### Path Parameters
|
||||||
- **itemId** - The item's ID
|
|
||||||
|
|
||||||
**Response 200:** Returns on successful toggle.
|
- `:key` – API key (8-digit number)
|
||||||
|
- `:itemId` – Item ID (integer)
|
||||||
|
|
||||||
**Response 500:**
|
#### Authentication
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Get Loan by Code
|
### 3. Get Loan by Code
|
||||||
|
|
||||||
`GET /get-loan-by-code/:key/:loan_code`
|
Fetch loan information by `loan_code`.
|
||||||
|
|
||||||
Retrieves loan details by its 6-digit loan code.
|
**GET** `/api/get-loan-by-code/:key/:loan_code`
|
||||||
|
|
||||||
**URL Parameters:**
|
#### Path Parameters
|
||||||
|
|
||||||
- **key** - API key
|
- `:key` – API key (8-digit number)
|
||||||
- **loan_code** - A 6-digit numeric loan code
|
- `:loan_code` – Loan code (string)
|
||||||
|
|
||||||
**Response 200:**
|
#### Authentication
|
||||||
|
|
||||||
|
- 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": "jdoe",
|
"username": "john",
|
||||||
"returned_date": null,
|
"returned_date": null,
|
||||||
"take_date": "2024-01-15T10:30:00.000Z",
|
"take_date": "2025-01-01T10:00:00.000Z",
|
||||||
"lockers": [1, 3]
|
"lockers": "[1, 2, 3]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response 404:**
|
#### Error Response (404)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Loan not found" }
|
{
|
||||||
|
"message": "Loan not found"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Set Take Date
|
### 4. Set Loan Return Date
|
||||||
|
|
||||||
`POST /set-take-date/:key/:loan_code`
|
Sets `returned_date = NOW()` on a loan and updates related items:
|
||||||
|
|
||||||
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.
|
- `in_safe = 1`
|
||||||
|
- `currently_borrowing = NULL`
|
||||||
|
- `last_borrowed_person = username`
|
||||||
|
|
||||||
**URL Parameters:**
|
**POST** `/api/set-return-date/:key/:loan_code`
|
||||||
|
|
||||||
- **key** - API key
|
#### Path Parameters
|
||||||
- **loan_code** - A 6-digit numeric loan code
|
|
||||||
|
|
||||||
**Response 200:** Empty JSON object on success.
|
- `:key` – API key (8-digit number)
|
||||||
|
- `:loan_code` – Loan code (string)
|
||||||
|
|
||||||
**Response 500:**
|
#### Authentication
|
||||||
|
|
||||||
```json
|
- Either Bearer token or `:key` API key.
|
||||||
{ "message": "Loan not found or already taken" }
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/set-return-date/12345678/12345 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** This endpoint will fail if the loan has already been taken or does not exist.
|
#### Successful Response (200)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Response (500)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Failed to set return date"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Set Return Date
|
### 5. Set Loan Take Date
|
||||||
|
|
||||||
`POST /set-return-date/:key/:loan_code`
|
Sets `take_date = NOW()` on a loan and updates related items:
|
||||||
|
|
||||||
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.
|
- `in_safe = 0`
|
||||||
|
- `currently_borrowing = username`
|
||||||
|
|
||||||
**URL Parameters:**
|
**POST** `/api/set-take-date/:key/:loan_code`
|
||||||
|
|
||||||
- **key** - API key
|
#### Path Parameters
|
||||||
- **loan_code** - A 6-digit numeric loan code
|
|
||||||
|
|
||||||
**Response 200:** Empty JSON object on success.
|
- `:key` – API key (8-digit number)
|
||||||
|
- `:loan_code` – Loan code (string)
|
||||||
|
|
||||||
**Response 500:**
|
#### Authentication
|
||||||
|
|
||||||
```json
|
- Either Bearer token or `:key` API key.
|
||||||
{ "message": "Failed to set return date" }
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/set-take-date/12345678/LOAN-12345 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** This endpoint will fail if the loan has already been returned (i.e., `returned_date` is not `NULL`).
|
#### Successful Response (200)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Response (500)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Failed to set take date"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Open Door
|
### 6. Open Door by Door Key
|
||||||
|
|
||||||
`GET /open-door/:key/:doorKey`
|
Looks up an item by its `door_key`, toggles `in_safe`, and returns safe information.
|
||||||
|
|
||||||
Toggles the safe state of an item identified by its door key and returns the associated safe number.
|
**GET** `/api/open-door/:key/:doorKey`
|
||||||
|
|
||||||
**URL Parameters:**
|
#### Path Parameters
|
||||||
|
|
||||||
- **key** - API key
|
- `:key` – API key (8-digit number)
|
||||||
- **doorKey** - The door key identifier assigned to an item
|
- `:doorKey` – Door key/token (string) used by hardware to identify the locker.
|
||||||
|
|
||||||
**Response 200:**
|
#### Authentication
|
||||||
|
|
||||||
|
- 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": 3,
|
"safe_nr": 5,
|
||||||
"id": 1
|
"id": 42
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response 500:**
|
#### Error Response (500)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to open door" }
|
{
|
||||||
|
"message": "Failed to open door"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Handling
|
---
|
||||||
|
|
||||||
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.
|
## Authentication Error Messages
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 416 KiB |
@@ -2,11 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
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://borrow_system-backend_v2:8004/;
|
proxy_pass http://demo_borrow_system-backend_v2:8102/;
|
||||||
}
|
}
|
||||||
|
|
||||||
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": "v2.1.2 (dev)",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 420 B |
@@ -12,11 +12,10 @@ import { triggerLogoutAtom } from "@/states/Atoms";
|
|||||||
import { MyLoansPage } from "./pages/MyLoansPage";
|
import { MyLoansPage } from "./pages/MyLoansPage";
|
||||||
import Landingpage from "./pages/Landingpage";
|
import Landingpage from "./pages/Landingpage";
|
||||||
import { changeLanguage } from "i18next";
|
import { changeLanguage } from "i18next";
|
||||||
import { Flex } from "@chakra-ui/react";
|
import { Box, 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();
|
||||||
|
|
||||||
@@ -72,8 +71,8 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Flex direction="column" minH="100dvh">
|
<Flex direction="column" minH="100vh">
|
||||||
<Flex as="main" flex="1" direction="column">
|
<Box as="main" flex="1">
|
||||||
<UserContext.Provider value={user}>
|
<UserContext.Provider value={user}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -81,14 +80,13 @@ 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 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
</Flex>
|
</Box>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Flex>
|
</Flex>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
Image,
|
|
||||||
Heading,
|
Heading,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -23,7 +22,6 @@ 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";
|
||||||
@@ -69,7 +67,6 @@ 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
|
||||||
@@ -144,7 +141,7 @@ export const Header = () => {
|
|||||||
value="help"
|
value="help"
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
window.open(
|
window.open(
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages",
|
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener,noreferrer",
|
"noopener,noreferrer",
|
||||||
)
|
)
|
||||||
@@ -156,16 +153,6 @@ 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"
|
||||||
@@ -192,13 +179,6 @@ 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"
|
||||||
@@ -288,7 +268,7 @@ export const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages"
|
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
@@ -298,17 +278,6 @@ 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,43 +36,12 @@ 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) {
|
||||||
@@ -178,31 +147,14 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Card.Footer>
|
<Card.Footer justifyContent="flex-end">
|
||||||
<Stack w="100%" gap={3}>
|
<Button variant="outline" onClick={() => props.setUserDialog(false)}>
|
||||||
{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 */}
|
{/* Passwort-Dialog (kontrolliert) */}
|
||||||
<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 />
|
||||||
@@ -263,31 +215,6 @@ 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -5,14 +5,7 @@ export const Footer = () => {
|
|||||||
const { data: info } = useVersionInfoQuery();
|
const { data: info } = useVersionInfoQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box as="footer" py={4} textAlign="center" width="100%">
|
||||||
as="footer"
|
|
||||||
py={4}
|
|
||||||
textAlign="center"
|
|
||||||
width="100%"
|
|
||||||
flexShrink={0}
|
|
||||||
fontSize="sm"
|
|
||||||
>
|
|
||||||
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
||||||
<br />
|
<br />
|
||||||
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
|
||||||
import {
|
import * as React from "react";
|
||||||
ColorModeProvider,
|
import type { ReactNode } from "react";
|
||||||
type ColorModeProviderProps,
|
import { ColorModeProvider as ThemeColorModeProvider } from "./color-mode";
|
||||||
} from "./color-mode"
|
|
||||||
|
|
||||||
export function Provider(props: ColorModeProviderProps) {
|
export interface 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 {...props} />
|
<ColorModeProvider>{children}</ColorModeProvider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
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,8 +18,6 @@ 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;
|
||||||
@@ -29,8 +27,6 @@ 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("");
|
||||||
@@ -50,29 +46,13 @@ 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}
|
||||||
@@ -178,7 +158,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)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -198,17 +178,16 @@ 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);
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -217,6 +196,5 @@ export const HomePage = () => {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ 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 { Header } from "@/components/Header";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const formatDateTime = (value: string | null | undefined) => {
|
export const formatDateTime = (value: string | null | undefined) => {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
@@ -33,7 +32,6 @@ 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 = {
|
||||||
@@ -48,6 +46,7 @@ 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[]>([]);
|
||||||
@@ -60,7 +59,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);
|
||||||
@@ -79,16 +78,6 @@ 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);
|
||||||
@@ -96,7 +85,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")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +102,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) {
|
||||||
@@ -126,8 +115,14 @@ const Landingpage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
<>
|
||||||
<Header />
|
<Heading as="h1" size="lg" mb={2}>
|
||||||
|
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")}
|
||||||
@@ -173,9 +168,6 @@ 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>
|
||||||
@@ -192,7 +184,6 @@ 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>
|
||||||
@@ -269,7 +260,7 @@ const Landingpage: React.FC = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Container>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,47 +4,27 @@ 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 { useNavigate, useLocation } from "react-router-dom";
|
import { Navigate, 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 { Footer } from "@/components/footer/Footer";
|
||||||
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 (triggerLogout) {
|
if (isLoggedIn) {
|
||||||
setShowLogout(true);
|
|
||||||
window.setTimeout(() => {
|
|
||||||
setShowLogout(false);
|
|
||||||
}, 4500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoggedIn) return;
|
|
||||||
|
|
||||||
// Existing sessions should redirect immediately, fresh logins wait for animation.
|
|
||||||
if (!showAnimation) {
|
|
||||||
navigate(from, { replace: true });
|
navigate(from, { replace: true });
|
||||||
return;
|
window.location.reload(); // if deleted, the user context is not updated in time
|
||||||
}
|
}
|
||||||
|
}, [isLoggedIn, navigate, from]);
|
||||||
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`, {
|
||||||
@@ -63,8 +43,6 @@ 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 };
|
||||||
@@ -85,23 +63,15 @@ export const LoginPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTriggerLogout(false);
|
setTriggerLogout(false);
|
||||||
|
navigate(from, { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return <Navigate to={from} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen flex items-center justify-center p-4">
|
||||||
{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">
|
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<Card.Root maxW="sm">
|
<Card.Root maxW="sm">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
@@ -128,17 +98,9 @@ export const LoginPage = () => {
|
|||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Card.Footer justifyContent="flex-end">
|
<Card.Footer justifyContent="flex-end">
|
||||||
{isError && (
|
{isError && (
|
||||||
<MyAlert
|
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
||||||
status="error"
|
|
||||||
title={errorMsg}
|
|
||||||
description={errorDsc}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
||||||
type="submit"
|
|
||||||
onClick={() => handleLogin()}
|
|
||||||
variant="solid"
|
|
||||||
>
|
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
@@ -153,7 +115,7 @@ export const LoginPage = () => {
|
|||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</form>
|
</form>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,13 +52,6 @@ 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"));
|
||||||
@@ -91,14 +84,6 @@ 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"));
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const getBorrowableItems = async (
|
export const getBorrowableItems = async (
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
) => {
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -17,22 +20,11 @@ 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",
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description:
|
description: t("serverError"),
|
||||||
"An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +62,6 @@ 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",
|
"app-title": "Ausleihsystem (demo)",
|
||||||
"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,16 +88,5 @@
|
|||||||
"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",
|
"app-title": "Borrow System (demo)",
|
||||||
"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,16 +88,5 @@
|
|||||||
"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,16 +1,23 @@
|
|||||||
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 tsconfigPaths from "vite-tsconfig-paths";
|
import path from "node:path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
plugins: [tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 8001,
|
allowedHosts: ["insta.the1s.de"],
|
||||||
watch: {
|
port: 8101,
|
||||||
usePolling: true,
|
watch: { usePolling: true },
|
||||||
|
hmr: {
|
||||||
|
host: "insta.the1s.de",
|
||||||
|
port: 8101,
|
||||||
|
protocol: "wss",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
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://borrow_system-backend_v2:8004/;
|
proxy_pass http://demo_borrow_system-backend_v2:8102/;
|
||||||
}
|
}
|
||||||
|
|
||||||
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": "v1.3.2 (dev)",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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 = {
|
||||||
@@ -45,7 +44,6 @@ 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
|
||||||
@@ -90,7 +88,6 @@ 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>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useState } from "react";
|
|||||||
import { loginFunc } from "@/utils/loginUser";
|
import { loginFunc } from "@/utils/loginUser";
|
||||||
import MyAlert from "../components/myChakra/MyAlert";
|
import MyAlert from "../components/myChakra/MyAlert";
|
||||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
|
|
||||||
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -44,7 +43,8 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
|||||||
</Field.Root>
|
</Field.Root>
|
||||||
<Field.Root>
|
<Field.Root>
|
||||||
<Field.Label>password</Field.Label>
|
<Field.Label>password</Field.Label>
|
||||||
<PasswordInput
|
<Input
|
||||||
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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> = ({
|
||||||
@@ -17,7 +16,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
viewGegenstaende,
|
viewGegenstaende,
|
||||||
viewUser,
|
viewUser,
|
||||||
viewAPI,
|
viewAPI,
|
||||||
viewConfig
|
|
||||||
}) => {
|
}) => {
|
||||||
const [info, setInfo] = useState<any>(null);
|
const [info, setInfo] = useState<any>(null);
|
||||||
|
|
||||||
@@ -85,15 +83,6 @@ 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">
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ButtonProps,
|
|
||||||
GroupProps,
|
|
||||||
InputProps,
|
|
||||||
StackProps,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
HStack,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
Stack,
|
|
||||||
mergeRefs,
|
|
||||||
useControllableState,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import * as React from "react"
|
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu"
|
|
||||||
|
|
||||||
export interface PasswordVisibilityProps {
|
|
||||||
/**
|
|
||||||
* The default visibility state of the password input.
|
|
||||||
*/
|
|
||||||
defaultVisible?: boolean
|
|
||||||
/**
|
|
||||||
* The controlled visibility state of the password input.
|
|
||||||
*/
|
|
||||||
visible?: boolean
|
|
||||||
/**
|
|
||||||
* Callback invoked when the visibility state changes.
|
|
||||||
*/
|
|
||||||
onVisibleChange?: (visible: boolean) => void
|
|
||||||
/**
|
|
||||||
* Custom icons for the visibility toggle button.
|
|
||||||
*/
|
|
||||||
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PasswordInputProps
|
|
||||||
extends InputProps,
|
|
||||||
PasswordVisibilityProps {
|
|
||||||
rootProps?: GroupProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PasswordInput = React.forwardRef<
|
|
||||||
HTMLInputElement,
|
|
||||||
PasswordInputProps
|
|
||||||
>(function PasswordInput(props, ref) {
|
|
||||||
const {
|
|
||||||
rootProps,
|
|
||||||
defaultVisible,
|
|
||||||
visible: visibleProp,
|
|
||||||
onVisibleChange,
|
|
||||||
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
|
|
||||||
...rest
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const [visible, setVisible] = useControllableState({
|
|
||||||
value: visibleProp,
|
|
||||||
defaultValue: defaultVisible || false,
|
|
||||||
onChange: onVisibleChange,
|
|
||||||
})
|
|
||||||
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputGroup
|
|
||||||
endElement={
|
|
||||||
<VisibilityTrigger
|
|
||||||
disabled={rest.disabled}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
if (rest.disabled) return
|
|
||||||
if (e.button !== 0) return
|
|
||||||
e.preventDefault()
|
|
||||||
setVisible(!visible)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{visible ? visibilityIcon.off : visibilityIcon.on}
|
|
||||||
</VisibilityTrigger>
|
|
||||||
}
|
|
||||||
{...rootProps}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...rest}
|
|
||||||
ref={mergeRefs(ref, inputRef)}
|
|
||||||
type={visible ? "text" : "password"}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
function VisibilityTrigger(props, ref) {
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
tabIndex={-1}
|
|
||||||
ref={ref}
|
|
||||||
me="-2"
|
|
||||||
aspectRatio="square"
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
height="calc(100% - {spacing.2})"
|
|
||||||
aria-label="Toggle password visibility"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
interface PasswordStrengthMeterProps extends StackProps {
|
|
||||||
max?: number
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PasswordStrengthMeter = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
PasswordStrengthMeterProps
|
|
||||||
>(function PasswordStrengthMeter(props, ref) {
|
|
||||||
const { max = 4, value, ...rest } = props
|
|
||||||
|
|
||||||
const percent = (value / max) * 100
|
|
||||||
const { label, colorPalette } = getColorPalette(percent)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
|
|
||||||
<HStack width="full" {...rest}>
|
|
||||||
{Array.from({ length: max }).map((_, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
height="1"
|
|
||||||
flex="1"
|
|
||||||
rounded="sm"
|
|
||||||
data-selected={index < value ? "" : undefined}
|
|
||||||
layerStyle="fill.subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
_selected={{
|
|
||||||
colorPalette,
|
|
||||||
layerStyle: "fill.solid",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
{label && <HStack textStyle="xs">{label}</HStack>}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function getColorPalette(percent: number) {
|
|
||||||
switch (true) {
|
|
||||||
case percent < 33:
|
|
||||||
return { label: "Low", colorPalette: "red" }
|
|
||||||
case percent < 66:
|
|
||||||
return { label: "Medium", colorPalette: "orange" }
|
|
||||||
default:
|
|
||||||
return { label: "High", colorPalette: "green" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["vite/client"],
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
@@ -24,10 +23,13 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
/* Path aliases */
|
/* Chakra / Pfad Aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ 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",
|
||||||
port: 8003,
|
allowedHosts: ["admin.insta.the1s.de"],
|
||||||
watch: {
|
port: 8103,
|
||||||
usePolling: true,
|
watch: { usePolling: true },
|
||||||
|
hmr: {
|
||||||
|
host: "admin.insta.the1s.de",
|
||||||
|
port: 8103,
|
||||||
|
protocol: "wss",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"backend-info": {
|
"backend-info": {
|
||||||
"version": "v2.2 (dev)"
|
"version": "v2.1 (demo)"
|
||||||
},
|
},
|
||||||
"frontend-info": {
|
"frontend-info": {
|
||||||
"version": "v2.2 (dev)"
|
"version": "v2.1 (demo)"
|
||||||
},
|
},
|
||||||
"admin-panel-info": {
|
"admin-panel-info": {
|
||||||
"version": "v1.3.2 (dev)"
|
"version": "v1.3.2 (demo)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "v2.1.1 (dev)",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "v2.1.1 (dev)",
|
"version": "1.0.0",
|
||||||
"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"
|
||||||
@@ -350,24 +349,6 @@
|
|||||||
"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",
|
||||||
@@ -546,15 +527,6 @@
|
|||||||
"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": "v2.1.1 (dev)",
|
"version": "1.0.0",
|
||||||
"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,7 +15,6 @@
|
|||||||
"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"
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
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 = ?", [userId]);
|
const [result] = await pool.query("DELETE FROM users WHERE id = ? AND secret_user = false", [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 = ?",
|
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ? AND secret_user = false",
|
||||||
[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 = ?",
|
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ? AND secret_user = false",
|
||||||
[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",
|
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users WHERE secret_user = false",
|
||||||
);
|
);
|
||||||
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 = ?",
|
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ? AND secret_user = false",
|
||||||
[userId],
|
[userId],
|
||||||
);
|
);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -22,7 +22,7 @@ export const getItemsFromDatabaseV2 = async () => {
|
|||||||
export const getLoanByCodeV2 = async (loan_code) => {
|
export const getLoanByCodeV2 = async (loan_code) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
|
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
|
||||||
[loan_code],
|
[loan_code]
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return { success: true, data: result[0] };
|
return { success: true, data: result[0] };
|
||||||
@@ -33,7 +33,7 @@ export const getLoanByCodeV2 = async (loan_code) => {
|
|||||||
export const changeInSafeStateV2 = async (itemId) => {
|
export const changeInSafeStateV2 = async (itemId) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||||
[itemId],
|
[itemId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -42,62 +42,50 @@ export const changeInSafeStateV2 = async (itemId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setReturnDateV2 = async (loanCode) => {
|
export const setReturnDateV2 = async (loanCode) => {
|
||||||
try {
|
|
||||||
const [items] = await pool.query(
|
const [items] = await pool.query(
|
||||||
"SELECT loaned_items_id, username FROM loans WHERE loan_code = ?",
|
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (items.length === 0)
|
const [owner] = await pool.query(
|
||||||
return { success: false, message: "No items found for loan" };
|
"SELECT username FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (items.length === 0) return { success: false };
|
||||||
|
|
||||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||||
? items[0].loaned_items_id
|
? items[0].loaned_items_id
|
||||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||||
|
|
||||||
|
const [setItemStates] = await pool.query(
|
||||||
|
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
|
||||||
|
[owner[0].username, itemIds]
|
||||||
|
);
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
|
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows === 0) return { success: false };
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
|
return { success: true };
|
||||||
if (itemIds.length > 0) {
|
|
||||||
await pool.query(
|
|
||||||
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = ? WHERE id IN (?)",
|
|
||||||
[items[0].username, itemIds],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, data: { returned: true } };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("setReturnDateV2 error:", error);
|
|
||||||
return { success: false, message: "Failed to set return date" };
|
|
||||||
}
|
}
|
||||||
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setTakeDateV2 = async (loanCode) => {
|
export const setTakeDateV2 = async (loanCode) => {
|
||||||
const [isTaken] = await pool.query(
|
|
||||||
"SELECT take_date FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTaken.length === 0 || isTaken[0].take_date !== null) {
|
|
||||||
return { success: false, message: "Loan not found or already taken" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const [items] = await pool.query(
|
const [items] = await pool.query(
|
||||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [owner] = await pool.query(
|
const [owner] = await pool.query(
|
||||||
"SELECT username FROM loans WHERE loan_code = ?",
|
"SELECT username FROM loans WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (items.length === 0)
|
if (items.length === 0) return { success: false };
|
||||||
return { success: false, message: "No items found for loan" };
|
|
||||||
|
|
||||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||||
? items[0].loaned_items_id
|
? items[0].loaned_items_id
|
||||||
@@ -105,18 +93,18 @@ export const setTakeDateV2 = async (loanCode) => {
|
|||||||
|
|
||||||
const [setItemStates] = await pool.query(
|
const [setItemStates] = await pool.query(
|
||||||
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
||||||
[owner[0].username, itemIds],
|
[owner[0].username, itemIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
|
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { message: "Failed to set take date", success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoansV2 = async () => {
|
export const getAllLoansV2 = async () => {
|
||||||
@@ -130,12 +118,12 @@ export const getAllLoansV2 = async () => {
|
|||||||
export const openDoor = async (doorKey) => {
|
export const openDoor = async (doorKey) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
||||||
[doorKey],
|
[doorKey]
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
const [changeItemSate] = await pool.query(
|
const [changeItemSate] = await pool.query(
|
||||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||||
[result[0].id],
|
[result[0].id]
|
||||||
);
|
);
|
||||||
if (changeItemSate.affectedRows > 0) {
|
if (changeItemSate.affectedRows > 0) {
|
||||||
return { success: true, data: result[0] };
|
return { success: true, data: result[0] };
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
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,
|
||||||
@@ -42,7 +39,6 @@ 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);
|
||||||
@@ -51,39 +47,37 @@ router.get(
|
|||||||
} else {
|
} else {
|
||||||
res.status(404).json({ message: "Loan not found" });
|
res.status(404).json({ message: "Loan not found" });
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route for API to set the return date by the loan code
|
// Route for API to set the return date by the loan code
|
||||||
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);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json({});
|
res.status(200).json({ data: result.data });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set return date" });
|
res.status(500).json({ message: "Failed to set return date" });
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route for API to set the take away date by the loan code
|
// Route for API to set the take away date by the loan code
|
||||||
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);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json({});
|
res.status(200).json({ data: result.data });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: result.message });
|
res.status(500).json({ message: "Failed to set take date" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route for API to open a door
|
// Route for API to open a door
|
||||||
|
|||||||
@@ -234,23 +234,6 @@ 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,16 +48,8 @@ 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,16 +1,9 @@
|
|||||||
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,
|
||||||
@@ -25,11 +18,7 @@ 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(
|
router.post("/createLoan", authenticate, async (req, res) => {
|
||||||
"/createLoan",
|
|
||||||
checkIfServiceIsActive(loan_service),
|
|
||||||
authenticate,
|
|
||||||
async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { items, startDate, endDate, note } = req.body || {};
|
const { items, startDate, endDate, note } = req.body || {};
|
||||||
|
|
||||||
@@ -65,7 +54,6 @@ router.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
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(
|
||||||
@@ -74,10 +62,7 @@ router.post(
|
|||||||
mailInfo.data.start_date,
|
mailInfo.data.start_date,
|
||||||
mailInfo.data.end_date,
|
mailInfo.data.end_date,
|
||||||
mailInfo.data.created_at,
|
mailInfo.data.created_at,
|
||||||
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,
|
||||||
@@ -100,14 +85,9 @@ router.post(
|
|||||||
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(
|
router.get("/loans", authenticate, async (req, res) => {
|
||||||
"/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);
|
||||||
@@ -116,14 +96,9 @@ router.get(
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch loans" });
|
res.status(500).json({ message: "Failed to fetch loans" });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
|
||||||
"/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) {
|
||||||
@@ -131,14 +106,9 @@ router.post(
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set return date" });
|
res.status(500).json({ message: "Failed to set return date" });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
|
||||||
"/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) {
|
||||||
@@ -146,8 +116,7 @@ router.post(
|
|||||||
} 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();
|
||||||
@@ -158,50 +127,26 @@ router.get("/all-items", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete(
|
router.delete("/delete-loan/:id", authenticate, async (req, res) => {
|
||||||
"/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(
|
router.get("/all-loans", authenticate, async (req, res) => {
|
||||||
"/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(
|
router.post("/borrowable-items", authenticate, async (req, res) => {
|
||||||
"/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
|
||||||
@@ -222,7 +167,6 @@ router.post(
|
|||||||
.status(500)
|
.status(500)
|
||||||
.json({ message: "Failed to fetch borrowable items" });
|
.json({ message: "Failed to fetch borrowable items" });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -34,14 +34,7 @@ const formatDateTime = (value) => {
|
|||||||
return "N/A";
|
return "N/A";
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildLoanEmail({
|
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
}) {
|
|
||||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||||
const itemsList =
|
const itemsList =
|
||||||
Array.isArray(items) && items.length
|
Array.isArray(items) && items.length
|
||||||
@@ -123,12 +116,6 @@ function buildLoanEmail({
|
|||||||
createdDate,
|
createdDate,
|
||||||
)}</td>
|
)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td style="padding:10px 14px; color:#6b7280; vertical-align:top;">Notiz</td>
|
|
||||||
<td style="padding:10px 14px; font-weight:600; color:#111827;">${
|
|
||||||
note || "Keine Notiz"
|
|
||||||
}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p style="margin:22px 0 0 0; font-size:14px;">
|
<p style="margin:22px 0 0 0; font-size:14px;">
|
||||||
@@ -147,14 +134,7 @@ function buildLoanEmail({
|
|||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLoanEmailText({
|
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
}) {
|
|
||||||
const itemsText =
|
const itemsText =
|
||||||
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
||||||
return [
|
return [
|
||||||
@@ -165,18 +145,10 @@ function buildLoanEmailText({
|
|||||||
`Start: ${formatDateTime(startDate)}`,
|
`Start: ${formatDateTime(startDate)}`,
|
||||||
`Ende: ${formatDateTime(endDate)}`,
|
`Ende: ${formatDateTime(endDate)}`,
|
||||||
`Erstellt am: ${formatDateTime(createdDate)}`,
|
`Erstellt am: ${formatDateTime(createdDate)}`,
|
||||||
`Notiz: ${note || "Keine Notiz"}`,
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMailLoan(
|
export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
) {
|
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: process.env.MAIL_HOST,
|
host: process.env.MAIL_HOST,
|
||||||
port: process.env.MAIL_PORT,
|
port: process.env.MAIL_PORT,
|
||||||
@@ -198,16 +170,8 @@ export function sendMailLoan(
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
createdDate,
|
createdDate,
|
||||||
note,
|
|
||||||
}),
|
|
||||||
html: buildLoanEmail({
|
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
}),
|
}),
|
||||||
|
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Loan message sent:", info.messageId);
|
console.log("Loan message sent:", info.messageId);
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
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 {
|
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
||||||
loginFunc,
|
|
||||||
changePassword,
|
|
||||||
getDeactivatedServices,
|
|
||||||
} from "./database/userMgmt.database.js";
|
|
||||||
import { sendMail } from "./services/mailer_v2.js";
|
import { sendMail } from "./services/mailer_v2.js";
|
||||||
|
|
||||||
router.post(
|
router.post("/login", async (req, res) => {
|
||||||
"/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({
|
||||||
@@ -33,14 +22,9 @@ router.post(
|
|||||||
} else {
|
} else {
|
||||||
res.status(401).json({ message: "Invalid credentials" });
|
res.status(401).json({ message: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post("/change-password", authenticate, async (req, res) => {
|
||||||
"/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;
|
||||||
@@ -50,30 +34,15 @@ router.post(
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to change password" });
|
res.status(500).json({ message: "Failed to change password" });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post("/contact", authenticate, async (req, res) => {
|
||||||
"/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;
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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,6 +11,7 @@ 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;
|
||||||
|
|
||||||
@@ -55,14 +56,3 @@ 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,25 +1,8 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import dotenv from "dotenv";
|
import env 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";
|
||||||
@@ -31,11 +14,14 @@ 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" }));
|
||||||
@@ -51,7 +37,6 @@ 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);
|
||||||
@@ -62,20 +47,6 @@ 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,12 +1,8 @@
|
|||||||
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");
|
||||||
@@ -49,13 +45,6 @@ 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];
|
||||||
@@ -72,13 +61,6 @@ 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();
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# 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,56 +1,51 @@
|
|||||||
services:
|
services:
|
||||||
# usr-frontend_v2:
|
demo_usr_frontend:
|
||||||
# container_name: borrow_system-usr-frontend
|
container_name: demo_borrow_system-usr-frontend
|
||||||
# build: ./FrontendV2
|
networks:
|
||||||
# ports:
|
- proxynet
|
||||||
# - "8001:80"
|
build: ./FrontendV2
|
||||||
# restart: always
|
restart: unless-stopped
|
||||||
|
|
||||||
# admin-frontend:
|
demo_admin_frontend:
|
||||||
# container_name: borrow_system-admin-frontend
|
container_name: demo_borrow_system-admin-frontend
|
||||||
# build: ./admin
|
networks:
|
||||||
# ports:
|
- proxynet
|
||||||
# - "8003:80"
|
build: ./admin
|
||||||
# restart: always
|
restart: unless-stopped
|
||||||
|
|
||||||
backend_v2:
|
demo_backend_v2:
|
||||||
container_name: borrow_system-backend_v2
|
container_name: demo_borrow_system-backend_v2
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
build: ./backendV2
|
build: ./backendV2
|
||||||
ports:
|
|
||||||
- "8004:8004"
|
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DB_HOST: mysql_v2
|
DB_HOST: demo_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:
|
||||||
- mysql_v2
|
- demo_mysql_v2
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
|
|
||||||
mysql_v2:
|
demo_mysql_v2:
|
||||||
container_name: borrow_system-mysql-v2
|
container_name: demo_borrow_system-mysql-v2
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
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:
|
||||||
- mysql-v2-data:/var/lib/mysql
|
- demo_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:
|
||||||
mysql-v2-data:
|
demo_mysql-v2-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
|||||||