Merge branch 'dev' into debian12

This commit is contained in:
2025-11-25 17:11:27 +01:00
12 changed files with 334 additions and 211 deletions

View File

@@ -1,87 +1,88 @@
# 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`.
---
## 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`
**Frontend:** https://insta.the1s.de
**Backend base URL:** `https://backend.insta.the1s.de/api`
---
## Authentication
All **protected** endpoints require an API key as a path parameter `:key`.
All API endpoints require **either**:
Rules for `:key`:
### 1. Bearer Token (JWT)
- Exactly 8 characters
- Digits only (`^[0-9]{8}$`)
Send an `Authorization` header:
```http
Authorization: Bearer <JWT_TOKEN>
```
- Used for user-based access.
- Token must be valid and not expired.
### 2. API Key (for devices / machine-to-machine)
Include an API key in the route as `:key` parameter:
```text
/api/.../:key/...
```
Example:
```http
GET /api/items/12345678
GET /api/items/ABC123
```
On missing / invalid key:
- Status: `401 Unauthorized`
- 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`
Where `ABC123` is your API key.
The API key is validated server-side.
---
## Endpoints (Overview)
## Common Response Codes
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
- `200 OK` Request was successful.
- `401 Unauthorized` Missing or malformed credentials.
- `403 Forbidden` Credentials invalid or not allowed to access this resource.
- `404 Not Found` Resource (e.g., loan) not found.
- `500 Internal Server Error` Unexpected server error.
---
## 1) Items
## Endpoints
### 1.1 Get all items
### 1. Get All Items
**GET** `/api/items/:key`
Returns all items wrapped in a `data` property.
Returns a list of all items.
- Handler: `getItemsFromDatabaseV2` in `api.database.js`
- SQL: `SELECT * FROM items;`
#### Path Parameters
#### Example request
- `:key` API key (string)
#### Authentication
- Either:
- Valid `Authorization: Bearer <token>`
- Or valid `:key` path parameter
#### Request Example
```http
GET https://backend.insta.the1s.de/api/items/12345678
GET /api/items/ABC123 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful response
or
```http
GET /api/items/dummyKey HTTP/1.1
Host: backend.insta.the1s.de
Authorization: Bearer <JWT_TOKEN>
```
#### Successful Response (200)
```json
{
@@ -90,8 +91,9 @@ GET https://backend.insta.the1s.de/api/items/12345678
"id": 1,
"item_name": "DJI 1er Mikro",
"can_borrow_role": 4,
"in_safe": 1,
"safe_nr": "01",
"inSafe": 1,
"safe_nr": 3,
"door_key": "123",
"entry_created_at": "2025-08-19T22:02:16.000Z",
"entry_updated_at": "2025-08-19T22:02:16.000Z",
"last_borrowed_person": "alice",
@@ -101,245 +103,271 @@ GET https://backend.insta.the1s.de/api/items/12345678
}
```
#### Error response
#### Error Response (500)
```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
### 2. Toggle Item Safe State
Toggles `in_safe` between `0` and `1` for a given item.
**Keep in mind that when you return a loan by code, the item states are automatically updated.**
**POST** `/api/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.
#### Path Parameters
Path parameters:
- `:key` API key (string)
- `:itemId` Item ID (integer)
- `:key` API key (8 digits)
- `:itemId` numeric `id` of the item
#### Authentication
Handler in `api.route.js` calls `changeInSafeStateV2(itemId)`, which executes:
- Either Bearer token or `:key` API key.
```sql
UPDATE items SET in_safe = NOT in_safe WHERE id = ?
```
#### Example request
#### Request Example
```http
POST https://backend.insta.the1s.de/api/change-state/12345678/42
POST /api/change-state/ABC123/42 HTTP/1.1
Host: backend.insta.the1s.de
```
(Will toggle `in_safe` for item `42`.)
#### Successful response (current implementation)
#### Successful Response (200)
```json
{
"data": null
"data": {}
}
```
#### Error responses
_(Implementation currently only returns `{ success: true }`, so `data` may be empty.)_
Invalid `state` (anything other than `"0"` or `"1"`):
#### Error Response (500)
```json
{ "message": "Invalid state value" }
{
"message": "Failed to update item state"
}
```
Failed update:
```json
{ "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
### 3. Get Loan by Code
### 3.1 Get loan by code
Fetch loan information by `loan_code`.
**GET** `/api/get-loan-by-code/:key/:loan_code`
Path parameters:
#### Path Parameters
- `:key` API key
- `:loan_code` 6-digit loan code (`^[0-9]{6}$` per DB constraint)
- `:key` API key (string)
- `:loan_code` Loan code (string)
Database layer (`getLoanByCodeV2`) currently selects:
#### Authentication
```sql
SELECT first_name, returned_date, take_date, lockers
FROM loans
WHERE loan_code = ?;
```
- Either Bearer token or `:key` API key.
#### Example request
#### Request Example
```http
GET https://backend.insta.the1s.de/api/get-loan-by-code/12345678/646473
GET /api/get-loan-by-code/ABC123/12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful response
#### Successful Response (200)
```json
{
"data": {
"first_name": "Theis",
"username": "john",
"returned_date": null,
"take_date": "2025-08-25T13:23:00.000Z",
"lockers": ["01", "03"]
"take_date": "2025-01-01T10:00:00.000Z",
"lockers": "[1, 2, 3]"
}
}
```
#### Error response
```json
{ "message": "Loan not found" }
```
#### Status codes
- `200 OK` loan found
- `401 Unauthorized` invalid / missing key
- `404 Not Found` no matching loan for this `loan_code`
---
### 3.2 Set take date
**POST** `/api/set-take-date/:key/:loan_code`
Path parameters:
- `:key` API key
- `:loan_code` loan code
#### Example request
```http
POST https://backend.insta.the1s.de/api/set-take-date/12345678/646473
```
#### Successful response
#### Error Response (404)
```json
{
"data": null
"message": "Loan not found"
}
```
#### 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
### 4. Set Loan Return Date
Sets `returned_date = NOW()` on a loan and updates related items:
- `in_safe = 1`
- `currently_borrowing = NULL`
- `last_borrowed_person = username`
**POST** `/api/set-return-date/:key/:loan_code`
Path parameters:
#### Path Parameters
- `:key` API key
- `:loan_code` loan code
- `:key` API key (string)
- `:loan_code` Loan code (string)
#### Example request
#### Authentication
- Either Bearer token or `:key` API key.
#### Request Example
```http
POST https://backend.insta.the1s.de/api/set-return-date/12345678/646473
POST /api/set-return-date/ABC123/12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful response (current implementation)
#### Successful Response (200)
```json
{
"data": null
"data": {}
}
```
#### Error response
#### Error Response (500)
```json
{ "message": "Failed to set return date" }
{
"message": "Failed to set return date"
}
```
#### Status codes
- `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
### 5. Set Loan Take Date
**Success list (authenticated items):**
Sets `take_date = NOW()` on a loan and updates related items:
- `in_safe = 0`
- `currently_borrowing = username`
**POST** `/api/set-take-date/:key/:loan_code`
#### Path Parameters
- `:key` API key (string)
- `:loan_code` Loan code (string)
#### Authentication
- Either Bearer token or `:key` API key.
#### Request Example
```http
POST /api/set-take-date/ABC123/LOAN-12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json
{
"data": [
/* array of rows */
]
"data": {}
}
```
**Success single loan:**
#### Error Response (500)
```json
{
"message": "Failed to set take date"
}
```
---
### 6. Open Door by Door Key
Looks up an item by its `door_key`, toggles `in_safe`, and returns safe information.
**GET** `/api/open-door/:key/:doorKey`
#### Path Parameters
- `:key` API key (string)
- `:doorKey` Door key/token (string) used by hardware to identify the locker.
#### Authentication
- Either Bearer token or `:key` API key.
#### Request Example
```http
GET /api/open-door/ABC123/123 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json
{
"data": {
/* selected loan fields */
"safe_nr": 5,
"id": 42
}
}
```
**Success mutations (current code):**
#### Error Response (500)
```json
{ "data": null }
{
"message": "Failed to open door"
}
```
**Errors:**
---
## Authentication Error Messages
### Missing credentials
Status: `401`
```json
{ "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" }
{
"message": "Unauthorized"
}
```
**HTTP Status Codes:**
### Invalid JWT
- `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
Status: `403`
```json
{
"message": "Present token invalid"
}
```
### Invalid API Key
Status: `403`
```json
{
"message": "API Key invalid"
}
```
---
## Notes
- All responses are JSON.
- Time fields like `take_date` and `returned_date` are in the format returned by MySQL (usually ISO-like strings).
- `loaned_items_id` in the database is stored as a JSON array string (e.g. `"[1,2,3]"`) and is parsed internally; clients do not interact with this field directly via current endpoints.

View File

@@ -29,8 +29,8 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
<Input id="item_name" placeholder="z.B. Laptop" />
</Field.Root>
<Field.Root>
<Field.Label>Schließfachnummer (immer zwei Zahlen)</Field.Label>
<Input id="safe_nr" placeholder="Nummer 01 - 06" />
<Field.Label>Schließfachnummer</Field.Label>
<Input id="safe_nr" placeholder="Nummer 1 - 6" />
</Field.Root>
<Field.Root>
<Field.Label>Ausleih-Berechtigung (Rolle)</Field.Label>
@@ -64,7 +64,6 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
const safeNr = safeNrValue === "" ? null : safeNrValue;
if (!name || Number.isNaN(role)) return;
if (safeNr !== null && !/^\d{2}$/.test(safeNr)) return;
const res = await createItem(name, role, safeNr);
if (res.success) {

View File

@@ -38,6 +38,7 @@ type Items = {
can_borrow_role: string;
in_safe: boolean;
safe_nr: string;
door_key: string;
entry_created_at: string;
entry_updated_at: string;
last_borrowed_person: string | null;
@@ -72,6 +73,12 @@ const ItemTable: React.FC = () => {
);
};
const handleDoorKeyChange = (id: number, value: string) => {
setItems((prev) =>
prev.map((it) => (it.id === id ? { ...it, door_key: value } : it))
);
};
const setError = (
status: "error" | "success",
message: string,
@@ -204,6 +211,9 @@ const ItemTable: React.FC = () => {
<Table.ColumnHeader>
<strong>Schließfachnummer</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Schlüssel</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag erstellt am</strong>
</Table.ColumnHeader>
@@ -290,6 +300,16 @@ const ItemTable: React.FC = () => {
value={item.safe_nr}
/>
</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_updated_at)}</Table.Cell>
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
@@ -301,6 +321,7 @@ const ItemTable: React.FC = () => {
item.id,
item.item_name,
item.safe_nr,
item.door_key,
item.can_borrow_role
).then((response) => {
if (response.success) {

View File

@@ -184,7 +184,7 @@ export const createItem = async (
return {
success: false,
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 };
@@ -198,6 +198,7 @@ export const handleEditItems = async (
itemId: number,
item_name: string,
safe_nr: string | null,
door_key: string | null,
can_borrow_role: string
) => {
try {
@@ -209,7 +210,7 @@ export const handleEditItems = async (
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`,
},
body: JSON.stringify({ item_name, safe_nr, can_borrow_role }),
body: JSON.stringify({ item_name, safe_nr, door_key, can_borrow_role }),
}
);
if (!response.ok) {

View File

@@ -36,7 +36,8 @@ export const editItemById = async (
itemId,
item_name,
can_borrow_role,
safe_nr
safe_nr,
door_key
) => {
let newSafeNr;
if (safe_nr === null || safe_nr === "") {
@@ -45,8 +46,8 @@ export const editItemById = async (
newSafeNr = safe_nr;
}
const [result] = await pool.query(
"UPDATE items SET item_name = ?, can_borrow_role = ?, safe_nr = ?, entry_updated_at = NOW() WHERE id = ?",
[item_name, can_borrow_role, newSafeNr, itemId]
"UPDATE items SET item_name = ?, can_borrow_role = ?, safe_nr = ?, door_key = ?, entry_updated_at = NOW() WHERE id = ?",
[item_name, can_borrow_role, newSafeNr, door_key, itemId]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };

View File

@@ -41,13 +41,14 @@ router.post("/create-item", authenticateAdmin, async (req, res) => {
router.post("/edit-item/:id", authenticateAdmin, async (req, res) => {
const itemId = req.params.id;
const { item_name, can_borrow_role, safe_nr } = req.body;
const { item_name, can_borrow_role, safe_nr, door_key } = req.body;
const result = await editItemById(
itemId,
item_name,
can_borrow_role,
safe_nr
safe_nr,
door_key
);
if (result.success) {
return res.status(200).json({ message: "Item edited successfully" });

View File

@@ -114,3 +114,22 @@ export const getAllLoansV2 = async () => {
}
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 };
};

View File

@@ -10,6 +10,7 @@ import {
setTakeDateV2,
setReturnDateV2,
getLoanByCodeV2,
openDoor,
} from "./api.database.js";
// Route for API to get all items from the database
@@ -79,4 +80,16 @@ router.post(
}
);
// 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;

View File

@@ -69,12 +69,20 @@ export const createLoanInDatabase = async (
)
.filter(Boolean);
// Build lockers array (unique, only 2-digit strings)
// Build lockers array (unique, only 2-digit numbers from safe_nr)
const lockers = [
...new Set(
itemsRows
.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))
),
];

View File

@@ -2,6 +2,38 @@ import nodemailer from "nodemailer";
import dotenv from "dotenv";
dotenv.config();
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 }) {
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
const itemsList =
@@ -142,7 +174,8 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) {
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
});
console.log("Message sent:", info.messageId);
// debugging logs
// console.log("Message sent:", info.messageId);
})();
console.log("sendMailLoan called");
// console.log("sendMailLoan called");
}

Binary file not shown.

View File

@@ -37,14 +37,13 @@ CREATE TABLE items (
item_name varchar(255) NOT NULL UNIQUE,
can_borrow_role INT NOT NULL,
in_safe bool NOT NULL DEFAULT true,
safe_nr CHAR(2) DEFAULT NULL,
safe_nr INT DEFAULT NULL UNIQUE,
door_key INT DEFAULT NULL UNIQUE,
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_borrowed_person varchar(255) DEFAULT NULL,
currently_borrowing varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
CHECK (safe_nr REGEXP '^[0-9]{2}$' OR safe_nr IS NULL),
UNIQUE KEY ux_items_safe_nr (safe_nr)
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE apiKeys (