Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a3c948386 | |||
| 6fb03530df | |||
| d2c36e71be | |||
| 40d784ab36 | |||
| 60c85efd37 | |||
| 747932cf03 | |||
| 0964109c4b | |||
| 5442f2f1f3 | |||
| 56e073244f | |||
| d2dc74971f | |||
| 25709ea0d9 | |||
| f8ab2490fe | |||
| 3de877dd2b | |||
| 5d0134017a | |||
| 07503ec079 | |||
| 4155982aac | |||
| 59e05e5c4c | |||
| 2facdab011 | |||
| 04db0bd7e0 | |||
| 40dd34bcac | |||
| 939d3e89c5 | |||
| a355c42964 | |||
| c8a230979f | |||
| 581cd4a1fd | |||
| c53e6e095a | |||
| eaa325668c | |||
| 1fa8b4a9a7 | |||
| 3ba3c1c0cb | |||
| ee54d51f8b | |||
| a8dab549af | |||
| 06976f7972 | |||
| 977a6c1b16 | |||
| 38c647c62f | |||
| 757b316b49 | |||
| d05e9ab3ee | |||
| 3f59ed6951 | |||
| 6efb0fee80 | |||
| 2e98fa50de | |||
| 7221ee1843 | |||
| ae0cb5af81 | |||
| 80f38fcd3d | |||
| 70f3d1fdcc | |||
| 4b08a574d8 | |||
| 5aa8a32020 | |||
| b58a04b030 | |||
| e1615f9345 | |||
| ce760eb721 | |||
| 109cd7660a | |||
| 727bd832dc | |||
| 3b93b1fa23 | |||
| 9963731b10 | |||
| 5546401aa4 | |||
| 2f405539fb | |||
| c803e42a76 | |||
| 76c0e6a64b | |||
| ebda6424c7 | |||
| e362515eff | |||
| 31960d1ff8 | |||
| 3bf5560834 | |||
| 4c60fea4c4 | |||
| 0577a63205 | |||
| fd2ccaa747 | |||
| df6b5eac59 | |||
| d64489aed4 | |||
| cb6b5858e5 | |||
| 85e6d7fe00 | |||
| 4b9f55268c | |||
| 90ca266793 |
@@ -113,3 +113,12 @@ secrets/
|
|||||||
keys/
|
keys/
|
||||||
|
|
||||||
ToDo.txt
|
ToDo.txt
|
||||||
|
|
||||||
|
|
||||||
|
# only in development branch
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# psd files from footage
|
||||||
|
footage/*.psd
|
||||||
|
|
||||||
|
icon/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "no-as-a-service"]
|
||||||
|
path = no-as-a-service
|
||||||
|
url = https://github.com/hotheadhacker/no-as-a-service.git
|
||||||
@@ -1,345 +1,177 @@
|
|||||||
# Backend API (V2) Documentation
|
# Borrow System API Documentation
|
||||||
|
|
||||||
This document describes the current backend API routes and their real response shapes, based on the code in `backendV2`.
|
## Overview
|
||||||
|
|
||||||
---
|
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.
|
||||||
|
|
||||||
## Base URLs
|
|
||||||
|
|
||||||
- Frontend: `https://insta.the1s.de`
|
|
||||||
- Backend: `https://backend.insta.the1s.de`
|
|
||||||
- Base path: `https://backend.insta.the1s.de/api`
|
|
||||||
|
|
||||||
Service status: `https://status.the1s.de`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
All **protected** endpoints require an API key as a path parameter `:key`.
|
All requests must include a valid API key in the URL path as the `:key` parameter. API keys are 8-digit numeric strings.
|
||||||
|
|
||||||
Rules for `:key`:
|
## Endpoints
|
||||||
|
|
||||||
- Exactly 8 characters
|
The Base URL for all endpoints is: `https://insta.the1s.de/backend/api`
|
||||||
- Digits only (`^[0-9]{8}$`)
|
|
||||||
|
|
||||||
Example:
|
### Get All Items
|
||||||
|
|
||||||
```http
|
`GET /items/:key`
|
||||||
GET /api/items/12345678
|
|
||||||
```
|
|
||||||
|
|
||||||
On missing / invalid key:
|
Returns all items in the system.
|
||||||
|
|
||||||
- Status: `401 Unauthorized`
|
**Response 200:**
|
||||||
- Body (exact message depends on `authenticate` in `backendV2/services/authentication.js`)
|
|
||||||
|
|
||||||
Auth-related modules:
|
|
||||||
|
|
||||||
- `backendV2/services/authentication.js`
|
|
||||||
- `backendV2/services/database.js`
|
|
||||||
|
|
||||||
Route handlers:
|
|
||||||
|
|
||||||
- `backendV2/routes/api/api.route.js`
|
|
||||||
- `backendV2/routes/api/api.database.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints (Overview)
|
|
||||||
|
|
||||||
1. **Public**
|
|
||||||
|
|
||||||
- `GET /api/all-items` – List all items (no auth; from original docs)
|
|
||||||
|
|
||||||
2. **Items (authenticated)**
|
|
||||||
|
|
||||||
- `GET /api/items/:key` – List all items
|
|
||||||
- `POST /api/change-state/:key/:itemId/:state` – Toggle item safe state
|
|
||||||
|
|
||||||
3. **Loans (authenticated)**
|
|
||||||
- `GET /api/get-loan-by-code/:key/:loan_code` – Get loan by code
|
|
||||||
- `POST /api/set-take-date/:key/:loan_code` – Set “take” date and mark items as out
|
|
||||||
- `POST /api/set-return-date/:key/:loan_code` – Set “return” date and mark items as returned
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1) Items
|
|
||||||
|
|
||||||
### 1.1 Get all items
|
|
||||||
|
|
||||||
**GET** `/api/items/:key`
|
|
||||||
|
|
||||||
Returns all items wrapped in a `data` property.
|
|
||||||
|
|
||||||
- Handler: `getItemsFromDatabaseV2` in `api.database.js`
|
|
||||||
- SQL: `SELECT * FROM items;`
|
|
||||||
|
|
||||||
#### Example request
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET https://backend.insta.the1s.de/api/items/12345678
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful response
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"item_name": "DJI 1er Mikro",
|
"item_name": "Laptop",
|
||||||
"can_borrow_role": 4,
|
"can_borrow_role": 1,
|
||||||
"in_safe": 1,
|
"in_safe": true,
|
||||||
"safe_nr": "01",
|
"safe_nr": 3,
|
||||||
"entry_created_at": "2025-08-19T22:02:16.000Z",
|
"door_key": 101,
|
||||||
"entry_updated_at": "2025-08-19T22:02:16.000Z",
|
"last_borrowed_person": "jdoe",
|
||||||
"last_borrowed_person": "alice",
|
|
||||||
"currently_borrowing": null
|
"currently_borrowing": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error response
|
**Response 500:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to fetch items" }
|
{ "message": "Failed to fetch items" }
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Status codes
|
|
||||||
|
|
||||||
- `200 OK` – success, `data` is an array (possibly empty)
|
|
||||||
- `401 Unauthorized` – invalid / missing key
|
|
||||||
- `500 Internal Server Error` – database error or `success: false` from DB layer
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.2 Toggle item safe state
|
### Change Item Safe State
|
||||||
|
|
||||||
**POST** `/api/change-state/:key/:itemId`
|
`POST /change-state/:key/:itemId`
|
||||||
|
|
||||||
> You do not need this endpoint to set the states of the items when the items are taken out or returned. When you take or return a loan, the item states are set automatically by the loan endpoints. This endpoint is only for manually toggling the `inSafe` state of an item.
|
Toggles the `in_safe` boolean state of an item.
|
||||||
|
|
||||||
Path parameters:
|
**URL Parameters:**
|
||||||
|
|
||||||
- `:key` – API key (8 digits)
|
- **key** - API key
|
||||||
- `:itemId` – numeric `id` of the item
|
- **itemId** - The item's ID
|
||||||
|
|
||||||
Handler in `api.route.js` calls `changeInSafeStateV2(itemId)`, which executes:
|
**Response 200:** Returns on successful toggle.
|
||||||
|
|
||||||
```sql
|
**Response 500:**
|
||||||
UPDATE items SET in_safe = NOT in_safe WHERE id = ?
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://backend.insta.the1s.de/api/change-state/12345678/42
|
|
||||||
```
|
|
||||||
|
|
||||||
(Will toggle `in_safe` for item `42`.)
|
|
||||||
|
|
||||||
#### Successful response (current implementation)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error responses
|
|
||||||
|
|
||||||
Invalid `state` (anything other than `"0"` or `"1"`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "message": "Invalid state value" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Failed update:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to update item state" }
|
{ "message": "Failed to update item state" }
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Status codes
|
|
||||||
|
|
||||||
- `200 OK` – item state toggled
|
|
||||||
- `400 Bad Request` – invalid `state` parameter
|
|
||||||
- `401 Unauthorized` – invalid / missing key
|
|
||||||
- `500 Internal Server Error` – database/update failure or `success: false` from DB layer
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3) Loans
|
### Get Loan by Code
|
||||||
|
|
||||||
### 3.1 Get loan by code
|
`GET /get-loan-by-code/:key/:loan_code`
|
||||||
|
|
||||||
**GET** `/api/get-loan-by-code/:key/:loan_code`
|
Retrieves loan details by its 6-digit loan code.
|
||||||
|
|
||||||
Path parameters:
|
**URL Parameters:**
|
||||||
|
|
||||||
- `:key` – API key
|
- **key** - API key
|
||||||
- `:loan_code` – 6-digit loan code (`^[0-9]{6}$` per DB constraint)
|
- **loan_code** - A 6-digit numeric loan code
|
||||||
|
|
||||||
Database layer (`getLoanByCodeV2`) currently selects:
|
**Response 200:**
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT first_name, returned_date, take_date, lockers
|
|
||||||
FROM loans
|
|
||||||
WHERE loan_code = ?;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example request
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET https://backend.insta.the1s.de/api/get-loan-by-code/12345678/646473
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful response
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"first_name": "Theis",
|
"username": "jdoe",
|
||||||
"returned_date": null,
|
"returned_date": null,
|
||||||
"take_date": "2025-08-25T13:23:00.000Z",
|
"take_date": "2024-01-15T10:30:00.000Z",
|
||||||
"lockers": ["01", "03"]
|
"lockers": [1, 3]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Error response
|
**Response 404:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Loan not found" }
|
{ "message": "Loan not found" }
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Status codes
|
---
|
||||||
|
|
||||||
- `200 OK` – loan found
|
### Set Take Date
|
||||||
- `401 Unauthorized` – invalid / missing key
|
|
||||||
- `404 Not Found` – no matching loan for this `loan_code`
|
`POST /set-take-date/:key/:loan_code`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**URL Parameters:**
|
||||||
|
|
||||||
|
- **key** - API key
|
||||||
|
- **loan_code** - A 6-digit numeric loan code
|
||||||
|
|
||||||
|
**Response 200:** Empty JSON object on success.
|
||||||
|
|
||||||
|
**Response 500:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "message": "Loan not found or already taken" }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** This endpoint will fail if the loan has already been taken or does not exist.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.2 Set take date
|
### Set Return Date
|
||||||
|
|
||||||
**POST** `/api/set-take-date/:key/:loan_code`
|
`POST /set-return-date/:key/:loan_code`
|
||||||
|
|
||||||
Path parameters:
|
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.
|
||||||
|
|
||||||
- `:key` – API key
|
**URL Parameters:**
|
||||||
- `:loan_code` – loan code
|
|
||||||
|
|
||||||
#### Example request
|
- **key** - API key
|
||||||
|
- **loan_code** - A 6-digit numeric loan code
|
||||||
|
|
||||||
```http
|
**Response 200:** Empty JSON object on success.
|
||||||
POST https://backend.insta.the1s.de/api/set-take-date/12345678/646473
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful response
|
**Response 500:**
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "message": "Failed to set take date" }
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Status codes
|
|
||||||
|
|
||||||
- `200 OK` – take date set and items marked as out
|
|
||||||
- `401 Unauthorized` – invalid / missing key
|
|
||||||
- `500 Internal Server Error` – invalid loan, missing items, or DB error / `success: false`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.3 Set return date
|
|
||||||
|
|
||||||
**POST** `/api/set-return-date/:key/:loan_code`
|
|
||||||
|
|
||||||
Path parameters:
|
|
||||||
|
|
||||||
- `:key` – API key
|
|
||||||
- `:loan_code` – loan code
|
|
||||||
|
|
||||||
#### Example request
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://backend.insta.the1s.de/api/set-return-date/12345678/646473
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Successful response (current implementation)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error response
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to set return date" }
|
{ "message": "Failed to set return date" }
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Status codes
|
> **Note:** This endpoint will fail if the loan has already been returned (i.e., `returned_date` is not `NULL`).
|
||||||
|
|
||||||
- `200 OK` – return date set and items marked as returned
|
|
||||||
- `401 Unauthorized` – invalid / missing key
|
|
||||||
- `500 Internal Server Error` – invalid loan, missing items, or DB error / `success: false`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Common Response Shapes
|
### Open Door
|
||||||
|
|
||||||
**Success – list (authenticated items):**
|
`GET /open-door/:key/:doorKey`
|
||||||
|
|
||||||
```json
|
Toggles the safe state of an item identified by its door key and returns the associated safe number.
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
/* array of rows */
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Success – single loan:**
|
**URL Parameters:**
|
||||||
|
|
||||||
|
- **key** - API key
|
||||||
|
- **doorKey** - The door key identifier assigned to an item
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
/* selected loan fields */
|
"safe_nr": 3,
|
||||||
|
"id": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Success – mutations (current code):**
|
**Response 500:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "data": null }
|
{ "message": "Failed to open door" }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Errors:**
|
## Error Handling
|
||||||
|
|
||||||
```json
|
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.
|
||||||
{ "message": "Failed to fetch items" }
|
|
||||||
{ "message": "Failed to update item state" }
|
|
||||||
{ "message": "Invalid state value" }
|
|
||||||
{ "message": "Loan not found" }
|
|
||||||
{ "message": "Failed to set return date" }
|
|
||||||
{ "message": "Failed to set take date" }
|
|
||||||
```
|
|
||||||
|
|
||||||
**HTTP Status Codes:**
|
|
||||||
|
|
||||||
- `200 OK` – operation succeeded
|
|
||||||
- `400 Bad Request` – invalid `state` parameter
|
|
||||||
- `401 Unauthorized` – invalid/missing API key
|
|
||||||
- `404 Not Found` – loan not found
|
|
||||||
- `500 Internal Server Error` – database / server failure or `success: false` from DB layer
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 416 KiB |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18 as builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/icon_borrow-system-frontend_dark.png"
|
||||||
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontendv2</title>
|
<title>Ausleihsystem</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /backend {
|
||||||
|
return 301 /backend/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /backend/ {
|
||||||
|
proxy_pass http://borrow_system-backend_v2:8004/;
|
||||||
|
}
|
||||||
|
|
||||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "v2.1.2 (dev)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^3.28.0",
|
"@chakra-ui/react": "^3.28.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@lottiefiles/dotlottie-react": "^0.19.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"i18next": "^25.6.0",
|
"i18next": "^25.6.0",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shapes-icon lucide-shapes"><path d="M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"/><rect x="3" y="14" width="7" height="7" rx="1"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 420 B |
@@ -12,10 +12,11 @@ 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 { Box, Flex } from "@chakra-ui/react";
|
import { Flex } from "@chakra-ui/react";
|
||||||
import { Footer } from "./components/footer/Footer";
|
import { Footer } from "./components/footer/Footer";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import { ContactPage } from "./pages/ContactPage";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -71,8 +72,8 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Flex direction="column" minH="100vh">
|
<Flex direction="column" minH="100dvh">
|
||||||
<Box as="main" flex="1">
|
<Flex as="main" flex="1" direction="column">
|
||||||
<UserContext.Provider value={user}>
|
<UserContext.Provider value={user}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -80,13 +81,14 @@ 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>
|
||||||
</Box>
|
</Flex>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Flex>
|
</Flex>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Changelog",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"version": "v2.1.0",
|
|
||||||
"date": "2025-10-24",
|
|
||||||
"changes": [
|
|
||||||
{
|
|
||||||
"type": "Hinzugefügt",
|
|
||||||
"text": [
|
|
||||||
"Neue Changelog-Komponente mit zentriertem Layout.",
|
|
||||||
"Unterstützung für mehrsprachige Einträge (Englisch und Deutsch)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Verbessert",
|
|
||||||
"text": [
|
|
||||||
"Performance-Optimierungen beim Laden der Listenansichten.",
|
|
||||||
"Verbesserte Barrierefreiheit durch ARIA-Attribute."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Behoben",
|
|
||||||
"text": [
|
|
||||||
"Fehler bei der Datumsauswahl im Safari-Browser.",
|
|
||||||
"Anzeigeprobleme bei hohen DPI-Einstellungen."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "v2.0.3",
|
|
||||||
"date": "2025-10-10",
|
|
||||||
"changes": [
|
|
||||||
{
|
|
||||||
"type": "Geändert",
|
|
||||||
"text": [
|
|
||||||
"Standard-Timeout für API-Requests auf 10s erhöht."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Sicherheit",
|
|
||||||
"text": [
|
|
||||||
"Abhängigkeiten aktualisiert (kritische CVEs behoben)."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
const STORAGE_KEY = "changelog";
|
|
||||||
|
|
||||||
type ChangeType =
|
|
||||||
| "Hinzugefügt"
|
|
||||||
| "Geändert"
|
|
||||||
| "Behoben"
|
|
||||||
| "Entfernt"
|
|
||||||
| "Verbessert"
|
|
||||||
| "Sicherheit"
|
|
||||||
| "Veraltet"
|
|
||||||
| string;
|
|
||||||
|
|
||||||
type ChangeEntry = {
|
|
||||||
type: ChangeType;
|
|
||||||
text: string | string[]; // aus localStorage kann es eine Liste sein
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChangelogItem = {
|
|
||||||
version?: string;
|
|
||||||
date: string;
|
|
||||||
changes: ChangeEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type StoredChangelog = {
|
|
||||||
title: string;
|
|
||||||
items: ChangelogItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const typeStyles: Record<string, string> = {
|
|
||||||
Hinzugefügt:
|
|
||||||
"bg-emerald-500/15 text-emerald-300 ring-1 ring-inset ring-emerald-500/30",
|
|
||||||
Geändert: "bg-blue-500/15 text-blue-300 ring-1 ring-inset ring-blue-500/30",
|
|
||||||
Behoben: "bg-amber-500/15 text-amber-300 ring-1 ring-inset ring-amber-500/30",
|
|
||||||
Entfernt: "bg-rose-500/15 text-rose-300 ring-1 ring-inset ring-rose-500/30",
|
|
||||||
Verbessert:
|
|
||||||
"bg-indigo-500/15 text-indigo-300 ring-1 ring-inset ring-indigo-500/30",
|
|
||||||
Sicherheit: "bg-red-500/15 text-red-300 ring-1 ring-inset ring-red-500/30",
|
|
||||||
Veraltet: "bg-zinc-700/30 text-zinc-300 ring-1 ring-inset ring-zinc-600/40",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Changelog() {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const [data, setData] = useState<StoredChangelog | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => setMounted(true), []);
|
|
||||||
|
|
||||||
const loadFromStorage = () => {
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
const raw =
|
|
||||||
typeof window !== "undefined"
|
|
||||||
? localStorage.getItem(STORAGE_KEY)
|
|
||||||
: null;
|
|
||||||
if (!raw) {
|
|
||||||
setData(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parsed = JSON.parse(raw) as StoredChangelog;
|
|
||||||
if (!parsed || !Array.isArray(parsed.items)) {
|
|
||||||
throw new Error("Ungültiges Format");
|
|
||||||
}
|
|
||||||
setData(parsed);
|
|
||||||
} catch (e) {
|
|
||||||
setError("Changelog konnte nicht aus localStorage geladen werden.");
|
|
||||||
setData(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadFromStorage();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") setOpen(false);
|
|
||||||
};
|
|
||||||
const onClickOutside = (e: MouseEvent) => {
|
|
||||||
if (cardRef.current && !cardRef.current.contains(e.target as Node)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onStorage = (e: StorageEvent) => {
|
|
||||||
if (e.key === STORAGE_KEY) loadFromStorage();
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
document.addEventListener("mousedown", onClickOutside);
|
|
||||||
window.addEventListener("storage", onStorage);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKey);
|
|
||||||
document.removeEventListener("mousedown", onClickOutside);
|
|
||||||
window.removeEventListener("storage", onStorage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
const title = data?.title ?? "Changelog";
|
|
||||||
const items = data?.items ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-zinc-950 bg-[radial-gradient(60%_60%_at_50%_0%,rgba(99,102,241,0.12),rgba(24,24,27,0))] flex items-center justify-center p-6">
|
|
||||||
<div
|
|
||||||
ref={cardRef}
|
|
||||||
className={[
|
|
||||||
"relative w-full max-w-6xl transition-all duration-300 ease-out",
|
|
||||||
mounted
|
|
||||||
? "opacity-100 translate-y-0 scale-100"
|
|
||||||
: "opacity-0 translate-y-1 scale-[0.99]",
|
|
||||||
].join(" ")}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{/* Gradient border wrapper */}
|
|
||||||
<div className="rounded-2xl p-[1px] bg-gradient-to-b from-zinc-700/60 via-zinc-700/20 to-zinc-800/60 shadow-2xl">
|
|
||||||
{/* Card */}
|
|
||||||
<div className="relative rounded-[calc(theme(borderRadius.2xl)-1px)] border border-zinc-800/70 bg-zinc-900/70 supports-[backdrop-filter]:bg-zinc-900/60 backdrop-blur-xl ring-1 ring-white/10">
|
|
||||||
{/* Accent top line */}
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500/40 to-transparent" />
|
|
||||||
|
|
||||||
{/* Close button */}
|
|
||||||
<button
|
|
||||||
aria-label="Changelog schließen"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className="absolute right-3 top-3 inline-flex h-9 w-9 items-center justify-center rounded-md text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 transition"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-5 w-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.8}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M6 6l12 12M18 6L6 18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<header className="px-10 pt-8 pb-6 border-b border-zinc-800/70">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300 ring-1 ring-inset ring-indigo-500/30">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-5 w-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.6}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-[30px] leading-8 font-semibold text-zinc-100 tracking-[-0.01em]">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-zinc-400">
|
|
||||||
Aktuelle Änderungen und Updates
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Body */}
|
|
||||||
<div className="relative max-h-[78vh] overflow-y-auto">
|
|
||||||
<div className="absolute pointer-events-none inset-x-0 top-0 h-8 bg-gradient-to-b from-zinc-900/70 to-transparent" />
|
|
||||||
<div className="absolute pointer-events-none inset-x-0 bottom-0 h-10 bg-gradient-to-t from-zinc-900/80 to-transparent" />
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="px-10 py-8">
|
|
||||||
<div className="rounded-lg border border-red-900/40 bg-red-900/10 px-4 py-3 text-sm text-red-300">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!error && items.length === 0 && (
|
|
||||||
<div className="px-10 py-16 text-center">
|
|
||||||
<p className="text-zinc-400">
|
|
||||||
Kein Changelog im localStorage gefunden (Key: {STORAGE_KEY}
|
|
||||||
).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul className="divide-y divide-zinc-800/70">
|
|
||||||
{items.map((entry, idx) => (
|
|
||||||
<li
|
|
||||||
key={`${entry.version ?? entry.date}-${idx}`}
|
|
||||||
className="px-10 py-8"
|
|
||||||
>
|
|
||||||
{/* Kopfzeile je Release */}
|
|
||||||
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2">
|
|
||||||
{entry.version && (
|
|
||||||
<span className="inline-flex items-center rounded-md bg-gradient-to-b from-zinc-100 to-zinc-300 text-zinc-900 px-3 py-0.5 text-sm font-semibold shadow-sm">
|
|
||||||
{entry.version}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<time
|
|
||||||
className="text-sm text-zinc-400"
|
|
||||||
dateTime={entry.date}
|
|
||||||
>
|
|
||||||
{new Date(entry.date).toLocaleDateString("de-DE", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "2-digit",
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Zweispaltiges Layout: Typ links, Text rechts (mit schöner Leselänge) */}
|
|
||||||
<dl
|
|
||||||
role="list"
|
|
||||||
className="mt-6 grid grid-cols-1 gap-x-8 gap-y-3 md:grid-cols-[max-content_1fr]"
|
|
||||||
>
|
|
||||||
{entry.changes.map((c, i) => (
|
|
||||||
<div key={i} className="contents">
|
|
||||||
<dt className="md:w-44 md:justify-end md:text-right">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ${
|
|
||||||
typeStyles[c.type] ??
|
|
||||||
"bg-zinc-700/30 text-zinc-300 ring-1 ring-inset ring-zinc-600/40"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{c.type}
|
|
||||||
</span>
|
|
||||||
</dt>
|
|
||||||
|
|
||||||
<dd className="max-w-[74ch] text-[15px] leading-7 text-zinc-200 tracking-[0.005em]">
|
|
||||||
{Array.isArray(c.text) ? (
|
|
||||||
<ul className="ml-4 list-disc marker:text-zinc-500/70 space-y-1.5">
|
|
||||||
{c.text.map((t, k) => (
|
|
||||||
<li key={k} className="break-words">
|
|
||||||
{t}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p className="break-words">{c.text}</p>
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* soft bottom glow */}
|
|
||||||
<div className="pointer-events-none absolute inset-x-12 -bottom-4 h-8 blur-2xl bg-indigo-600/20 rounded-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Alert, Stack, VStack, Spinner, Text, Heading } from "@chakra-ui/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const DeactivatedServices = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [deactivatedServices, setDeactivatedServices] = useState<
|
||||||
|
{ function_name: string }[]
|
||||||
|
>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDeactivatedServices = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/api/users/deactivated-services`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setDeactivatedServices(data);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch deactivated services");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching deactivated services:", error);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDeactivatedServices();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{deactivatedServices.length >= 1 && (
|
||||||
|
<Stack gap="2">
|
||||||
|
<Heading size={"xl"}>{t("deactivated-services")}</Heading>
|
||||||
|
{isLoading && (
|
||||||
|
<VStack colorPalette="teal">
|
||||||
|
<Spinner color="colorPalette.600" />
|
||||||
|
<Text color="colorPalette.600">{t("loading")}</Text>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
{deactivatedServices.length >= 1 &&
|
||||||
|
deactivatedServices.map((item) => (
|
||||||
|
<Alert.Root key={item.function_name} status="warning">
|
||||||
|
<Alert.Indicator />
|
||||||
|
<Alert.Title>
|
||||||
|
{item.function_name} {t("is-deactivated")}
|
||||||
|
</Alert.Title>
|
||||||
|
</Alert.Root>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,101 +1,45 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
|
Image,
|
||||||
Heading,
|
Heading,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
CloseButton,
|
|
||||||
Dialog,
|
|
||||||
Portal,
|
|
||||||
HStack,
|
HStack,
|
||||||
IconButton,
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
Box,
|
Box,
|
||||||
Avatar,
|
Avatar,
|
||||||
Card,
|
|
||||||
Grid,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
CircleUserRound,
|
CircleUserRound,
|
||||||
RotateCcwKey,
|
|
||||||
Code,
|
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
LogOut,
|
LogOut,
|
||||||
CalendarPlus,
|
CalendarPlus,
|
||||||
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";
|
||||||
import MyAlert from "./myChakra/MyAlert";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { UserDialogue } from "./UserDialogue";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const userData = useUserContext();
|
const userData = useUserContext();
|
||||||
console.log(userData);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Error handling states
|
|
||||||
const [isMsg, setIsMsg] = useState(false);
|
|
||||||
const [msgStatus, setMsgStatus] = useState<"error" | "success">("error");
|
|
||||||
const [msgTitle, setMsgTitle] = useState("");
|
|
||||||
const [msgDescription, setMsgDescription] = useState("");
|
|
||||||
|
|
||||||
const [oldPassword, setOldPassword] = useState("");
|
|
||||||
const [newPassword, setNewPassword] = useState("");
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
|
||||||
|
|
||||||
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
||||||
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
||||||
|
|
||||||
// Dialog control
|
|
||||||
const [isPwOpen, setPwOpen] = useState(false);
|
|
||||||
const [userDialog, setUserDialog] = useState(false);
|
const [userDialog, setUserDialog] = useState(false);
|
||||||
|
|
||||||
const changePassword = async () => {
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
setMsgTitle(t("err_pw_change"));
|
|
||||||
setMsgDescription(t("pw_mismatch"));
|
|
||||||
setMsgStatus("error");
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/api/users/change-password`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ oldPassword, newPassword }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setMsgTitle(t("err_pw_change"));
|
|
||||||
setMsgDescription(t("pw_mismatch"));
|
|
||||||
setMsgStatus("error");
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMsgTitle(t("pw_success"));
|
|
||||||
setMsgDescription(t("pw_success_desc"));
|
|
||||||
setMsgStatus("success");
|
|
||||||
setIsMsg(true);
|
|
||||||
|
|
||||||
setOldPassword("");
|
|
||||||
setNewPassword("");
|
|
||||||
setConfirmPassword("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const username = userData.first_name ? userData.first_name : "N/A";
|
const username = userData.first_name ? userData.first_name : "N/A";
|
||||||
const fullname = userData.first_name + " " + userData.last_name;
|
const fullname = userData.first_name + " " + userData.last_name;
|
||||||
const randomColor = [
|
const randomColor = [
|
||||||
@@ -125,6 +69,7 @@ export const Header = () => {
|
|||||||
className="mb-6"
|
className="mb-6"
|
||||||
position="relative"
|
position="relative"
|
||||||
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
|
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
|
||||||
|
marginBottom={1}
|
||||||
>
|
>
|
||||||
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
|
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
|
||||||
<Box
|
<Box
|
||||||
@@ -199,9 +144,9 @@ export const Header = () => {
|
|||||||
value="help"
|
value="help"
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
window.open(
|
window.open(
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
|
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages",
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener,noreferrer"
|
"noopener,noreferrer",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
children={
|
children={
|
||||||
@@ -212,18 +157,12 @@ export const Header = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
value="source-code"
|
value="contact"
|
||||||
onSelect={() =>
|
onSelect={() => navigate("/contact", { replace: true })}
|
||||||
window.open(
|
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system",
|
|
||||||
"_blank",
|
|
||||||
"noopener,noreferrer"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
children={
|
children={
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<Code size={16} />
|
<ContactRound size={16} />
|
||||||
<Text as="span">{t("source-code")}</Text>
|
<Text as="span">{t("contact")}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -253,6 +192,13 @@ export const Header = () => {
|
|||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
|
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
|
||||||
<Flex align="center" justify="space-between" gap={2}>
|
<Flex align="center" justify="space-between" gap={2}>
|
||||||
|
<Image
|
||||||
|
src="/icon_borrow-system-frontend_dark.png"
|
||||||
|
alt="borrow-system logo"
|
||||||
|
boxSize="10"
|
||||||
|
objectFit="contain"
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
<Heading
|
<Heading
|
||||||
size="2xl"
|
size="2xl"
|
||||||
className="tracking-tight text-slate-900 dark:text-slate-100"
|
className="tracking-tight text-slate-900 dark:text-slate-100"
|
||||||
@@ -342,7 +288,7 @@ export const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
|
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
@@ -353,17 +299,15 @@ export const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<Button
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
|
variant={"outline"}
|
||||||
target="_blank"
|
onClick={() => navigate("/contact", { replace: true })}
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Code size={18} />
|
<ContactRound size={18} />
|
||||||
<Text as="span">{t("source-code")}</Text>
|
<Text as="span">{t("contact")}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
|
||||||
|
|
||||||
<Button onClick={logout} variant="outline" colorScheme="red">
|
<Button onClick={logout} variant="outline" colorScheme="red">
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
@@ -376,145 +320,12 @@ export const Header = () => {
|
|||||||
|
|
||||||
{/* User Info Dialoge */}
|
{/* User Info Dialoge */}
|
||||||
{userDialog && (
|
{userDialog && (
|
||||||
<Flex
|
<UserDialogue
|
||||||
position="fixed"
|
setUserDialog={setUserDialog}
|
||||||
inset={0}
|
fullname={fullname}
|
||||||
zIndex={1000}
|
randomColor={randomColor}
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
bg="blackAlpha.400"
|
|
||||||
backdropFilter="blur(6px)"
|
|
||||||
>
|
|
||||||
<Card.Root maxW="sm" w="full" mx={4}>
|
|
||||||
<Card.Header>
|
|
||||||
<Card.Title>
|
|
||||||
<Flex justify="center" align="center" w="100%">
|
|
||||||
<Avatar.Root
|
|
||||||
size={"2xl"}
|
|
||||||
colorPalette={randomColor[Math.floor(Math.random() * 10)]}
|
|
||||||
>
|
|
||||||
<Avatar.Fallback name={fullname} />
|
|
||||||
</Avatar.Root>
|
|
||||||
</Flex>
|
|
||||||
</Card.Title>
|
|
||||||
<Card.Description>{t("user-info-desc")}</Card.Description>
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body>
|
|
||||||
<Stack gap="4" w="full">
|
|
||||||
<Box as="dl">
|
|
||||||
<Grid
|
|
||||||
templateColumns="auto 1fr"
|
|
||||||
rowGap={2}
|
|
||||||
columnGap={4}
|
|
||||||
alignItems="start"
|
|
||||||
>
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("first-name")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.first_name}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("last-name")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.last_name}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("username")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.username}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("role")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.role}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("admin-status")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">
|
|
||||||
{userData.is_admin ? t("yes") : t("no")}
|
|
||||||
</Text>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Button variant="solid" onClick={() => setPwOpen(true)}>
|
|
||||||
<HStack gap={2}>
|
|
||||||
<RotateCcwKey size={18} />
|
|
||||||
<Text as="span">{t("change-password")}</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Card.Body>
|
|
||||||
<Card.Footer justifyContent="flex-end">
|
|
||||||
<Button variant="outline" onClick={() => setUserDialog(false)}>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
|
||||||
</Card.Root>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Passwort-Dialog (kontrolliert) */}
|
|
||||||
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
|
|
||||||
<Portal>
|
|
||||||
<Dialog.Backdrop />
|
|
||||||
<Dialog.Positioner>
|
|
||||||
<Dialog.Content maxW="md">
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>{t("change-password")}</Dialog.Title>
|
|
||||||
</Dialog.Header>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
changePassword();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog.Body>
|
|
||||||
<Stack gap={3}>
|
|
||||||
<PasswordInput
|
|
||||||
value={oldPassword}
|
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
|
||||||
placeholder={t("old-password")}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
placeholder={t("new-password")}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
placeholder={t("confirm-password")}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Dialog.Body>
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Stack w="100%" gap={3}>
|
|
||||||
{isMsg && (
|
|
||||||
<MyAlert
|
|
||||||
status={msgStatus}
|
|
||||||
title={msgTitle}
|
|
||||||
description={msgDescription}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<HStack justify="flex-end" gap={2}>
|
|
||||||
<Dialog.ActionTrigger asChild>
|
|
||||||
<Button variant="outline">{t("cancel")}</Button>
|
|
||||||
</Dialog.ActionTrigger>
|
|
||||||
<Button type="submit" colorScheme="teal">
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</form>
|
|
||||||
<Dialog.CloseTrigger asChild>
|
|
||||||
<CloseButton size="sm" />
|
|
||||||
</Dialog.CloseTrigger>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Positioner>
|
|
||||||
</Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
CloseButton,
|
||||||
|
Dialog,
|
||||||
|
Portal,
|
||||||
|
HStack,
|
||||||
|
Box,
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
import { RotateCcwKey } from "lucide-react";
|
||||||
|
import MyAlert from "./myChakra/MyAlert";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import { useUserContext } from "@/states/Context";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
type UserDialogueProps = {
|
||||||
|
setUserDialog: (value: boolean) => void;
|
||||||
|
fullname: string;
|
||||||
|
randomColor: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserDialogue = (props: UserDialogueProps) => {
|
||||||
|
const userData = useUserContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
// Error handling states
|
||||||
|
const [isMsg, setIsMsg] = useState(false);
|
||||||
|
const [msgStatus, setMsgStatus] = useState<"error" | "success">("error");
|
||||||
|
const [msgTitle, setMsgTitle] = 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 [newPassword, setNewPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
|
||||||
|
// Dialog control
|
||||||
|
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 () => {
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setMsgTitle(t("err_pw_change"));
|
||||||
|
setMsgDescription(t("pw_mismatch"));
|
||||||
|
setMsgStatus("error");
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/api/users/change-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ oldPassword, newPassword }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setMsgTitle(t("err_pw_change"));
|
||||||
|
setMsgDescription(t("pw_mismatch"));
|
||||||
|
setMsgStatus("error");
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMsgTitle(t("pw_success"));
|
||||||
|
setMsgDescription(t("pw_success_desc"));
|
||||||
|
setMsgStatus("success");
|
||||||
|
setIsMsg(true);
|
||||||
|
|
||||||
|
setOldPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
position="fixed"
|
||||||
|
inset={0}
|
||||||
|
zIndex={1000}
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
bg="blackAlpha.400"
|
||||||
|
backdropFilter="blur(6px)"
|
||||||
|
>
|
||||||
|
<Card.Root maxW="sm" w="full" mx={4}>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>
|
||||||
|
<Flex justify="center" align="center" w="100%">
|
||||||
|
<Avatar.Root
|
||||||
|
size={"2xl"}
|
||||||
|
colorPalette={props.randomColor[Math.floor(Math.random() * 10)]}
|
||||||
|
>
|
||||||
|
<Avatar.Fallback name={props.fullname} />
|
||||||
|
</Avatar.Root>
|
||||||
|
</Flex>
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>{t("user-info-desc")}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body>
|
||||||
|
<Stack gap="4" w="full">
|
||||||
|
<Box as="dl">
|
||||||
|
<Grid
|
||||||
|
templateColumns="auto 1fr"
|
||||||
|
rowGap={2}
|
||||||
|
columnGap={4}
|
||||||
|
alignItems="start"
|
||||||
|
>
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("first-name")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.first_name}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("last-name")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.last_name}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("username")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.username}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("role")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.role}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("admin-status")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.is_admin ? t("yes") : t("no")}</Text>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button variant="solid" onClick={() => setPwOpen(true)}>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<RotateCcwKey size={18} />
|
||||||
|
<Text as="span">{t("change-password")}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card.Body>
|
||||||
|
<Card.Footer>
|
||||||
|
<Stack w="100%" gap={3}>
|
||||||
|
{isMsgNAAS && (
|
||||||
|
<MyAlert
|
||||||
|
status={msgStatusNAAS}
|
||||||
|
title={msgTitleNAAS}
|
||||||
|
description={msgDescriptionNAAS}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<HStack justify="flex-end" gap={2} wrap="wrap">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => props.setUserDialog(false)}
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => openNAAS()}>
|
||||||
|
{t("try-naas")}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
{/* Passwort-Dialog */}
|
||||||
|
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
|
||||||
|
<Portal>
|
||||||
|
<Dialog.Backdrop />
|
||||||
|
<Dialog.Positioner>
|
||||||
|
<Dialog.Content maxW="md">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>{t("change-password")}</Dialog.Title>
|
||||||
|
</Dialog.Header>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
changePassword();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Body>
|
||||||
|
<Stack gap={3}>
|
||||||
|
<PasswordInput
|
||||||
|
value={oldPassword}
|
||||||
|
onChange={(e) => setOldPassword(e.target.value)}
|
||||||
|
placeholder={t("old-password")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder={t("new-password")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder={t("confirm-password")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Stack w="100%" gap={3}>
|
||||||
|
{isMsg && (
|
||||||
|
<MyAlert
|
||||||
|
status={msgStatus}
|
||||||
|
title={msgTitle}
|
||||||
|
description={msgDescription}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<HStack justify="flex-end" gap={2}>
|
||||||
|
<Dialog.ActionTrigger asChild>
|
||||||
|
<Button variant="outline">{t("cancel")}</Button>
|
||||||
|
</Dialog.ActionTrigger>
|
||||||
|
<Button type="submit" colorScheme="teal">
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
<Dialog.CloseTrigger asChild>
|
||||||
|
<CloseButton size="sm" />
|
||||||
|
</Dialog.CloseTrigger>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||||
|
|
||||||
|
export const unlockAnimation = () => {
|
||||||
|
return (
|
||||||
|
<DotLottieReact
|
||||||
|
src="https://lottie.host/f839baa1-9c64-44c4-9386-f0e4c87ab208/2Iw1m4k86d.lottie"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const approvalAnimation = () => {
|
||||||
|
return (
|
||||||
|
<DotLottieReact
|
||||||
|
src="https://lottie.host/b7257009-9e3f-43e2-8112-a176f4696e4c/iQxxqAVOGX.lottie"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutAnimation = () => {
|
||||||
|
return (
|
||||||
|
<DotLottieReact
|
||||||
|
src="https://lottie.host/4975758c-de38-4d15-9f74-927709751d32/v8FtKpnD1y.lottie"
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,12 +9,11 @@ export const Footer = () => {
|
|||||||
as="footer"
|
as="footer"
|
||||||
py={4}
|
py={4}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
position="fixed"
|
width="100%"
|
||||||
bottom="0"
|
flexShrink={0}
|
||||||
left="0"
|
fontSize="sm"
|
||||||
right="0"
|
|
||||||
>
|
>
|
||||||
Made with ❤️ by Theis Gaedigk - Year 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"} |
|
||||||
Backend-Version: {info ? info["backend-info"].version : "N/A"}
|
Backend-Version: {info ? info["backend-info"].version : "N/A"}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Field,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Container,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { Header } from "@/components/Header";
|
||||||
|
|
||||||
|
interface Alert {
|
||||||
|
type: "info" | "warning" | "success" | "error" | "neutral";
|
||||||
|
headline: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContactPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [alert, setAlert] = useState<Alert | null>(null);
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
// Logic to send the message
|
||||||
|
const result = await fetch(`${API_BASE}/api/users/contact`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
setAlert({
|
||||||
|
type: "success",
|
||||||
|
headline: t("contactPage_successHeadline"),
|
||||||
|
text: t("contactPage_successText"),
|
||||||
|
});
|
||||||
|
setMessage("");
|
||||||
|
} else if (result.status === 503) {
|
||||||
|
setAlert({
|
||||||
|
type: "error",
|
||||||
|
headline: t("serviceDeactivatedHeadline"),
|
||||||
|
text: t("contactPage_serviceDeactivatedText"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAlert({
|
||||||
|
type: "error",
|
||||||
|
headline: t("contactPage_errorHeadline"),
|
||||||
|
text: t("contactPage_errorText"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
|
<Header />
|
||||||
|
<Field.Root invalid={message === ""}>
|
||||||
|
<Field.Label>
|
||||||
|
<Text>{t("contactPage_messageDescription")}</Text>
|
||||||
|
<Field.RequiredIndicator />
|
||||||
|
</Field.Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t("contactPage_messagePlaceholder")}
|
||||||
|
variant="subtle"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
{message === "" && (
|
||||||
|
<Field.ErrorText>{t("contactPage_messageErrorText")}</Field.ErrorText>
|
||||||
|
)}
|
||||||
|
</Field.Root>
|
||||||
|
{alert && (
|
||||||
|
<Alert.Root status={alert.type}>
|
||||||
|
<Alert.Indicator />
|
||||||
|
<Alert.Content>
|
||||||
|
<Alert.Title>{alert.headline}</Alert.Title>
|
||||||
|
<Alert.Description>{alert.text}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
|
</Alert.Root>
|
||||||
|
)}
|
||||||
|
<Button onClick={sendMessage}>{t("contactPage_sendButton")}</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,8 @@ import { borrowAbleItemsAtom } from "@/states/Atoms";
|
|||||||
import { createLoan } from "@/utils/Fetcher";
|
import { createLoan } from "@/utils/Fetcher";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { approvalAnimation } from "@/components/dotLottie";
|
||||||
|
import { DeactivatedServices } from "@/components/DeactivatedServices";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -27,6 +29,8 @@ export interface User {
|
|||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [showAnimation, setShowAnimation] = useState(false);
|
||||||
|
|
||||||
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
||||||
const [startDate, setStartDate] = useState("");
|
const [startDate, setStartDate] = useState("");
|
||||||
const [endDate, setEndDate] = useState("");
|
const [endDate, setEndDate] = useState("");
|
||||||
@@ -46,13 +50,29 @@ export const HomePage = () => {
|
|||||||
setSelectedItems((prevSelected) =>
|
setSelectedItems((prevSelected) =>
|
||||||
prevSelected.includes(itemId)
|
prevSelected.includes(itemId)
|
||||||
? prevSelected.filter((id) => id !== itemId)
|
? prevSelected.filter((id) => id !== itemId)
|
||||||
: [...prevSelected, itemId]
|
: [...prevSelected, itemId],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showApprovalAnimation = (seconds: number) => {
|
||||||
|
const milliseconds = seconds * 1000;
|
||||||
|
|
||||||
|
setShowAnimation(true);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setShowAnimation(false);
|
||||||
|
}, milliseconds);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{showAnimation && (
|
||||||
|
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||||
|
<div>{approvalAnimation()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
<Header />
|
<Header />
|
||||||
|
<DeactivatedServices />
|
||||||
{isMsg && (
|
{isMsg && (
|
||||||
<MyAlert
|
<MyAlert
|
||||||
status={msgStatus}
|
status={msgStatus}
|
||||||
@@ -108,7 +128,6 @@ export const HomePage = () => {
|
|||||||
}
|
}
|
||||||
setBorrowableItems(response.data);
|
setBorrowableItems(response.data);
|
||||||
setIsMsg(false);
|
setIsMsg(false);
|
||||||
console.log(borrowableItems);
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -159,7 +178,7 @@ export const HomePage = () => {
|
|||||||
maxLength={MAX_CHARACTERS}
|
maxLength={MAX_CHARACTERS}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setNote(
|
setNote(
|
||||||
e.currentTarget.value.slice(0, MAX_CHARACTERS)
|
e.currentTarget.value.slice(0, MAX_CHARACTERS),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -179,16 +198,17 @@ export const HomePage = () => {
|
|||||||
setMsgStatus("error");
|
setMsgStatus("error");
|
||||||
setMsgTitle(response.title || t("error"));
|
setMsgTitle(response.title || t("error"));
|
||||||
setMsgDescription(
|
setMsgDescription(
|
||||||
response.description || t("unknown-error")
|
response.description || t("unknown-error"),
|
||||||
);
|
);
|
||||||
setIsMsg(true);
|
setIsMsg(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
showApprovalAnimation(3);
|
||||||
setMsgStatus("success");
|
setMsgStatus("success");
|
||||||
setMsgTitle(t("success"));
|
setMsgTitle(t("success"));
|
||||||
setMsgDescription(t("loan-success"));
|
setMsgDescription(t("loan-success"));
|
||||||
setIsMsg(true);
|
setIsMsg(true);
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -197,5 +217,6 @@ export const HomePage = () => {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Button,
|
Button,
|
||||||
|
Container,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
import MyAlert from "@/components/myChakra/MyAlert";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Header } from "@/components/Header";
|
||||||
|
|
||||||
export const formatDateTime = (value: string | null | undefined) => {
|
export const formatDateTime = (value: string | null | undefined) => {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
@@ -32,6 +33,7 @@ type Loan = {
|
|||||||
returned_date: string | null;
|
returned_date: string | null;
|
||||||
take_date: string | null;
|
take_date: string | null;
|
||||||
loaned_items_name: string[] | string;
|
loaned_items_name: string[] | string;
|
||||||
|
note: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Device = {
|
type Device = {
|
||||||
@@ -46,7 +48,6 @@ type Device = {
|
|||||||
|
|
||||||
const Landingpage: React.FC = () => {
|
const Landingpage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loans, setLoans] = useState<Loan[]>([]);
|
const [loans, setLoans] = useState<Loan[]>([]);
|
||||||
@@ -59,7 +60,7 @@ const Landingpage: React.FC = () => {
|
|||||||
const setError = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
message: string,
|
message: string,
|
||||||
description: string
|
description: string,
|
||||||
) => {
|
) => {
|
||||||
setIsError(false);
|
setIsError(false);
|
||||||
setErrorStatus(status);
|
setErrorStatus(status);
|
||||||
@@ -78,6 +79,16 @@ const Landingpage: React.FC = () => {
|
|||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (loanRes.status === 503) {
|
||||||
|
setError(
|
||||||
|
"error",
|
||||||
|
t("serviceDeactivatedHeadline"),
|
||||||
|
t("loan_page_serviceDeactivatedText"),
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loanData = await loanRes.json();
|
const loanData = await loanRes.json();
|
||||||
if (Array.isArray(loanData)) {
|
if (Array.isArray(loanData)) {
|
||||||
setLoans(loanData);
|
setLoans(loanData);
|
||||||
@@ -85,7 +96,7 @@ const Landingpage: React.FC = () => {
|
|||||||
setError(
|
setError(
|
||||||
"error",
|
"error",
|
||||||
t("error-by-loading"),
|
t("error-by-loading"),
|
||||||
t("unexpected-date-format_loan")
|
t("unexpected-date-format_loan"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +113,7 @@ const Landingpage: React.FC = () => {
|
|||||||
setError(
|
setError(
|
||||||
"error",
|
"error",
|
||||||
t("error-by-loading"),
|
t("error-by-loading"),
|
||||||
t("unexpected-date-format_device")
|
t("unexpected-date-format_device"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -115,14 +126,8 @@ const Landingpage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
<Heading as="h1" size="lg" mb={2}>
|
<Header />
|
||||||
Matthias-Claudius-Schule Technik
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Button onClick={() => navigate("/", { replace: true })}>
|
|
||||||
{t("back")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Heading as="h2" size="md" mb={4}>
|
<Heading as="h2" size="md" mb={4}>
|
||||||
{t("all-loans")}
|
{t("all-loans")}
|
||||||
@@ -168,6 +173,9 @@ const Landingpage: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>{t("return-date")}</strong>
|
<strong>{t("return-date")}</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>{t("note")}</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
@@ -184,6 +192,7 @@ const Landingpage: React.FC = () => {
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
||||||
|
<Table.Cell>{loan.note}</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
))}
|
))}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
@@ -260,7 +269,7 @@ const Landingpage: React.FC = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,47 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
|||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Navigate, useNavigate } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { 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 from = location.state?.from?.pathname || "/";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn) {
|
if (triggerLogout) {
|
||||||
navigate("/", { replace: true });
|
setShowLogout(true);
|
||||||
window.location.reload(); // Wenn entfernt: Seite bleibt schwarz und muss manuell neu geladen werden
|
window.setTimeout(() => {
|
||||||
|
setShowLogout(false);
|
||||||
|
}, 4500);
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, navigate]);
|
|
||||||
|
if (!isLoggedIn) return;
|
||||||
|
|
||||||
|
// Existing sessions should redirect immediately, fresh logins wait for animation.
|
||||||
|
if (!showAnimation) {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
window.location.reload(); // keeps user context in sync after login
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, [isLoggedIn, showAnimation, navigate, from]);
|
||||||
|
|
||||||
const loginFnc = async (username: string, password: string) => {
|
const loginFnc = async (username: string, password: string) => {
|
||||||
const response = await fetch(`${API_BASE}/api/users/login`, {
|
const response = await fetch(`${API_BASE}/api/users/login`, {
|
||||||
@@ -41,6 +63,8 @@ export const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowAnimation(true);
|
||||||
|
|
||||||
Cookies.set("token", data.token);
|
Cookies.set("token", data.token);
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -61,15 +85,23 @@ export const LoginPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTriggerLogout(false);
|
setTriggerLogout(false);
|
||||||
navigate("/", { replace: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
return <Navigate to="/" 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>
|
||||||
@@ -96,9 +128,17 @@ export const LoginPage = () => {
|
|||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Card.Footer justifyContent="flex-end">
|
<Card.Footer justifyContent="flex-end">
|
||||||
{isError && (
|
{isError && (
|
||||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
<MyAlert
|
||||||
|
status="error"
|
||||||
|
title={errorMsg}
|
||||||
|
description={errorDsc}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => handleLogin()}
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
@@ -113,7 +153,7 @@ export const LoginPage = () => {
|
|||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</form>
|
</form>
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ export const MyLoansPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 503) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("serviceDeactivatedHeadline"));
|
||||||
|
setMsgDescription(t("loan_page_serviceDeactivatedText"));
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setMsgStatus("error");
|
setMsgStatus("error");
|
||||||
setMsgTitle(t("error"));
|
setMsgTitle(t("error"));
|
||||||
setMsgDescription(t("error-fetching-loans"));
|
setMsgDescription(t("error-fetching-loans"));
|
||||||
@@ -84,6 +91,14 @@ export const MyLoansPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
if (res.status === 507) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("error"));
|
||||||
|
setMsgDescription(t("error-deleting-loan-507"));
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setMsgStatus("error");
|
setMsgStatus("error");
|
||||||
setMsgTitle(t("error"));
|
setMsgTitle(t("error"));
|
||||||
setMsgDescription(t("error-deleting-loan"));
|
setMsgDescription(t("error-deleting-loan"));
|
||||||
@@ -112,6 +127,86 @@ export const MyLoansPage = () => {
|
|||||||
return `${d}.${M}.${y} ${h}:${min}`;
|
return `${d}.${M}.${y} ${h}:${min}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTakeAction = async (loanCode: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE}/api/loans/set-take-date/${loanCode}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("error"));
|
||||||
|
setMsgDescription(t("error-take-loan"));
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the loan in state
|
||||||
|
setLoans((prev) =>
|
||||||
|
prev.map((loan) =>
|
||||||
|
loan.loan_code === loanCode
|
||||||
|
? { ...loan, take_date: new Date().toISOString() }
|
||||||
|
: loan,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setMsgStatus("success");
|
||||||
|
setMsgTitle(t("success"));
|
||||||
|
setMsgDescription(t("take-loan-success"));
|
||||||
|
setIsMsg(true);
|
||||||
|
} catch (e) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("error"));
|
||||||
|
setMsgDescription(t("network-error"));
|
||||||
|
setIsMsg(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReturnAction = async (loanCode: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE}/api/loans/set-return-date/${loanCode}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("error"));
|
||||||
|
setMsgDescription(t("error-return-loan"));
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the loan in state
|
||||||
|
setLoans((prev) =>
|
||||||
|
prev.map((loan) =>
|
||||||
|
loan.loan_code === loanCode
|
||||||
|
? { ...loan, returned_date: new Date().toISOString() }
|
||||||
|
: loan,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setMsgStatus("success");
|
||||||
|
setMsgTitle(t("success"));
|
||||||
|
setMsgDescription(t("return-loan-success"));
|
||||||
|
setIsMsg(true);
|
||||||
|
} catch (e) {
|
||||||
|
setMsgStatus("error");
|
||||||
|
setMsgTitle(t("error"));
|
||||||
|
setMsgDescription(t("network-error"));
|
||||||
|
setIsMsg(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
@@ -190,8 +285,33 @@ export const MyLoansPage = () => {
|
|||||||
: "-"}
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{formatDate(loan.take_date)}</Table.Cell>
|
<Table.Cell>
|
||||||
<Table.Cell>{formatDate(loan.returned_date)}</Table.Cell>
|
{loan.take_date ? (
|
||||||
|
formatDate(loan.take_date)
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
colorPalette="teal"
|
||||||
|
onClick={() => handleTakeAction(loan.loan_code)}
|
||||||
|
>
|
||||||
|
{t("take")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{loan.returned_date ? (
|
||||||
|
formatDate(loan.returned_date)
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
colorPalette="blue"
|
||||||
|
onClick={() => handleReturnAction(loan.loan_code)}
|
||||||
|
disabled={!loan.take_date}
|
||||||
|
>
|
||||||
|
{t("return")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>{loan.note}</Table.Cell>
|
<Table.Cell>{loan.note}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Dialog.Root role="alertdialog">
|
<Dialog.Root role="alertdialog">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { API_BASE } from "@/config/api.config";
|
|||||||
|
|
||||||
export const getBorrowableItems = async (
|
export const getBorrowableItems = async (
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string
|
endDate: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
||||||
@@ -17,12 +17,22 @@ 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:
|
||||||
"Ein Fehler ist auf dem Server aufgetreten. Manchmal hilft es, die Seite neu zu laden.",
|
"An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +58,7 @@ export const createLoan = async (
|
|||||||
itemIds: number[],
|
itemIds: number[],
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
note: string | null
|
note: string | null,
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(`${API_BASE}/api/loans/createLoan`, {
|
const response = await fetch(`${API_BASE}/api/loans/createLoan`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -60,6 +70,16 @@ export const createLoan = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 503) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
status: "error",
|
||||||
|
title: "Service deactivated",
|
||||||
|
description:
|
||||||
|
"The loan service is currently deactivated. Please try again later.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
status: "error",
|
status: "error",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
|
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
|
||||||
"optional-note": "Optionale Notiz",
|
"optional-note": "Optionale Notiz",
|
||||||
"note": "Notiz",
|
"note": "Notiz",
|
||||||
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und ändern.",
|
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und das Passwort ändern. Falls Sie weitere Änderungen benötigen, wenden Sie sich bitte an einen Administrator.",
|
||||||
"role": "Rolle",
|
"role": "Rolle",
|
||||||
"admin-status": "Admin-Status",
|
"admin-status": "Admin-Status",
|
||||||
"first-name": "Vorname",
|
"first-name": "Vorname",
|
||||||
@@ -72,5 +72,32 @@
|
|||||||
"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",
|
||||||
"landingpage": "Übersichtsseite"
|
"landingpage": "Übersichtsseite",
|
||||||
|
"contactPage_successHeadline": "Nachricht erfolgreich gesendet",
|
||||||
|
"contactPage_successText": "Vielen Dank, dass Sie uns kontaktiert haben. Wir werden uns so schnell wie möglich bei Ihnen melden.",
|
||||||
|
"contactPage_errorHeadline": "Fehler beim Senden der Nachricht",
|
||||||
|
"contactPage_errorText": "Beim Senden Ihrer Nachricht ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||||
|
"contactPage_sendButton": "Nachricht senden",
|
||||||
|
"contactPage_messageLabel": "Nachricht",
|
||||||
|
"contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...",
|
||||||
|
"contactPage_messageErrorText": "Dieses Feld darf nicht leer sein.",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"take": "Abholen",
|
||||||
|
"return": "Zurückgeben",
|
||||||
|
"serverError": "Serverfehler. Bitte versuchen Sie es später erneut, oder laden Sie die Seite neu.",
|
||||||
|
"take-loan-success": "Ausleihe erfolgreich abgeholt",
|
||||||
|
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
|
||||||
|
"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.",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
|
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
|
||||||
"optional-note": "Optional note",
|
"optional-note": "Optional note",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
"user-info-desc": "Here you can view and edit your personal information.",
|
"user-info-desc": "Here you can view your personal information and change your password. If you need to make further changes, please contact an administrator.",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"admin-status": "Admin status",
|
"admin-status": "Admin status",
|
||||||
"first-name": "First name",
|
"first-name": "First name",
|
||||||
@@ -72,5 +72,32 @@
|
|||||||
"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",
|
||||||
"landingpage": "Overview page"
|
"landingpage": "Overview page",
|
||||||
|
"contactPage_successHeadline": "Message sent successfully",
|
||||||
|
"contactPage_successText": "Thank you for contacting us. We will get back to you as soon as possible.",
|
||||||
|
"contactPage_errorHeadline": "Error sending message",
|
||||||
|
"contactPage_errorText": "An error occurred while sending your message. Please try again later.",
|
||||||
|
"contactPage_sendButton": "Send message",
|
||||||
|
"contactPage_messageLabel": "Message",
|
||||||
|
"contactPage_messagePlaceholder": "Enter your message here...",
|
||||||
|
"contactPage_messageErrorText": "This field cannot be empty.",
|
||||||
|
"contact": "Contact",
|
||||||
|
"serverError": "Server error. Please try again later, or refresh the page.",
|
||||||
|
"take": "Take",
|
||||||
|
"return": "Return",
|
||||||
|
"take-loan-success": "Loan taken successfully",
|
||||||
|
"return-loan-success": "Loan returned successfully",
|
||||||
|
"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.",
|
||||||
|
"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,17 +1,16 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import svgr from "vite-plugin-svgr";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss()],
|
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
allowedHosts: ["insta.the1s.de"],
|
port: 8001,
|
||||||
port: 8101,
|
watch: {
|
||||||
watch: { usePolling: true },
|
usePolling: true,
|
||||||
hmr: {
|
|
||||||
host: "insta.the1s.de",
|
|
||||||
port: 8101,
|
|
||||||
protocol: "wss",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Copyright (c) 2026 Theis Gaedigk
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This source code is not to be copied, modified, or distributed in any form
|
||||||
|
without explicit written permission from the author.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /backend {
|
||||||
|
return 301 /backend/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /backend/ {
|
||||||
|
proxy_pass http://borrow_system-backend_v2:8004/;
|
||||||
|
}
|
||||||
|
|
||||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -3675,12 +3675,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
@@ -4466,9 +4470,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -4904,9 +4908,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib": {
|
"node_modules/minizlib": {
|
||||||
"version": "3.0.2",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||||
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minipass": "^7.1.2"
|
"minipass": "^7.1.2"
|
||||||
@@ -4915,21 +4919,6 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"mkdirp": "dist/cjs/src/bin.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -5307,9 +5296,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.8.2",
|
"version": "7.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
|
||||||
"integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==",
|
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@@ -5329,12 +5318,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.8.2",
|
"version": "7.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
|
||||||
"integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==",
|
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.8.2"
|
"react-router": "7.13.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
@@ -5492,9 +5481,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
@@ -5649,16 +5638,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.4.3",
|
"version": "7.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||||
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
"chownr": "^3.0.0",
|
"chownr": "^3.0.0",
|
||||||
"minipass": "^7.1.2",
|
"minipass": "^7.1.2",
|
||||||
"minizlib": "^3.0.1",
|
"minizlib": "^3.1.0",
|
||||||
"mkdirp": "^3.0.1",
|
|
||||||
"yallist": "^5.0.0"
|
"yallist": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "v1.3.2 (dev)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import UserTable from "../components/UserTable";
|
|||||||
import ItemTable from "../components/ItemTable";
|
import ItemTable from "../components/ItemTable";
|
||||||
import LoanTable from "../components/LoanTable";
|
import LoanTable from "../components/LoanTable";
|
||||||
import APIKeyTable from "@/components/APIKeyTable";
|
import APIKeyTable from "@/components/APIKeyTable";
|
||||||
|
import ServerConfig from "@/components/ServerConfig";
|
||||||
import { MoveLeft } from "lucide-react";
|
import { MoveLeft } from "lucide-react";
|
||||||
|
|
||||||
type DashboardProps = {
|
type DashboardProps = {
|
||||||
@@ -44,6 +45,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
||||||
viewUser={() => setActiveView("User")}
|
viewUser={() => setActiveView("User")}
|
||||||
viewAPI={() => setActiveView("API")}
|
viewAPI={() => setActiveView("API")}
|
||||||
|
viewConfig={() => setActiveView("Server Konfiguration")}
|
||||||
/>
|
/>
|
||||||
<Box flex="1" display="flex" flexDirection="column">
|
<Box flex="1" display="flex" flexDirection="column">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -88,6 +90,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
{activeView === "Ausleihen" && <LoanTable />}
|
{activeView === "Ausleihen" && <LoanTable />}
|
||||||
{activeView === "Gegenstände" && <ItemTable />}
|
{activeView === "Gegenstände" && <ItemTable />}
|
||||||
{activeView === "API" && <APIKeyTable />}
|
{activeView === "API" && <APIKeyTable />}
|
||||||
|
{activeView === "Server Konfiguration" && <ServerConfig />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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("");
|
||||||
@@ -43,8 +44,7 @@ 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>
|
||||||
<Input
|
<PasswordInput
|
||||||
type="password"
|
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type SidebarProps = {
|
|||||||
viewSchliessfaecher: () => void;
|
viewSchliessfaecher: () => void;
|
||||||
viewUser: () => void;
|
viewUser: () => void;
|
||||||
viewAPI: () => void;
|
viewAPI: () => void;
|
||||||
|
viewConfig: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
@@ -16,6 +17,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
viewGegenstaende,
|
viewGegenstaende,
|
||||||
viewUser,
|
viewUser,
|
||||||
viewAPI,
|
viewAPI,
|
||||||
|
viewConfig
|
||||||
}) => {
|
}) => {
|
||||||
const [info, setInfo] = useState<any>(null);
|
const [info, setInfo] = useState<any>(null);
|
||||||
|
|
||||||
@@ -83,6 +85,15 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
>
|
>
|
||||||
API Keys
|
API Keys
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
rounded="md"
|
||||||
|
_hover={{ bg: "gray.700", textDecoration: "none" }}
|
||||||
|
onClick={viewConfig}
|
||||||
|
>
|
||||||
|
Server Konfiguration
|
||||||
|
</Link>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const APIKeyTable: React.FC = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(data);
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError("error", "Failed to fetch items", "There is an error");
|
setError("error", "Failed to fetch items", "There is an error");
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
|
|||||||
<Input id="item_name" placeholder="z.B. Laptop" />
|
<Input id="item_name" placeholder="z.B. Laptop" />
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
<Field.Root>
|
<Field.Root>
|
||||||
<Field.Label>Schließfachnummer (immer zwei Zahlen)</Field.Label>
|
<Field.Label>Schließfachnummer</Field.Label>
|
||||||
<Input id="lockerNumber" placeholder="Nummer 01 - 06" />
|
<Input id="safe_nr" placeholder="Nummer 1 - 6" />
|
||||||
</Field.Root>
|
</Field.Root>
|
||||||
<Field.Root>
|
<Field.Root>
|
||||||
<Field.Label>Ausleih-Berechtigung (Rolle)</Field.Label>
|
<Field.Label>Ausleih-Berechtigung (Rolle)</Field.Label>
|
||||||
@@ -57,17 +57,15 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
|
|||||||
(document.getElementById("can_borrow_role") as HTMLInputElement)
|
(document.getElementById("can_borrow_role") as HTMLInputElement)
|
||||||
?.value
|
?.value
|
||||||
);
|
);
|
||||||
const lockerValue = (
|
const safeNrValue = (
|
||||||
document.getElementById("lockerNumber") as HTMLInputElement
|
document.getElementById("safe_nr") as HTMLInputElement
|
||||||
)?.value.trim();
|
)?.value.trim();
|
||||||
|
|
||||||
const lockerNumber =
|
const safeNr = safeNrValue === "" ? null : safeNrValue;
|
||||||
lockerValue === "" ? null : Number(lockerValue);
|
|
||||||
|
|
||||||
if (!name || Number.isNaN(role)) return;
|
if (!name || Number.isNaN(role)) return;
|
||||||
if (lockerNumber !== null && Number.isNaN(lockerNumber)) return;
|
|
||||||
|
|
||||||
const res = await createItem(name, role, lockerNumber);
|
const res = await createItem(name, role, safeNr);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
alert(
|
alert(
|
||||||
"success",
|
"success",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Items = {
|
|||||||
can_borrow_role: string;
|
can_borrow_role: string;
|
||||||
in_safe: boolean;
|
in_safe: boolean;
|
||||||
safe_nr: string;
|
safe_nr: string;
|
||||||
|
door_key: string;
|
||||||
entry_created_at: string;
|
entry_created_at: string;
|
||||||
entry_updated_at: string;
|
entry_updated_at: string;
|
||||||
last_borrowed_person: string | null;
|
last_borrowed_person: string | null;
|
||||||
@@ -68,7 +69,13 @@ const ItemTable: React.FC = () => {
|
|||||||
|
|
||||||
const handleLockerNumberChange = (id: number, value: string) => {
|
const handleLockerNumberChange = (id: number, value: string) => {
|
||||||
setItems((prev) =>
|
setItems((prev) =>
|
||||||
prev.map((it) => (it.id === id ? { ...it, lockerNumber: value } : it))
|
prev.map((it) => (it.id === id ? { ...it, safe_nr: value } : it))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoorKeyChange = (id: number, value: string) => {
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((it) => (it.id === id ? { ...it, door_key: value } : it))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,7 +193,12 @@ const ItemTable: React.FC = () => {
|
|||||||
|
|
||||||
{/* make table fill available width, like UserTable */}
|
{/* make table fill available width, like UserTable */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Table.Root size="sm" striped w="100%" style={{ tableLayout: "auto" }}>
|
<Table.Root
|
||||||
|
size="sm"
|
||||||
|
striped
|
||||||
|
w="100%"
|
||||||
|
style={{ tableLayout: "auto" }} // Spalten nach Content
|
||||||
|
>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
@@ -201,9 +213,12 @@ const ItemTable: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Im Schließfach</strong>
|
<strong>Im Schließfach</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
<strong>Schließfachnummer</strong>
|
<strong>Schließfachnummer</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
|
<strong>Schlüssel</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Eintrag erstellt am</strong>
|
<strong>Eintrag erstellt am</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
@@ -216,7 +231,7 @@ const ItemTable: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Dav **</strong>
|
<strong>Dav **</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
<strong>Aktionen</strong>
|
<strong>Aktionen</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
@@ -290,17 +305,28 @@ const ItemTable: React.FC = () => {
|
|||||||
value={item.safe_nr}
|
value={item.safe_nr}
|
||||||
/>
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
w="max-content"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDoorKeyChange(item.id, e.target.value)
|
||||||
|
}
|
||||||
|
value={item.door_key}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
|
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
|
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
|
||||||
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
||||||
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell whiteSpace="nowrap">
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleEditItems(
|
handleEditItems(
|
||||||
item.id,
|
item.id,
|
||||||
item.item_name,
|
item.item_name,
|
||||||
item.safe_nr,
|
item.safe_nr,
|
||||||
|
item.door_key,
|
||||||
item.can_borrow_role
|
item.can_borrow_role
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
Heading,
|
||||||
|
Switch,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import MyAlert from "./myChakra/MyAlert";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { formatDateTime } from "@/utils/userFuncs";
|
||||||
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
|
||||||
|
type Items = {
|
||||||
|
id: number;
|
||||||
|
function_name: string;
|
||||||
|
active: boolean;
|
||||||
|
entry_created_at: string;
|
||||||
|
updated_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerConfig: React.FC = () => {
|
||||||
|
const [items, setItems] = useState<Items[]>([]);
|
||||||
|
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [errorDsc, setErrorDsc] = useState("");
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [reload, setReload] = useState(false);
|
||||||
|
|
||||||
|
const handleSwitchChange = async (id: number, newState: boolean) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/api/admin/server-config/update?functionName=${encodeURIComponent(
|
||||||
|
items.find((item) => item.id === id)?.function_name || "",
|
||||||
|
)}&active=${newState}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
setReload((prev) => !prev);
|
||||||
|
setError(
|
||||||
|
"success",
|
||||||
|
"Status updated",
|
||||||
|
"The function status was updated successfully.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setError(
|
||||||
|
"error",
|
||||||
|
"Failed to update status",
|
||||||
|
"There is an error updating the function status.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError(
|
||||||
|
"error",
|
||||||
|
"Failed to update status",
|
||||||
|
"There is an error updating the function status.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (
|
||||||
|
status: "error" | "success",
|
||||||
|
message: string,
|
||||||
|
description: string,
|
||||||
|
) => {
|
||||||
|
setIsError(false);
|
||||||
|
setErrorStatus(status);
|
||||||
|
setErrorMessage(message);
|
||||||
|
setErrorDsc(description);
|
||||||
|
setIsError(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE}/api/admin/server-config/all`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
setError("error", "Failed to fetch items", "There is an error");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData().then((data) => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setItems(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading marginBottom={4} size="2xl">
|
||||||
|
Server Konfiguration
|
||||||
|
</Heading>
|
||||||
|
{isError && (
|
||||||
|
<MyAlert
|
||||||
|
status={errorStatus}
|
||||||
|
description={errorDsc}
|
||||||
|
title={errorMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<VStack colorPalette="teal">
|
||||||
|
<Spinner color="colorPalette.600" />
|
||||||
|
<Text color="colorPalette.600">Loading...</Text>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Table.Root size="sm" striped w="100%" style={{ tableLayout: "auto" }}>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
|
<strong>#</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Service Name</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Toggle</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Eintrag erstellt am</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Table.Row key={item.id}>
|
||||||
|
<Table.Cell whiteSpace="nowrap">{item.id}</Table.Cell>
|
||||||
|
<Table.Cell fontFamily="mono">{item.function_name}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Switch.Root
|
||||||
|
checked={item.active}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleSwitchChange(item.id, !item.active)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch.HiddenInput />
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
<Switch.Label />
|
||||||
|
</Switch.Root>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell whiteSpace="nowrap">
|
||||||
|
{formatDateTime(item.entry_created_at)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerConfig;
|
||||||
@@ -85,7 +85,6 @@ const UserTable: React.FC = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await fetchUserData();
|
const data = await fetchUserData();
|
||||||
console.log(data);
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setUsers(data);
|
setUsers(data);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,9 +165,8 @@ export const deleteItem = async (itemId: number) => {
|
|||||||
export const createItem = async (
|
export const createItem = async (
|
||||||
item_name: string,
|
item_name: string,
|
||||||
can_borrow_role: number,
|
can_borrow_role: number,
|
||||||
lockerNumber: number | null
|
lockerNumber: string | null
|
||||||
) => {
|
) => {
|
||||||
console.log(JSON.stringify({ item_name, can_borrow_role, lockerNumber }));
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE}/api/admin/item-data/create-item`,
|
`${API_BASE}/api/admin/item-data/create-item`,
|
||||||
@@ -184,7 +183,7 @@ export const createItem = async (
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
"Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes darf nicht mehrmals vergeben werden.",
|
"Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes und die Schließfachnummer dürfen nicht mehrmals vergeben werden.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -198,9 +197,9 @@ export const handleEditItems = async (
|
|||||||
itemId: number,
|
itemId: number,
|
||||||
item_name: string,
|
item_name: string,
|
||||||
safe_nr: string | null,
|
safe_nr: string | null,
|
||||||
|
door_key: string | null,
|
||||||
can_borrow_role: string
|
can_borrow_role: string
|
||||||
) => {
|
) => {
|
||||||
const newSafeNr = Number(safe_nr || 0);
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE}/api/admin/item-data/edit-item/${itemId}`,
|
`${API_BASE}/api/admin/item-data/edit-item/${itemId}`,
|
||||||
@@ -210,7 +209,7 @@ export const handleEditItems = async (
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ item_name, newSafeNr, can_borrow_role }),
|
body: JSON.stringify({ item_name, safe_nr, door_key, can_borrow_role }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ESNext",
|
"target": "ES2022",
|
||||||
"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 */
|
||||||
@@ -23,14 +24,10 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
/* Chakra / Pfad Aliases */
|
/* Path aliases */
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"ignoreDeprecations": "5.0"
|
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ export default defineConfig({
|
|||||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
allowedHosts: ["admin.insta.the1s.de"],
|
port: 8003,
|
||||||
port: 8103,
|
watch: {
|
||||||
watch: { usePolling: true },
|
usePolling: true,
|
||||||
hmr: {
|
|
||||||
host: "admin.insta.the1s.de",
|
|
||||||
port: 8103,
|
|
||||||
protocol: "wss",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"backend-info": {
|
"backend-info": {
|
||||||
"version": "v2.0"
|
"version": "v2.2 (dev)"
|
||||||
},
|
},
|
||||||
"frontend-info": {
|
"frontend-info": {
|
||||||
"version": "v2.0"
|
"version": "v2.2 (dev)"
|
||||||
},
|
},
|
||||||
"admin-panel-info": {
|
"admin-panel-info": {
|
||||||
"version": "v1.2"
|
"version": "v1.3.2 (dev)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "1.0.0",
|
"version": "v2.1.1 (dev)",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "1.0.0",
|
"version": "v2.1.1 (dev)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.4.1",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"nodemailer": "^7.0.6"
|
"nodemailer": "^7.0.6"
|
||||||
@@ -349,6 +350,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||||
|
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
@@ -527,6 +546,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "backendv2",
|
"name": "backendv2",
|
||||||
"version": "1.0.0",
|
"version": "v2.1.1 (dev)",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.4.1",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"nodemailer": "^7.0.6"
|
"nodemailer": "^7.0.6"
|
||||||
|
|||||||
@@ -32,10 +32,22 @@ export const createItem = async (item_name, can_borrow_role, lockerNumber) => {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editItemById = async (itemId, item_name, can_borrow_role) => {
|
export const editItemById = async (
|
||||||
|
itemId,
|
||||||
|
item_name,
|
||||||
|
can_borrow_role,
|
||||||
|
safe_nr,
|
||||||
|
door_key
|
||||||
|
) => {
|
||||||
|
let newSafeNr;
|
||||||
|
if (safe_nr === null || safe_nr === "") {
|
||||||
|
newSafeNr = null;
|
||||||
|
} else {
|
||||||
|
newSafeNr = safe_nr;
|
||||||
|
}
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE items SET item_name = ?, can_borrow_role = ?, entry_updated_at = NOW() WHERE id = ?",
|
"UPDATE items SET item_name = ?, can_borrow_role = ?, safe_nr = ?, door_key = ?, entry_updated_at = NOW() WHERE id = ?",
|
||||||
[item_name, can_borrow_role, itemId]
|
[item_name, can_borrow_role, newSafeNr, door_key, itemId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import mysql from "mysql2";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql
|
||||||
|
.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
export const getAllFunctions = async () => {
|
||||||
|
const [rows] = await pool.query("SELECT * FROM functions");
|
||||||
|
return { success: true, data: rows };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateFunctionStatus = async (functionName, active) => {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"UPDATE functions SET active = ? WHERE function_name = ?",
|
||||||
|
[active, functionName],
|
||||||
|
);
|
||||||
|
if (result.affectedRows > 0) return { success: true };
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
@@ -18,11 +18,11 @@ export const createUser = async (
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
email,
|
email,
|
||||||
first_name,
|
first_name,
|
||||||
last_name
|
last_name,
|
||||||
) => {
|
) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
[username, role, password, isAdmin, email, first_name, last_name]
|
[username, role, password, isAdmin, email, first_name, last_name],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -34,10 +34,10 @@ export const deleteUserById = async (userId) => {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePassword = async (userId, 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 id = ?",
|
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?",
|
||||||
[newPassword, userId]
|
[newPassword, username],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -49,11 +49,11 @@ export const editUserById = async (
|
|||||||
last_name,
|
last_name,
|
||||||
role,
|
role,
|
||||||
email,
|
email,
|
||||||
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 = ?",
|
||||||
[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 };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -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",
|
||||||
);
|
);
|
||||||
if (result.length > 0) return { success: true, data: result };
|
if (result.length > 0) return { success: true, data: result };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -70,7 +70,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 = ?",
|
||||||
[userId]
|
[userId],
|
||||||
);
|
);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
|
|||||||
@@ -41,11 +41,14 @@ router.post("/create-item", authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
router.post("/edit-item/:id", authenticateAdmin, async (req, res) => {
|
router.post("/edit-item/:id", authenticateAdmin, async (req, res) => {
|
||||||
const itemId = req.params.id;
|
const itemId = req.params.id;
|
||||||
const { item_name, can_borrow_role } = req.body;
|
const { item_name, can_borrow_role, safe_nr, door_key } = req.body;
|
||||||
|
|
||||||
const result = await editItemById(
|
const result = await editItemById(
|
||||||
itemId,
|
itemId,
|
||||||
item_name,
|
item_name,
|
||||||
can_borrow_role
|
can_borrow_role,
|
||||||
|
safe_nr,
|
||||||
|
door_key
|
||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return res.status(200).json({ message: "Item edited successfully" });
|
return res.status(200).json({ message: "Item edited successfully" });
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { authenticateAdmin } from "../../services/authentication.js";
|
||||||
|
const router = express.Router();
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// database funcs import
|
||||||
|
import {
|
||||||
|
getAllFunctions,
|
||||||
|
updateFunctionStatus,
|
||||||
|
} from "./database/serverConfMgmt.database.js";
|
||||||
|
|
||||||
|
// Route to get all functions and their statuses
|
||||||
|
router.get("/all", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await getAllFunctions();
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ data: result.data });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to fetch functions" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ message: "An error occurred", error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to update the status of a function
|
||||||
|
router.post("/update", async (req, res) => {
|
||||||
|
const functionName = req.query.functionName;
|
||||||
|
let active = req.query.active;
|
||||||
|
|
||||||
|
if (active === "false") {
|
||||||
|
active = 0;
|
||||||
|
} else if (active === "true") {
|
||||||
|
active = 1;
|
||||||
|
} else {
|
||||||
|
res.status(406).json({ message: "Got unexpected format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateFunctionStatus(functionName, active);
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ message: "Function status updated successfully" });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to update function status" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -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,50 +42,62 @@ 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 FROM loans WHERE loan_code = ?",
|
"SELECT loaned_items_id, username FROM loans WHERE loan_code = ?",
|
||||||
[loanCode]
|
[loanCode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [owner] = await pool.query(
|
if (items.length === 0)
|
||||||
"SELECT username FROM loans WHERE loan_code = ?",
|
return { success: false, message: "No items found for loan" };
|
||||||
[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 = ?",
|
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
|
||||||
[loanCode]
|
[loanCode],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
if (result.affectedRows === 0) return { success: false };
|
||||||
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) return { success: false };
|
if (items.length === 0)
|
||||||
|
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
|
||||||
@@ -93,18 +105,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 = ?",
|
"UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
|
||||||
[loanCode]
|
[loanCode],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { success: false };
|
return { message: "Failed to set take date", success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoansV2 = async () => {
|
export const getAllLoansV2 = async () => {
|
||||||
@@ -114,3 +126,22 @@ export const getAllLoansV2 = async () => {
|
|||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openDoor = async (doorKey) => {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
||||||
|
[doorKey],
|
||||||
|
);
|
||||||
|
if (result.length > 0) {
|
||||||
|
const [changeItemSate] = await pool.query(
|
||||||
|
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||||
|
[result[0].id],
|
||||||
|
);
|
||||||
|
if (changeItemSate.affectedRows > 0) {
|
||||||
|
return { success: true, data: result[0] };
|
||||||
|
} else {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
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,
|
||||||
setTakeDateV2,
|
setTakeDateV2,
|
||||||
setReturnDateV2,
|
setReturnDateV2,
|
||||||
getLoanByCodeV2,
|
getLoanByCodeV2,
|
||||||
|
openDoor,
|
||||||
} from "./api.database.js";
|
} from "./api.database.js";
|
||||||
|
|
||||||
// Route for API to get all items from the database
|
// Route for API to get all items from the database
|
||||||
@@ -38,6 +42,7 @@ router.post("/change-state/:key/:itemId", authenticate, async (req, res) => {
|
|||||||
router.get(
|
router.get(
|
||||||
"/get-loan-by-code/:key/:loan_code",
|
"/get-loan-by-code/:key/:loan_code",
|
||||||
authenticate,
|
authenticate,
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const loan_code = req.params.loan_code;
|
const loan_code = req.params.loan_code;
|
||||||
const result = await getLoanByCodeV2(loan_code);
|
const result = await getLoanByCodeV2(loan_code);
|
||||||
@@ -46,37 +51,51 @@ 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({ data: result.data });
|
res.status(200).json({});
|
||||||
} 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({ data: result.data });
|
res.status(200).json({});
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set take date" });
|
res.status(500).json({ message: result.message });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Route for API to open a door
|
||||||
|
router.get("/open-door/:key/:doorKey", authenticate, async (req, res) => {
|
||||||
|
const doorKey = req.params.doorKey;
|
||||||
|
|
||||||
|
const result = await openDoor(doorKey);
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ data: result.data });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to open door" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const createLoanInDatabase = async (
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
note,
|
note,
|
||||||
itemIds
|
itemIds,
|
||||||
) => {
|
) => {
|
||||||
if (!username)
|
if (!username)
|
||||||
return { success: false, code: "BAD_REQUEST", message: "Missing username" };
|
return { success: false, code: "BAD_REQUEST", message: "Missing username" };
|
||||||
@@ -52,7 +52,7 @@ export const createLoanInDatabase = async (
|
|||||||
// Ensure all items exist and collect names + lockers
|
// Ensure all items exist and collect names + lockers
|
||||||
const [itemsRows] = await conn.query(
|
const [itemsRows] = await conn.query(
|
||||||
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
|
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
|
||||||
[itemIds]
|
[itemIds],
|
||||||
);
|
);
|
||||||
if (!itemsRows || itemsRows.length !== itemIds.length) {
|
if (!itemsRows || itemsRows.length !== itemIds.length) {
|
||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
@@ -65,16 +65,24 @@ export const createLoanInDatabase = async (
|
|||||||
|
|
||||||
const itemNames = itemIds
|
const itemNames = itemIds
|
||||||
.map(
|
.map(
|
||||||
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name
|
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name,
|
||||||
)
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Build lockers array (unique, only 2-digit strings)
|
// Build lockers array (unique, only 2-digit numbers from safe_nr)
|
||||||
const lockers = [
|
const lockers = [
|
||||||
...new Set(
|
...new Set(
|
||||||
itemsRows
|
itemsRows
|
||||||
.map((r) => r.safe_nr)
|
.map((r) => r.safe_nr)
|
||||||
.filter((sn) => typeof sn === "string" && /^\d{2}$/.test(sn))
|
.filter(
|
||||||
|
(sn) =>
|
||||||
|
sn !== null &&
|
||||||
|
sn !== undefined &&
|
||||||
|
Number.isInteger(Number(sn)) &&
|
||||||
|
Number(sn) >= 0 &&
|
||||||
|
Number(sn) <= 99,
|
||||||
|
)
|
||||||
|
.map((sn) => Number(sn)),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -90,7 +98,7 @@ export const createLoanInDatabase = async (
|
|||||||
AND l.start_date < ?
|
AND l.start_date < ?
|
||||||
AND COALESCE(l.returned_date, l.end_date) > ?
|
AND COALESCE(l.returned_date, l.end_date) > ?
|
||||||
`,
|
`,
|
||||||
[itemIds, end, start]
|
[itemIds, end, start],
|
||||||
);
|
);
|
||||||
if (confRows?.[0]?.conflicts > 0) {
|
if (confRows?.[0]?.conflicts > 0) {
|
||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
@@ -107,7 +115,7 @@ export const createLoanInDatabase = async (
|
|||||||
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
||||||
const [exists] = await conn.query(
|
const [exists] = await conn.query(
|
||||||
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
||||||
[candidate]
|
[candidate],
|
||||||
);
|
);
|
||||||
if (exists.length === 0) {
|
if (exists.length === 0) {
|
||||||
loanCode = candidate;
|
loanCode = candidate;
|
||||||
@@ -138,7 +146,7 @@ export const createLoanInDatabase = async (
|
|||||||
JSON.stringify(itemIds.map((n) => Number(n))),
|
JSON.stringify(itemIds.map((n) => Number(n))),
|
||||||
JSON.stringify(itemNames),
|
JSON.stringify(itemNames),
|
||||||
note,
|
note,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
@@ -181,7 +189,7 @@ export const getLoanInfoWithID = async (loanId) => {
|
|||||||
export const getLoansFromDatabase = async (username) => {
|
export const getLoansFromDatabase = async (username) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
|
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
|
||||||
[username]
|
[username],
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return { success: true, status: true, data: result };
|
return { success: true, status: true, data: result };
|
||||||
@@ -194,7 +202,7 @@ export const getLoansFromDatabase = async (username) => {
|
|||||||
export const getBorrowableItemsFromDatabase = async (
|
export const getBorrowableItemsFromDatabase = async (
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
role = 0
|
role = 0,
|
||||||
) => {
|
) => {
|
||||||
// Overlap if: loan.start < end AND effective_end > start
|
// Overlap if: loan.start < end AND effective_end > start
|
||||||
// effective_end is returned_date if set, otherwise end_date
|
// effective_end is returned_date if set, otherwise end_date
|
||||||
@@ -226,9 +234,26 @@ 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],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -252,3 +277,69 @@ export const getItems = async () => {
|
|||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setReturnDate = async (loanCode) => {
|
||||||
|
const [items] = await pool.query(
|
||||||
|
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [owner] = await pool.query(
|
||||||
|
"SELECT username FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (items.length === 0) return { success: false };
|
||||||
|
|
||||||
|
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||||
|
? items[0].loaned_items_id
|
||||||
|
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||||
|
|
||||||
|
const [setItemStates] = await pool.query(
|
||||||
|
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
|
||||||
|
[owner[0].username, itemIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||||
|
[loanCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setTakeDate = async (loanCode) => {
|
||||||
|
const [items] = await pool.query(
|
||||||
|
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [owner] = await pool.query(
|
||||||
|
"SELECT username FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (items.length === 0) return { success: false };
|
||||||
|
|
||||||
|
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||||
|
? items[0].loaned_items_id
|
||||||
|
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||||
|
|
||||||
|
const [setItemStates] = await pool.query(
|
||||||
|
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
||||||
|
[owner[0].username, itemIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||||
|
[loanCode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const pool = mysql
|
|||||||
export const loginFunc = async (username, password) => {
|
export const loginFunc = async (username, password) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT * FROM users WHERE username = ? AND password = ?",
|
"SELECT * FROM users WHERE username = ? AND password = ?",
|
||||||
[username, password]
|
[username, password],
|
||||||
);
|
);
|
||||||
if (result.length > 0) return { success: true, data: result[0] };
|
if (result.length > 0) return { success: true, data: result[0] };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -40,7 +40,7 @@ export const changePassword = async (username, oldPassword, newPassword) => {
|
|||||||
// get user current password
|
// get user current password
|
||||||
const [user] = await pool.query(
|
const [user] = await pool.query(
|
||||||
"SELECT * FROM users WHERE username = ? AND password = ?",
|
"SELECT * FROM users WHERE username = ? AND password = ?",
|
||||||
[username, oldPassword]
|
[username, oldPassword],
|
||||||
);
|
);
|
||||||
if (user.length === 0) return { success: false };
|
if (user.length === 0) return { success: false };
|
||||||
|
|
||||||
@@ -48,8 +48,16 @@ export const changePassword = async (username, oldPassword, newPassword) => {
|
|||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET password = ? WHERE username = ?",
|
"UPDATE users SET password = ? WHERE username = ?",
|
||||||
[newPassword, username]
|
[newPassword, username],
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDeactivatedServices = async () => {
|
||||||
|
const [rows] = await pool.query("SELECT function_name FROM functions WHERE active = 0;");
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return { success: true, data: rows };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { authenticate, generateToken } from "../../services/authentication.js";
|
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||||
|
import {
|
||||||
|
checkIfServiceIsActive,
|
||||||
|
checkIfServiceIsActive2,
|
||||||
|
} from "../../services/functions.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
const loan_service = "Loan Service";
|
||||||
|
const loan_mailer_service = "Loan Mailer";
|
||||||
|
|
||||||
// database funcs import
|
// database funcs import
|
||||||
import {
|
import {
|
||||||
createLoanInDatabase,
|
createLoanInDatabase,
|
||||||
@@ -13,10 +20,16 @@ import {
|
|||||||
getALLLoans,
|
getALLLoans,
|
||||||
getItems,
|
getItems,
|
||||||
SETdeleteLoanFromDatabase,
|
SETdeleteLoanFromDatabase,
|
||||||
|
setReturnDate,
|
||||||
|
setTakeDate,
|
||||||
} from "./database/loansMgmt.database.js";
|
} from "./database/loansMgmt.database.js";
|
||||||
import { sendMailLoan } from "./services/mailer.js";
|
import { sendMailLoan } from "./services/mailer.js";
|
||||||
|
|
||||||
router.post("/createLoan", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/createLoan",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { items, startDate, endDate, note } = req.body || {};
|
const { items, startDate, endDate, note } = req.body || {};
|
||||||
|
|
||||||
@@ -48,10 +61,11 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
note,
|
note,
|
||||||
itemIds
|
itemIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
||||||
@@ -59,8 +73,11 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
mailInfo.data.loaned_items_name,
|
mailInfo.data.loaned_items_name,
|
||||||
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,
|
||||||
@@ -83,9 +100,14 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
console.error("createLoan error:", err);
|
console.error("createLoan error:", err);
|
||||||
return res.status(500).json({ message: "Failed to create loan" });
|
return res.status(500).json({ message: "Failed to create loan" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.get("/loans", authenticate, async (req, res) => {
|
router.get(
|
||||||
|
"/loans",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const result = await getLoansFromDatabase(req.user.username);
|
const result = await getLoansFromDatabase(req.user.username);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json(result.data);
|
res.status(200).json(result.data);
|
||||||
@@ -94,7 +116,38 @@ router.get("/loans", authenticate, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch loans" });
|
res.status(500).json({ message: "Failed to fetch loans" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/set-return-date/:loan_code",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
|
const loanCode = req.params.loan_code;
|
||||||
|
const result = await setReturnDate(loanCode);
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ data: result.data });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to set return date" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/set-take-date/:loan_code",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
|
const loanCode = req.params.loan_code;
|
||||||
|
const result = await setTakeDate(loanCode);
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json({ data: result.data });
|
||||||
|
} else {
|
||||||
|
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();
|
||||||
@@ -105,26 +158,50 @@ router.get("/all-items", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/delete-loan/:id", authenticate, async (req, res) => {
|
router.delete(
|
||||||
|
"/delete-loan/:id",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const loanId = req.params.id;
|
const loanId = req.params.id;
|
||||||
const result = await SETdeleteLoanFromDatabase(loanId);
|
const result = await SETdeleteLoanFromDatabase(loanId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json({ message: "Loan deleted successfully" });
|
res.status(200).json({ message: "Loan deleted successfully" });
|
||||||
} else {
|
} else {
|
||||||
|
if (result.code === "LOAN_NOT_FOUND") {
|
||||||
|
res.status(404).json({ message: "Loan not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.code === "LOAN_NOT_RETURNED") {
|
||||||
|
res.status(507).json({
|
||||||
|
message: "Cannot delete loan that has not been returned",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(500).json({ message: "Failed to delete loan" });
|
res.status(500).json({ message: "Failed to delete loan" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.get("/all-loans", authenticate, async (req, res) => {
|
router.get(
|
||||||
|
"/all-loans",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const result = await getALLLoans();
|
const result = await getALLLoans();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json(result.data);
|
res.status(200).json(result.data);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to fetch loans" });
|
res.status(500).json({ message: "Failed to fetch loans" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/borrowable-items", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/borrowable-items",
|
||||||
|
checkIfServiceIsActive(loan_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const { startDate, endDate } = req.body || {};
|
const { startDate, endDate } = req.body || {};
|
||||||
if (!startDate || !endDate) {
|
if (!startDate || !endDate) {
|
||||||
return res
|
return res
|
||||||
@@ -135,7 +212,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
|
|||||||
const result = await getBorrowableItemsFromDatabase(
|
const result = await getBorrowableItemsFromDatabase(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
req.user.role
|
req.user.role,
|
||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// return the array directly for consistency with /items
|
// return the array directly for consistency with /items
|
||||||
@@ -145,6 +222,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.json({ message: "Failed to fetch borrowable items" });
|
.json({ message: "Failed to fetch borrowable items" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -2,14 +2,53 @@ import nodemailer from "nodemailer";
|
|||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
const formatDateTime = (value) => {
|
||||||
|
if (value == null) return "N/A";
|
||||||
|
|
||||||
|
const toOut = (d) => {
|
||||||
|
if (!(d instanceof Date) || isNaN(d.getTime())) return "N/A";
|
||||||
|
const dd = String(d.getDate()).padStart(2, "0");
|
||||||
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const yyyy = d.getFullYear();
|
||||||
|
const hh = String(d.getHours()).padStart(2, "0");
|
||||||
|
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||||
|
return `${dd}.${mm}.${yyyy} ${hh}:${mi} Uhr`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value instanceof Date) return toOut(value);
|
||||||
|
if (typeof value === "number") return toOut(new Date(value));
|
||||||
|
|
||||||
|
const s = String(value).trim();
|
||||||
|
|
||||||
|
// Direct pattern: "YYYY-MM-DD[ T]HH:mm[:ss]"
|
||||||
|
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::\d{2})?/);
|
||||||
|
if (m) {
|
||||||
|
const [, y, M, d, h, min] = m;
|
||||||
|
return `${d}.${M}.${y} ${h}:${min} Uhr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO or other parseable formats
|
||||||
|
const dObj = new Date(s);
|
||||||
|
if (!isNaN(dObj.getTime())) return toOut(dObj);
|
||||||
|
|
||||||
|
return "N/A";
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildLoanEmail({
|
||||||
|
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
|
||||||
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
|
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
|
||||||
.map(
|
.map(
|
||||||
(i) =>
|
(i) =>
|
||||||
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`
|
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`,
|
||||||
)
|
)
|
||||||
.join("")}</ul>`
|
.join("")}</ul>`
|
||||||
: "<span style='color:#111827;'>N/A</span>";
|
: "<span style='color:#111827;'>N/A</span>";
|
||||||
@@ -69,21 +108,27 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
|
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
|
||||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||||
startDate
|
startDate,
|
||||||
)}</td>
|
)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
|
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
|
||||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||||
endDate
|
endDate,
|
||||||
)}</td>
|
)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
|
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
|
||||||
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
|
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
|
||||||
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;">
|
||||||
@@ -102,7 +147,14 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
|||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
function buildLoanEmailText({
|
||||||
|
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 [
|
||||||
@@ -113,10 +165,18 @@ function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
|||||||
`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(user, items, startDate, endDate, createdDate) {
|
export function sendMailLoan(
|
||||||
|
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,
|
||||||
@@ -138,11 +198,18 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
createdDate,
|
createdDate,
|
||||||
|
note,
|
||||||
|
}),
|
||||||
|
html: buildLoanEmail({
|
||||||
|
user,
|
||||||
|
items,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
createdDate,
|
||||||
|
note,
|
||||||
}),
|
}),
|
||||||
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Message sent:", info.messageId);
|
console.log("Loan message sent:", info.messageId);
|
||||||
})();
|
})();
|
||||||
console.log("sendMailLoan called");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export function sendMail(username, message) {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.MAIL_HOST,
|
||||||
|
port: process.env.MAIL_PORT,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: process.env.MAIL_USER,
|
||||||
|
pass: process.env.MAIL_PASSWORD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const mailText = `Neue Kontaktanfrage im Ausleihsystem.\n\nBenutzername: ${username}\n\nNachricht:\n${message}`;
|
||||||
|
|
||||||
|
const mailHtml = `<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Neue Nachricht im Ausleihsystem</title>
|
||||||
|
</head>
|
||||||
|
<body style="font-family: Arial, sans-serif; line-height: 1.5; color: #222;">
|
||||||
|
<h2>Neue Nachricht im Ausleihsystem</h2>
|
||||||
|
<p><strong>Benutzername:</strong> ${username}</p>
|
||||||
|
<p><strong>Nachricht:</strong></p>
|
||||||
|
<p style="white-space: pre-line;">${message}</p>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
|
||||||
|
to: process.env.MAIL_SENDEES_CONTACT,
|
||||||
|
subject: "Sie haben eine neue Nachricht!",
|
||||||
|
text: mailText,
|
||||||
|
html: mailHtml,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Contact message sent: %s", info.messageId);
|
||||||
|
})();
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { authenticate, generateToken } from "../../services/authentication.js";
|
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||||
|
import { checkIfServiceIsActive } from "../../services/functions.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// database funcs import
|
const user_frontend_service = "User Frontend";
|
||||||
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
const contact_form_service = "Contact Form Service";
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
// database funcs import
|
||||||
|
import {
|
||||||
|
loginFunc,
|
||||||
|
changePassword,
|
||||||
|
getDeactivatedServices,
|
||||||
|
} from "./database/userMgmt.database.js";
|
||||||
|
import { sendMail } from "./services/mailer_v2.js";
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/login",
|
||||||
|
checkIfServiceIsActive(user_frontend_service),
|
||||||
|
async (req, res) => {
|
||||||
const result = await loginFunc(req.body.username, req.body.password);
|
const result = await loginFunc(req.body.username, req.body.password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const token = await generateToken({
|
const token = await generateToken({
|
||||||
@@ -21,9 +33,14 @@ router.post("/login", async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(401).json({ message: "Invalid credentials" });
|
res.status(401).json({ message: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
router.post("/change-password", authenticate, async (req, res) => {
|
router.post(
|
||||||
|
"/change-password",
|
||||||
|
checkIfServiceIsActive(user_frontend_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
const oldPassword = req.body.oldPassword;
|
const oldPassword = req.body.oldPassword;
|
||||||
const newPassword = req.body.newPassword;
|
const newPassword = req.body.newPassword;
|
||||||
const username = req.user.username;
|
const username = req.user.username;
|
||||||
@@ -33,6 +50,30 @@ router.post("/change-password", authenticate, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to change password" });
|
res.status(500).json({ message: "Failed to change password" });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/contact",
|
||||||
|
checkIfServiceIsActive(contact_form_service),
|
||||||
|
authenticate,
|
||||||
|
async (req, res) => {
|
||||||
|
const message = req.body.message;
|
||||||
|
const username = req.user.username;
|
||||||
|
|
||||||
|
sendMail(username, message);
|
||||||
|
|
||||||
|
res.status(200).json({ message: "Contact message sent successfully" });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get("/deactivated-services", authenticate, async (req, res) => {
|
||||||
|
const result = await getDeactivatedServices();
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json(result.data);
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ message: "Failed to fetch deactivated services" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
USE borrow_system_new;
|
|
||||||
|
|
||||||
-- Reset tables (no FKs defined, so order is safe)
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
TRUNCATE TABLE loans;
|
|
||||||
TRUNCATE TABLE apiKeys;
|
|
||||||
TRUNCATE TABLE items;
|
|
||||||
TRUNCATE TABLE users;
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
-- Users (roles 1–6, plain-text passwords; is_admin is BOOL)
|
|
||||||
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin) VALUES
|
|
||||||
('admin', 'adminpass', 'admin@example.com', 'System', 'Admin', 6, TRUE),
|
|
||||||
('alice', 'alice123', 'alice@example.com', 'Alice', 'Andersen',1, FALSE),
|
|
||||||
('bob', 'bob12345', 'bob@example.com', 'Bob', 'Berg', 2, FALSE),
|
|
||||||
('carol', 'carol123', 'carol@example.com', 'Carol', 'Christensen', 3, FALSE),
|
|
||||||
('dave', 'dave123', 'dave@example.com', 'Dave', 'Dahl', 4, FALSE),
|
|
||||||
('erin', 'erin123', 'erin@example.com', 'Erin', 'Enevoldsen', 5, FALSE),
|
|
||||||
('frank', 'frank123', 'frank@example.com', 'Frank', 'Fisher', 2, FALSE),
|
|
||||||
('grace', 'grace123', 'grace@example.com', 'Grace', 'Gundersen',1, FALSE),
|
|
||||||
('heidi', 'heidi123', 'heidi@example.com', 'Heidi', 'Hansen', 4, FALSE),
|
|
||||||
('tech', 'techpass', 'tech@example.com', 'Tech', 'User', 5, TRUE);
|
|
||||||
|
|
||||||
-- Items (safe_nr is two digits or NULL; matches CHECK and UNIQUE constraint)
|
|
||||||
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, last_borrowed_person, currently_borrowing) VALUES
|
|
||||||
('Laptop A', 2, FALSE, NULL, 'grace', 'bob'),
|
|
||||||
('Laptop B', 2, TRUE, '01', NULL, NULL),
|
|
||||||
('Camera Canon', 3, TRUE, '02', 'erin', NULL),
|
|
||||||
('Microphone Rode', 1, TRUE, '03', 'grace', NULL),
|
|
||||||
('Tripod Manfrotto', 1, TRUE, '04', 'frank', NULL),
|
|
||||||
('Oscilloscope Tek', 4, TRUE, '05', NULL, NULL),
|
|
||||||
('VR Headset', 3, FALSE, NULL, 'heidi', 'carol'),
|
|
||||||
('Keycard Programmer', 6, TRUE, '06', 'admin', NULL);
|
|
||||||
|
|
||||||
-- Loans (JSON strings, 6-digit numeric loan_code per CHECK)
|
|
||||||
-- Assumes the items above have ids 1..8 in insert order
|
|
||||||
INSERT INTO loans (
|
|
||||||
username,
|
|
||||||
lockers,
|
|
||||||
loan_code,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
take_date,
|
|
||||||
returned_date,
|
|
||||||
loaned_items_id,
|
|
||||||
loaned_items_name,
|
|
||||||
deleted,
|
|
||||||
note
|
|
||||||
) VALUES
|
|
||||||
-- Active loan: bob has Laptop A (item id 1, locker "01")
|
|
||||||
('bob',
|
|
||||||
'["01"]',
|
|
||||||
'123456',
|
|
||||||
'2025-11-15 09:00:00',
|
|
||||||
'2025-11-22 17:00:00',
|
|
||||||
'2025-11-15 09:15:00',
|
|
||||||
NULL,
|
|
||||||
'[1]',
|
|
||||||
'["Laptop A"]',
|
|
||||||
FALSE,
|
|
||||||
'Active loan - Laptop A'
|
|
||||||
),
|
|
||||||
-- Returned loan: frank had Tripod Manfrotto (item id 5, locker "04")
|
|
||||||
('frank',
|
|
||||||
'["04"]',
|
|
||||||
'234567',
|
|
||||||
'2025-10-01 10:00:00',
|
|
||||||
'2025-10-07 16:00:00',
|
|
||||||
'2025-10-01 10:05:00',
|
|
||||||
'2025-10-05 15:30:00',
|
|
||||||
'[5]',
|
|
||||||
'["Tripod Manfrotto"]',
|
|
||||||
FALSE,
|
|
||||||
'Completed loan'
|
|
||||||
),
|
|
||||||
-- Future reservation: dave will take Oscilloscope Tek (item id 6, locker "05")
|
|
||||||
('dave',
|
|
||||||
'["05"]',
|
|
||||||
'345678',
|
|
||||||
'2025-12-10 09:00:00',
|
|
||||||
'2025-12-12 17:00:00',
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
'[6]',
|
|
||||||
'["Oscilloscope Tek"]',
|
|
||||||
FALSE,
|
|
||||||
'Reserved'
|
|
||||||
),
|
|
||||||
-- Active loan: carol has VR Headset (item id 7, locker "02")
|
|
||||||
('carol',
|
|
||||||
'["02"]',
|
|
||||||
'456789',
|
|
||||||
'2025-11-10 13:00:00',
|
|
||||||
'2025-11-20 12:00:00',
|
|
||||||
'2025-11-10 13:10:00',
|
|
||||||
NULL,
|
|
||||||
'[7]',
|
|
||||||
'["VR Headset"]',
|
|
||||||
FALSE,
|
|
||||||
'Active loan - VR Headset'
|
|
||||||
),
|
|
||||||
-- Soft-deleted historic loan: grace had Microphone + Tripod (item ids 4,5; lockers "03","04")
|
|
||||||
('grace',
|
|
||||||
'["03","04"]',
|
|
||||||
'567890',
|
|
||||||
'2025-09-01 09:00:00',
|
|
||||||
'2025-09-03 17:00:00',
|
|
||||||
'2025-09-01 09:10:00',
|
|
||||||
'2025-09-03 16:45:00',
|
|
||||||
'[4,5]',
|
|
||||||
'["Microphone Rode","Tripod Manfrotto"]',
|
|
||||||
TRUE,
|
|
||||||
'Canceled/soft-deleted record'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- API keys (8-digit numeric keys per CHECK)
|
|
||||||
INSERT INTO apiKeys (api_key, entry_name, last_used_at) VALUES
|
|
||||||
('12345678', 'CI token', '2025-11-15 08:00:00'),
|
|
||||||
('87654321', 'Local dev', NULL),
|
|
||||||
('00000001', 'Monitoring', '2025-11-10 12:30:00');
|
|
||||||
@@ -37,13 +37,13 @@ CREATE TABLE items (
|
|||||||
item_name varchar(255) NOT NULL UNIQUE,
|
item_name varchar(255) NOT NULL UNIQUE,
|
||||||
can_borrow_role INT NOT NULL,
|
can_borrow_role INT NOT NULL,
|
||||||
in_safe bool NOT NULL DEFAULT true,
|
in_safe bool NOT NULL DEFAULT true,
|
||||||
safe_nr CHAR(2) DEFAULT NULL UNIQUE,
|
safe_nr INT DEFAULT NULL UNIQUE,
|
||||||
|
door_key INT DEFAULT NULL UNIQUE,
|
||||||
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,
|
||||||
last_borrowed_person varchar(255) DEFAULT NULL,
|
last_borrowed_person varchar(255) DEFAULT NULL,
|
||||||
currently_borrowing varchar(255) DEFAULT NULL,
|
currently_borrowing varchar(255) DEFAULT NULL,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id)
|
||||||
CHECK (safe_nr REGEXP '^[0-9]{2}$' OR safe_nr IS NULL)
|
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
CREATE TABLE apiKeys (
|
CREATE TABLE apiKeys (
|
||||||
@@ -55,3 +55,14 @@ CREATE TABLE apiKeys (
|
|||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (api_key REGEXP '^[0-9]{8}$')
|
CHECK (api_key REGEXP '^[0-9]{8}$')
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE functions (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
function_name VARCHAR(500) NOT NULL UNIQUE,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
INSERT INTO functions (function_name) VALUES ("Loan Mailer"), ("Loan Service"), ("Contact Form Service"), ("User Frontend"), ("API")
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import env from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import info from "./info.json" assert { type: "json" };
|
import info from "./info.json" assert { type: "json" };
|
||||||
import { authenticate } from "./services/authentication.js";
|
import { authenticate } from "./services/authentication.js";
|
||||||
|
import { rateLimit } from "express-rate-limit";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const app = express();
|
||||||
|
const port = 8004;
|
||||||
|
const naasURL = process.env.NAAS_URL;
|
||||||
|
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 1 * 60 * 1000, // 1 minute
|
||||||
|
limit: 50, // Limit each IP to 50 requests per `window` (here, per 1 minute).
|
||||||
|
standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
|
||||||
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
|
||||||
|
ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive
|
||||||
|
// store: ... , // Redis, Memcached, etc. See below.
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(limiter);
|
||||||
|
|
||||||
// frontend routes
|
// frontend routes
|
||||||
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
|
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
|
||||||
@@ -14,14 +31,11 @@ import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js";
|
|||||||
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
|
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
|
||||||
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
|
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
|
||||||
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
|
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
|
||||||
|
import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js";
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
import apiRouter from "./routes/api/api.route.js";
|
import apiRouter from "./routes/api/api.route.js";
|
||||||
|
|
||||||
env.config();
|
|
||||||
const app = express();
|
|
||||||
const port = 8004;
|
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
// Body-Parser VOR den Routen registrieren
|
// Body-Parser VOR den Routen registrieren
|
||||||
app.use(express.json({ limit: "10mb" }));
|
app.use(express.json({ limit: "10mb" }));
|
||||||
@@ -37,6 +51,7 @@ app.use("/api/admin/user-data", userDataMgmtRouter);
|
|||||||
app.use("/api/admin/item-data", itemDataMgmtRouter);
|
app.use("/api/admin/item-data", itemDataMgmtRouter);
|
||||||
app.use("/api/admin/api-data", apiDataMgmtRouter);
|
app.use("/api/admin/api-data", apiDataMgmtRouter);
|
||||||
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
|
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
|
||||||
|
app.use("/api/admin/server-config", serverConfMgmtRouter);
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
app.use("/api", apiRouter);
|
app.use("/api", apiRouter);
|
||||||
@@ -47,6 +62,20 @@ app.listen(port, () => {
|
|||||||
console.log(`Server is running on port: ${port}`);
|
console.log(`Server is running on port: ${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/no", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(naasURL);
|
||||||
|
if (!response.ok) {
|
||||||
|
res.status(500).send("Request to no-as-a-service went wrong.");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error communicating with no-as-a-service:", error);
|
||||||
|
res.status(500).send("Error communicating with no-as-a-service.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/verify", authenticate, async (req, res) => {
|
app.get("/verify", authenticate, async (req, res) => {
|
||||||
res.status(200).json({ message: "Token is valid", user: req.user });
|
res.status(200).json({ message: "Token is valid", user: req.user });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { SignJWT, jwtVerify } from "jose";
|
import { SignJWT, jwtVerify } from "jose";
|
||||||
import env from "dotenv";
|
import env from "dotenv";
|
||||||
import { verifyAPIKeyDB } from "./database.js";
|
import { verifyAPIKeyDB } from "./database.js";
|
||||||
|
import { checkIfServiceIsActive2 } from "./functions.js";
|
||||||
env.config();
|
env.config();
|
||||||
|
|
||||||
|
const api_service = "API";
|
||||||
|
const user_frontend_service = "User Frontend";
|
||||||
|
|
||||||
const secretKey = process.env.SECRET_KEY;
|
const secretKey = process.env.SECRET_KEY;
|
||||||
if (!secretKey) {
|
if (!secretKey) {
|
||||||
throw new Error("Missing SECRET_KEY environment variable");
|
throw new Error("Missing SECRET_KEY environment variable");
|
||||||
@@ -45,6 +49,13 @@ export async function authenticate(req, res, next) {
|
|||||||
const apiKey = req.params.key;
|
const apiKey = req.params.key;
|
||||||
|
|
||||||
if (authHeader) {
|
if (authHeader) {
|
||||||
|
const serviceActive = await checkIfServiceIsActive2(user_frontend_service);
|
||||||
|
if (!serviceActive) {
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: "User Frontend is currently unavailable." });
|
||||||
|
}
|
||||||
|
|
||||||
const parts = authHeader.split(" ");
|
const parts = authHeader.split(" ");
|
||||||
const scheme = parts[0];
|
const scheme = parts[0];
|
||||||
const token = parts[1];
|
const token = parts[1];
|
||||||
@@ -61,6 +72,13 @@ export async function authenticate(req, res, next) {
|
|||||||
return res.status(403).json({ message: "Present token invalid" }); // present token invalid
|
return res.status(403).json({ message: "Present token invalid" }); // present token invalid
|
||||||
}
|
}
|
||||||
} else if (apiKey) {
|
} else if (apiKey) {
|
||||||
|
const serviceActive = await checkIfServiceIsActive2(api_service);
|
||||||
|
if (!serviceActive) {
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: "API Service is currently unavailable." });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await verifyAPIKey(apiKey);
|
await verifyAPIKey(apiKey);
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import mysql from "mysql2";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = mysql
|
||||||
|
.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
export function checkIfServiceIsActive(service) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
|
||||||
|
[service],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(503)
|
||||||
|
.json({ message: `-${service}- is currently unavailable.` });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkIfServiceIsActive2(service) {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
|
||||||
|
[service],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog for upcoming version: v2.2
|
||||||
|
|
||||||
|
This update provides some new features for the design. It also contains some improvements and I have also fixed some bugs.
|
||||||
|
|
||||||
|
## New features
|
||||||
|
|
||||||
|
- The overview page now has the note column and is overall better organised
|
||||||
|
- I also addded the regular header to the page
|
||||||
|
- I have added three animations to the Borrow System
|
||||||
|
- I have added a new icon for the frontend, which is now also used in the header and the favicon. It is a dark version of the old icon, which fits better to the overall design. I have made it with Icon Composer. The old icon is still used for the admin panel, which has a light design. (Maybe I will change the admin panel design in the future...)
|
||||||
|
- When you go to your user card (over the user icon in the header) you have a new button "Click me". If you click it, you will get an message... _I am just saying: I have implemented the no-as-a-service code in to my Backend._
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
- I have the error logging for the API route wehre you can take loans improved.
|
||||||
|
- If you try to delete a loan that has not been returned yet, you will get an 507 error code.
|
||||||
|
|
||||||
|
## Fixed bugs
|
||||||
|
|
||||||
|
- Fixed bug: #13
|
||||||
|
- Fixed bug for messaging when server has an error
|
||||||
|
- Fixed footer height
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New version numbers
|
||||||
|
|
||||||
|
**Backend:** v2.2
|
||||||
|
|
||||||
|
**Frontend:** v2.2
|
||||||
|
|
||||||
|
**Admin panel:** v1.3.2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
-[Theis](https://portfolio-theis.de)
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
services:
|
services:
|
||||||
usr-frontend_v2:
|
# usr-frontend_v2:
|
||||||
container_name: borrow_system-usr-frontend
|
# container_name: borrow_system-usr-frontend
|
||||||
build: ./FrontendV2
|
# build: ./FrontendV2
|
||||||
ports:
|
# ports:
|
||||||
- "8101:80"
|
# - "8001:80"
|
||||||
restart: unless-stopped
|
# restart: always
|
||||||
|
|
||||||
admin-frontend:
|
# admin-frontend:
|
||||||
container_name: borrow_system-admin-frontend
|
# container_name: borrow_system-admin-frontend
|
||||||
build: ./admin
|
# build: ./admin
|
||||||
ports:
|
# ports:
|
||||||
- "8103:80"
|
# - "8003:80"
|
||||||
restart: unless-stopped
|
# restart: always
|
||||||
|
|
||||||
backend_v2:
|
backend_v2:
|
||||||
container_name: borrow_system-backend_v2
|
container_name: borrow_system-backend_v2
|
||||||
build: ./backendV2
|
build: ./backendV2
|
||||||
ports:
|
ports:
|
||||||
- "8102:8102"
|
- "8004:8004"
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DB_HOST: mysql_v2
|
DB_HOST: mysql_v2
|
||||||
@@ -26,12 +26,12 @@ services:
|
|||||||
DB_NAME: borrow_system_new
|
DB_NAME: borrow_system_new
|
||||||
depends_on:
|
depends_on:
|
||||||
- mysql_v2
|
- mysql_v2
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
|
|
||||||
mysql_v2:
|
mysql_v2:
|
||||||
container_name: borrow_system-mysql-v2
|
container_name: borrow_system-mysql-v2
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
|
||||||
MYSQL_DATABASE: borrow_system_new
|
MYSQL_DATABASE: borrow_system_new
|
||||||
@@ -42,6 +42,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3310:3306"
|
- "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:
|
mysql-v2-data:
|
||||||
|
|||||||