10 Commits

Author SHA1 Message Date
f2bb326040 Merge branch 'dev' into debian12 2025-11-23 21:40:11 +01:00
8c701db900 changed ports 2025-11-23 21:11:23 +01:00
d1664338a6 add networks configuration for frontend and backend services in docker-compose 2025-11-23 21:06:12 +01:00
1a2624cd9e again 2025-11-23 20:34:19 +01:00
a138190cc6 fixed bugs 2025-11-23 20:32:14 +01:00
993e0cd74b fixed bugs 2025-11-23 20:29:31 +01:00
dab004a7b6 changed docker config 2025-11-23 20:26:27 +01:00
d039336f39 Merge branch 'dev' into debian12 2025-11-23 20:20:41 +01:00
4c781e9325 changed ports 2025-11-23 20:12:41 +01:00
451e6b3646 published v2 2025-11-23 20:11:36 +01:00
36 changed files with 297 additions and 7238 deletions

View File

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

@@ -14,7 +14,7 @@ export const Footer = () => {
left="0" left="0"
right="0" right="0"
> >
Made with by Theis Gaedigk - Class of 2019 at MCS-Bochum Made with by Theis Gaedigk - Year 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"}

View File

@@ -1,15 +1,22 @@
"use client" "use client";
import { ChakraProvider, defaultSystem } from "@chakra-ui/react" import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import { import * as React from "react";
ColorModeProvider, import type { ReactNode } from "react";
type ColorModeProviderProps,
} from "./color-mode"
export function Provider(props: ColorModeProviderProps) { export interface ColorModeProviderProps {
children: React.ReactNode;
}
export function ColorModeProvider({ children }: ColorModeProviderProps) {
// add real color-mode logic here if you need it
return <>{children}</>;
}
export function Provider({ children }: { children: ReactNode }) {
return ( return (
<ChakraProvider value={defaultSystem}> <ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} /> <ColorModeProvider>{children}</ColorModeProvider>
</ChakraProvider> </ChakraProvider>
) );
} }

View File

@@ -1,16 +1,23 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths"; import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()], plugins: [tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 8001, allowedHosts: ["insta.the1s.de"],
watch: { port: 8101,
usePolling: true, watch: { usePolling: true },
hmr: {
host: "insta.the1s.de",
port: 8101,
protocol: "wss",
}, },
}, },
}); });

View File

@@ -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</Field.Label> <Field.Label>Schließfachnummer (immer zwei Zahlen)</Field.Label>
<Input id="safe_nr" placeholder="Nummer 1 - 6" /> <Input id="lockerNumber" placeholder="Nummer 01 - 06" />
</Field.Root> </Field.Root>
<Field.Root> <Field.Root>
<Field.Label>Ausleih-Berechtigung (Rolle)</Field.Label> <Field.Label>Ausleih-Berechtigung (Rolle)</Field.Label>
@@ -57,15 +57,17 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
(document.getElementById("can_borrow_role") as HTMLInputElement) (document.getElementById("can_borrow_role") as HTMLInputElement)
?.value ?.value
); );
const safeNrValue = ( const lockerValue = (
document.getElementById("safe_nr") as HTMLInputElement document.getElementById("lockerNumber") as HTMLInputElement
)?.value.trim(); )?.value.trim();
const safeNr = safeNrValue === "" ? null : safeNrValue; const lockerNumber =
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, safeNr); const res = await createItem(name, role, lockerNumber);
if (res.success) { if (res.success) {
alert( alert(
"success", "success",

View File

@@ -38,7 +38,6 @@ 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;
@@ -73,12 +72,6 @@ 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 = ( const setError = (
status: "error" | "success", status: "error" | "success",
message: string, message: string,
@@ -211,9 +204,6 @@ const ItemTable: React.FC = () => {
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Schließfachnummer</strong> <strong>Schließfachnummer</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Schlüssel</strong>
</Table.ColumnHeader>
<Table.ColumnHeader> <Table.ColumnHeader>
<strong>Eintrag erstellt am</strong> <strong>Eintrag erstellt am</strong>
</Table.ColumnHeader> </Table.ColumnHeader>
@@ -300,16 +290,6 @@ 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>
@@ -321,7 +301,6 @@ const ItemTable: React.FC = () => {
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) {

View File

@@ -165,7 +165,7 @@ 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: string | null lockerNumber: number | null
) => { ) => {
console.log(JSON.stringify({ item_name, can_borrow_role, lockerNumber })); console.log(JSON.stringify({ item_name, can_borrow_role, lockerNumber }));
try { try {
@@ -184,7 +184,7 @@ export const createItem = async (
return { return {
success: false, success: false,
message: message:
"Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes und die Schließfachnummer dürfen nicht mehrmals vergeben werden.", "Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes darf nicht mehrmals vergeben werden.",
}; };
} }
return { success: true }; return { success: true };
@@ -198,7 +198,6 @@ 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
) => { ) => {
try { try {
@@ -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, safe_nr, door_key, can_borrow_role }), body: JSON.stringify({ item_name, safe_nr, can_borrow_role }),
} }
); );
if (!response.ok) { if (!response.ok) {

View File

@@ -8,9 +8,13 @@ export default defineConfig({
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()], plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 8003, allowedHosts: ["admin.insta.the1s.de"],
watch: { port: 8103,
usePolling: true, watch: { usePolling: true },
hmr: {
host: "admin.insta.the1s.de",
port: 8103,
protocol: "wss",
}, },
}, },
}); });

View File

@@ -1,11 +1,11 @@
{ {
"backend-info": { "backend-info": {
"version": "v2.0.1 (dev)" "version": "v2.0"
}, },
"frontend-info": { "frontend-info": {
"version": "v2.0 (dev)" "version": "v2.0"
}, },
"admin-panel-info": { "admin-panel-info": {
"version": "v1.3 (dev)" "version": "v1.2"
} }
} }

View File

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

View File

@@ -41,14 +41,13 @@ 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, safe_nr, door_key } = req.body; const { item_name, can_borrow_role, safe_nr } = req.body;
const result = await editItemById( const result = await editItemById(
itemId, itemId,
item_name, item_name,
can_borrow_role, can_borrow_role,
safe_nr, 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" });

View File

@@ -114,22 +114,3 @@ 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 };
};

View File

@@ -10,7 +10,6 @@ import {
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
@@ -80,16 +79,4 @@ 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; export default router;

View File

@@ -69,20 +69,12 @@ export const createLoanInDatabase = async (
) )
.filter(Boolean); .filter(Boolean);
// Build lockers array (unique, only 2-digit numbers from safe_nr) // Build lockers array (unique, only 2-digit strings)
const lockers = [ const lockers = [
...new Set( ...new Set(
itemsRows itemsRows
.map((r) => r.safe_nr) .map((r) => r.safe_nr)
.filter( .filter((sn) => typeof sn === "string" && /^\d{2}$/.test(sn))
(sn) =>
sn !== null &&
sn !== undefined &&
Number.isInteger(Number(sn)) &&
Number(sn) >= 0 &&
Number(sn) <= 99
)
.map((sn) => Number(sn))
), ),
]; ];

View File

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

BIN
backendV2/scheme.xlsx Normal file

Binary file not shown.

View File

@@ -37,15 +37,20 @@ 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 INT DEFAULT NULL UNIQUE, safe_nr CHAR(2) DEFAULT NULL,
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),
UNIQUE KEY ux_items_safe_nr (safe_nr)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE UNIQUE INDEX ux_items_safe_nr_not_null
ON items (safe_nr)
WHERE safe_nr IS NOT NULL;
CREATE TABLE apiKeys ( CREATE TABLE apiKeys (
id INT NOT NULL AUTO_INCREMENT, id INT NOT NULL AUTO_INCREMENT,
api_key CHAR(8) NOT NULL UNIQUE, api_key CHAR(8) NOT NULL UNIQUE,

View File

@@ -20,7 +20,7 @@ import apiRouter from "./routes/api/api.route.js";
env.config(); env.config();
const app = express(); const app = express();
const port = 8004; const port = 8102;
app.use(cors()); app.use(cors());
// Body-Parser VOR den Routen registrieren // Body-Parser VOR den Routen registrieren

View File

@@ -1,23 +1,32 @@
services: services:
# usr-frontend_v2: usr-frontend_v2:
# container_name: borrow_system-usr-frontend container_name: borrow_system-usr-frontend
# build: ./FrontendV2 networks:
# ports: - proxynet
# - "8001:80" - borrow_system-internal
# restart: unless-stopped build: ./FrontendV2
ports:
- "8101:80"
restart: unless-stopped
# admin-frontend: admin-frontend:
# container_name: borrow_system-admin-frontend container_name: borrow_system-admin-frontend
# build: ./admin networks:
# ports: - proxynet
# - "8003:80" - borrow_system-internal
# restart: unless-stopped build: ./admin
ports:
- "8103:80"
restart: unless-stopped
backend_v2: backend_v2:
container_name: borrow_system-backend_v2 container_name: borrow_system-backend_v2
networks:
- proxynet
- borrow_system-internal
build: ./backendV2 build: ./backendV2
ports: ports:
- "8004:8004" - "8102:8102"
environment: environment:
NODE_ENV: production NODE_ENV: production
DB_HOST: mysql_v2 DB_HOST: mysql_v2
@@ -30,6 +39,8 @@ services:
mysql_v2: mysql_v2:
container_name: borrow_system-mysql-v2 container_name: borrow_system-mysql-v2
networks:
- borrow_system-internal
image: mysql:8.0 image: mysql:8.0
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -45,3 +56,9 @@ services:
volumes: volumes:
mysql-data: mysql-data:
mysql-v2-data: mysql-v2-data:
networks:
proxynet:
external: true
borrow_system-internal:
external: false

41
next-js/.gitignore vendored
View File

@@ -1,41 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,26 +0,0 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,34 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@@ -1,65 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid px-5 transition-colors hover:border-transparent dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}

View File

@@ -1,18 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@@ -1,8 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactCompiler: true,
};
export default nextConfig;

6557
next-js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
{
"name": "next-js",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.0.5",
"react": "19.2.0",
"react-dom": "19.2.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.0.5",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -1,34 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}