40 Commits

Author SHA1 Message Date
b44edb2b1d chnaged config 2026-01-16 17:17:15 +01:00
a72fabc0a0 Merge branch 'dev' into debian12 2026-01-16 17:11:30 +01:00
727bd832dc edited docs 2026-01-16 17:10:47 +01:00
3b93b1fa23 secured admin frontend as well 2026-01-16 17:09:11 +01:00
9963731b10 secured backend -> made backend internal accessable 2026-01-16 17:07:56 +01:00
5546401aa4 refactored dialogue component 2026-01-07 15:44:44 +01:00
2f405539fb changed translation 2026-01-07 15:32:06 +01:00
1406f28f86 Merge branch 'dev' into debian12 2026-01-07 15:06:51 +01:00
c803e42a76 fixed docs api key example 2026-01-07 15:06:28 +01:00
76c0e6a64b added 404 2025-12-05 14:44:09 +01:00
ebda6424c7 fixed design of item table in the admin panel 2025-12-05 10:17:46 +01:00
38d1091e9b Merge branch 'dev' into debian12 2025-11-30 21:23:22 +01:00
f82efecb8c edited docker config 2025-11-30 21:21:21 +01:00
1f12bc8839 t 2025-11-30 21:17:36 +01:00
f19750f6f3 edited port config 2025-11-30 21:12:14 +01:00
808b3fd5c4 Merge branch 'dev' into debian12 2025-11-30 21:07:32 +01:00
e362515eff edited gitignore 2025-11-29 14:56:49 +01:00
31960d1ff8 fixed footer message 2025-11-25 18:02:05 +01:00
0891598eb9 changed version info 2025-11-25 17:30:56 +01:00
39ff02f2e7 Merge branch 'dev' into debian12 2025-11-25 17:11:27 +01:00
3bf5560834 edited docs 2025-11-25 17:09:22 +01:00
4c60fea4c4 fixed bug: mailer did not send email 2025-11-25 16:57:20 +01:00
0577a63205 deleted xls 2025-11-25 16:15:08 +01:00
fd2ccaa747 feat: add door_key field to items and update related logic in forms and database 2025-11-24 17:12:37 +01:00
df6b5eac59 fixed bug and edited version 2025-11-24 16:55:27 +01:00
d64489aed4 edited scheme but still not working 2025-11-24 16:05:56 +01:00
cc67fb4f85 changed version info 2025-11-24 15:35:03 +01:00
75ff4aadc1 fixed color bug 2025-11-24 14:16:55 +01:00
6f998d07c1 Merge branch 'dev' into debian12 2025-11-23 21:52:34 +01:00
cb6b5858e5 refactor: rename lockerNumber to safe_nr and update related logic 2025-11-23 21:52:12 +01:00
f2bb326040 Merge branch 'dev' into debian12 2025-11-23 21:40:11 +01:00
85e6d7fe00 fixed bugs 2025-11-23 21:39:18 +01:00
4b9f55268c updated scheme 2025-11-23 21:25:08 +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
90ca266793 changed docker config 2025-11-23 20:26:49 +01:00
dab004a7b6 changed docker config 2025-11-23 20:26:27 +01:00
26 changed files with 639 additions and 440 deletions

6
.gitignore vendored
View File

@@ -112,4 +112,8 @@ backend/public/uploads/
secrets/ secrets/
keys/ keys/
ToDo.txt ToDo.txt
# only in development branch
next-env.d.ts

View File

@@ -1,27 +1,32 @@
# 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`. **Frontend:** https://insta.the1s.de
**Backend base URL:** `https://insta.the1s.de/backend/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 **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 Send an `Authorization` header:
- 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:
@@ -29,59 +34,48 @@ Example:
GET /api/items/12345678 GET /api/items/12345678
``` ```
On missing / invalid key: Where `12345678` is your API 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`
--- ---
## Endpoints (Overview) ## Common Response Codes
1. **Public** - `200 OK` Request was successful.
- `401 Unauthorized` Missing or malformed credentials.
- `GET /api/all-items` List all items (no auth; from original docs) - `403 Forbidden` Credentials invalid or not allowed to access this resource.
- `404 Not Found` Resource (e.g., loan) not found.
2. **Items (authenticated)** - `500 Internal Server Error` Unexpected server error.
- `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 ## Endpoints
### 1.1 Get all items ### 1. Get All Items
**GET** `/api/items/:key` **GET** `/api/items/:key`
Returns all items wrapped in a `data` property. Returns a list of all items.
- Handler: `getItemsFromDatabaseV2` in `api.database.js` #### Path Parameters
- SQL: `SELECT * FROM items;`
#### Example request - `:key` API key (8-digit number)
#### Authentication
- Either:
- Valid `Authorization: Bearer <token>`
- Or valid `:key` path parameter
#### Request Example
```http ```http
GET https://backend.insta.the1s.de/api/items/12345678 GET /api/items/12345678 HTTP/1.1
Host: backend.insta.the1s.de
Authorization: Bearer <JWT_TOKEN>
``` ```
#### Successful response #### Successful Response (200)
```json ```json
{ {
@@ -90,8 +84,9 @@ GET https://backend.insta.the1s.de/api/items/12345678
"id": 1, "id": 1,
"item_name": "DJI 1er Mikro", "item_name": "DJI 1er Mikro",
"can_borrow_role": 4, "can_borrow_role": 4,
"in_safe": 1, "inSafe": 1,
"safe_nr": "01", "safe_nr": 3,
"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",
@@ -101,245 +96,271 @@ GET https://backend.insta.the1s.de/api/items/12345678
} }
``` ```
#### Error response #### Error 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 ### 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`
> 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 (8-digit number)
- `:itemId` Item ID (integer)
- `:key` API key (8 digits) #### Authentication
- `:itemId` numeric `id` of the item
Handler in `api.route.js` calls `changeInSafeStateV2(itemId)`, which executes: - Either Bearer token or `:key` API key.
```sql #### Request Example
UPDATE items SET in_safe = NOT in_safe WHERE id = ?
```
#### Example request
```http ```http
POST https://backend.insta.the1s.de/api/change-state/12345678/42 POST /api/change-state/12345678/42 HTTP/1.1
Host: backend.insta.the1s.de
``` ```
(Will toggle `in_safe` for item `42`.) #### Successful Response (200)
#### Successful response (current implementation)
```json ```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 ```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` **GET** `/api/get-loan-by-code/:key/:loan_code`
Path parameters: #### Path Parameters
- `:key` API key - `:key` API key (8-digit number)
- `:loan_code` 6-digit loan code (`^[0-9]{6}$` per DB constraint) - `:loan_code` Loan code (string)
Database layer (`getLoanByCodeV2`) currently selects: #### Authentication
```sql - Either Bearer token or `:key` API key.
SELECT first_name, returned_date, take_date, lockers
FROM loans
WHERE loan_code = ?;
```
#### Example request #### Request Example
```http ```http
GET https://backend.insta.the1s.de/api/get-loan-by-code/12345678/646473 GET /api/get-loan-by-code/12345678/12345 HTTP/1.1
Host: backend.insta.the1s.de
``` ```
#### Successful response #### Successful Response (200)
```json ```json
{ {
"data": { "data": {
"first_name": "Theis", "username": "john",
"returned_date": null, "returned_date": null,
"take_date": "2025-08-25T13:23:00.000Z", "take_date": "2025-01-01T10:00:00.000Z",
"lockers": ["01", "03"] "lockers": "[1, 2, 3]"
} }
} }
``` ```
#### Error response #### Error Response (404)
```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
```json ```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` **POST** `/api/set-return-date/:key/:loan_code`
Path parameters: #### Path Parameters
- `:key` API key - `:key` API key (8-digit number)
- `:loan_code` loan code - `:loan_code` Loan code (string)
#### Example request #### Authentication
- Either Bearer token or `:key` API key.
#### Request Example
```http ```http
POST https://backend.insta.the1s.de/api/set-return-date/12345678/646473 POST /api/set-return-date/12345678/12345 HTTP/1.1
Host: backend.insta.the1s.de
``` ```
#### Successful response (current implementation) #### Successful Response (200)
```json ```json
{ {
"data": null "data": {}
} }
``` ```
#### Error response #### Error Response (500)
```json ```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 (8-digit number)
- `:loan_code` Loan code (string)
#### Authentication
- Either Bearer token or `:key` API key.
#### Request Example
```http
POST /api/set-take-date/12345678/LOAN-12345 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ {
"data": [ "data": {}
/* array of rows */
]
} }
``` ```
**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 (8-digit number)
- `: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/12345678/123 HTTP/1.1
Host: backend.insta.the1s.de
```
#### Successful Response (200)
```json ```json
{ {
"data": { "data": {
/* selected loan fields */ "safe_nr": 5,
"id": 42
} }
} }
``` ```
**Success mutations (current code):** #### Error Response (500)
```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": "Failed to update item state" } "message": "Unauthorized"
{ "message": "Invalid state value" } }
{ "message": "Loan not found" }
{ "message": "Failed to set return date" }
{ "message": "Failed to set take date" }
``` ```
**HTTP Status Codes:** ### Invalid JWT
- `200 OK` operation succeeded Status: `403`
- `400 Bad Request` invalid `state` parameter
- `401 Unauthorized` invalid/missing API key ```json
- `404 Not Found` loan not found {
- `500 Internal Server Error` database / server failure or `success: false` from DB layer "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

@@ -1,4 +1,4 @@
FROM node:18 as builder FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app

View File

@@ -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:8102/;
}
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;

View File

@@ -4,25 +4,18 @@ import {
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, Code,
LifeBuoy, LifeBuoy,
LogOut, LogOut,
@@ -33,69 +26,19 @@ import {
} 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 = [
@@ -375,146 +318,7 @@ export const Header = () => {
</Flex> </Flex>
{/* User Info Dialoge */} {/* User Info Dialoge */}
{userDialog && ( {userDialog && <UserDialogue setUserDialog={setUserDialog} fullname={fullname} randomColor={randomColor} />}
<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={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>
); );
}; };

View File

@@ -0,0 +1,220 @@
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 [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
// Dialog control
const [isPwOpen, setPwOpen] = 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("");
};
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 justifyContent="flex-end">
<Button variant="outline" onClick={() => props.setUserDialog(false)}>
{t("cancel")}
</Button>
</Card.Footer>
</Card.Root>
{/* 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>
</Flex>
);
};

View File

@@ -14,7 +14,7 @@ export const Footer = () => {
left="0" left="0"
right="0" 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"}

View File

@@ -1,15 +1,23 @@
"use client" "use client";
import { ChakraProvider, defaultSystem } from "@chakra-ui/react" import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import { import * as React from "react";
ColorModeProvider, import type { ReactNode } from "react";
type ColorModeProviderProps, import { ColorModeProvider as ThemeColorModeProvider } from "./color-mode";
} from "./color-mode"
export function Provider(props: ColorModeProviderProps) { export interface ColorModeProviderProps {
children: React.ReactNode;
}
export function ColorModeProvider({ children }: ColorModeProviderProps) {
// Wrap children with the real color-mode provider
return <ThemeColorModeProvider>{children}</ThemeColorModeProvider>;
}
export function Provider({ children }: { children: ReactNode }) {
return ( return (
<ChakraProvider value={defaultSystem}> <ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} /> <ColorModeProvider>{children}</ColorModeProvider>
</ChakraProvider> </ChakraProvider>
) );
} }

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,8 +1,14 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss()], plugins: [tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
allowedHosts: ["insta.the1s.de"], allowedHosts: ["insta.the1s.de"],

View File

@@ -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:8102/;
}
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;

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 (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",

View File

@@ -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) {

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: number | null lockerNumber: string | 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 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 +198,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 +210,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) {

View File

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

View File

@@ -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 };

View File

@@ -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" });

View File

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

View File

@@ -10,6 +10,7 @@ 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
@@ -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; export default router;

View File

@@ -69,12 +69,20 @@ export const createLoanInDatabase = async (
) )
.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))
), ),
]; ];

View File

@@ -2,6 +2,38 @@ 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 =
@@ -142,7 +174,8 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) {
html: buildLoanEmail({ 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,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 (

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,6 +1,8 @@
services: services:
usr-frontend_v2: usr-frontend_v2:
container_name: borrow_system-usr-frontend container_name: borrow_system-usr-frontend
networks:
- proxynet
build: ./FrontendV2 build: ./FrontendV2
ports: ports:
- "8101:80" - "8101:80"
@@ -8,6 +10,8 @@ services:
admin-frontend: admin-frontend:
container_name: borrow_system-admin-frontend container_name: borrow_system-admin-frontend
networks:
- proxynet
build: ./admin build: ./admin
ports: ports:
- "8103:80" - "8103:80"
@@ -15,9 +19,9 @@ services:
backend_v2: backend_v2:
container_name: borrow_system-backend_v2 container_name: borrow_system-backend_v2
networks:
- proxynet
build: ./backendV2 build: ./backendV2
ports:
- "8102:8102"
environment: environment:
NODE_ENV: production NODE_ENV: production
DB_HOST: mysql_v2 DB_HOST: mysql_v2
@@ -30,6 +34,8 @@ services:
mysql_v2: mysql_v2:
container_name: borrow_system-mysql-v2 container_name: borrow_system-mysql-v2
networks:
- proxynet
image: mysql:8.0 image: mysql:8.0
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -39,9 +45,11 @@ services:
volumes: volumes:
- mysql-v2-data:/var/lib/mysql - mysql-v2-data:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
ports:
- "3310:3306"
volumes: volumes:
mysql-data: mysql-data:
mysql-v2-data: mysql-v2-data:
networks:
proxynet:
external: true