diff --git a/Docs/backend_API_docs/README.md b/Docs/backend_API_docs/README.md index eeaa3ed..a18ebb2 100644 --- a/Docs/backend_API_docs/README.md +++ b/Docs/backend_API_docs/README.md @@ -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 +``` + +- 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 ` + - 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 +``` + +#### 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. diff --git a/admin/src/components/AddItemForm.tsx b/admin/src/components/AddItemForm.tsx index e48247a..e88bbd0 100644 --- a/admin/src/components/AddItemForm.tsx +++ b/admin/src/components/AddItemForm.tsx @@ -29,8 +29,8 @@ const AddItemForm: React.FC = ({ onClose, alert }) => { - Schließfachnummer (immer zwei Zahlen) - + Schließfachnummer + Ausleih-Berechtigung (Rolle) @@ -64,7 +64,6 @@ const AddItemForm: React.FC = ({ 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) { diff --git a/admin/src/components/ItemTable.tsx b/admin/src/components/ItemTable.tsx index 16336e0..ccadef0 100644 --- a/admin/src/components/ItemTable.tsx +++ b/admin/src/components/ItemTable.tsx @@ -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 = () => { Schließfachnummer + + Schlüssel + Eintrag erstellt am @@ -290,6 +300,16 @@ const ItemTable: React.FC = () => { value={item.safe_nr} /> + + + handleDoorKeyChange(item.id, e.target.value) + } + value={item.door_key} + /> + {formatDateTime(item.entry_created_at)} {formatDateTime(item.entry_updated_at)} {item.last_borrowed_person} @@ -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) { diff --git a/admin/src/utils/userActions.ts b/admin/src/utils/userActions.ts index 06974df..85ebc25 100644 --- a/admin/src/utils/userActions.ts +++ b/admin/src/utils/userActions.ts @@ -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) { diff --git a/backendV2/routes/admin/database/itemDataMgmt.database.js b/backendV2/routes/admin/database/itemDataMgmt.database.js index 2637f9c..0593bde 100644 --- a/backendV2/routes/admin/database/itemDataMgmt.database.js +++ b/backendV2/routes/admin/database/itemDataMgmt.database.js @@ -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 }; diff --git a/backendV2/routes/admin/itemDataMgmt.route.js b/backendV2/routes/admin/itemDataMgmt.route.js index 77eb6ad..95c1ee5 100644 --- a/backendV2/routes/admin/itemDataMgmt.route.js +++ b/backendV2/routes/admin/itemDataMgmt.route.js @@ -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" }); diff --git a/backendV2/routes/api/api.database.js b/backendV2/routes/api/api.database.js index 507453b..d80365c 100644 --- a/backendV2/routes/api/api.database.js +++ b/backendV2/routes/api/api.database.js @@ -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 }; +}; diff --git a/backendV2/routes/api/api.route.js b/backendV2/routes/api/api.route.js index 4d1dcee..56a289d 100644 --- a/backendV2/routes/api/api.route.js +++ b/backendV2/routes/api/api.route.js @@ -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; diff --git a/backendV2/routes/app/database/loansMgmt.database.js b/backendV2/routes/app/database/loansMgmt.database.js index 4bea657..99cdbaa 100644 --- a/backendV2/routes/app/database/loansMgmt.database.js +++ b/backendV2/routes/app/database/loansMgmt.database.js @@ -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)) ), ]; diff --git a/backendV2/routes/app/services/mailer.js b/backendV2/routes/app/services/mailer.js index 9b03858..337894a 100644 --- a/backendV2/routes/app/services/mailer.js +++ b/backendV2/routes/app/services/mailer.js @@ -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"); } diff --git a/backendV2/scheme.xlsx b/backendV2/scheme.xlsx deleted file mode 100644 index 156790c..0000000 Binary files a/backendV2/scheme.xlsx and /dev/null differ diff --git a/backendV2/schemeV2.sql b/backendV2/schemeV2.sql index 934ed5a..95ff6ed 100644 --- a/backendV2/schemeV2.sql +++ b/backendV2/schemeV2.sql @@ -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 (