Compare commits
45 Commits
8589971dc8
...
debian12
| Author | SHA1 | Date | |
|---|---|---|---|
| 195f270064 | |||
| c8a230979f | |||
| 3d592c5c76 | |||
| 581cd4a1fd | |||
| c53e6e095a | |||
| eaa325668c | |||
| 08104d32db | |||
| 1fa8b4a9a7 | |||
| 3ba3c1c0cb | |||
| ee54d51f8b | |||
| a8dab549af | |||
| 06976f7972 | |||
| cc7c024892 | |||
| 3eb452aeab | |||
| f46a654184 | |||
| 863409aed9 | |||
| 052137a697 | |||
| 2f3583ccd0 | |||
| 9da72cc5bf | |||
| c633627b7c | |||
| 5259c41b13 | |||
| 3d9e3814fe | |||
| b44edb2b1d | |||
| a72fabc0a0 | |||
| 1406f28f86 | |||
| 38d1091e9b | |||
| f82efecb8c | |||
| 1f12bc8839 | |||
| f19750f6f3 | |||
| 808b3fd5c4 | |||
| 0891598eb9 | |||
| 39ff02f2e7 | |||
| cc67fb4f85 | |||
| 75ff4aadc1 | |||
| 6f998d07c1 | |||
| f2bb326040 | |||
| 8c701db900 | |||
| d1664338a6 | |||
| 1a2624cd9e | |||
| a138190cc6 | |||
| 993e0cd74b | |||
| dab004a7b6 | |||
| d039336f39 | |||
| 4c781e9325 | |||
| 451e6b3646 |
@@ -1,366 +1,177 @@
|
||||
# Borrow System API Documentation
|
||||
|
||||
**Frontend:** https://insta.the1s.de
|
||||
**Backend base URL:** `https://insta.the1s.de/backend/api`
|
||||
## Overview
|
||||
|
||||
---
|
||||
The Borrow System API provides endpoints for managing items, loans, and door access for a borrowing/locker system. All endpoints require authentication via an 8-digit API key passed as a URL parameter.
|
||||
|
||||
## Authentication
|
||||
|
||||
All API endpoints require **either**:
|
||||
|
||||
### 1. Bearer Token (JWT)
|
||||
|
||||
Send an `Authorization` header:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
- Used for user-based access.
|
||||
- Token must be valid and not expired.
|
||||
|
||||
### 2. API Key (for devices / machine-to-machine)
|
||||
|
||||
Include an API key in the route as `:key` parameter:
|
||||
|
||||
```text
|
||||
/api/.../:key/...
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```http
|
||||
GET /api/items/12345678
|
||||
```
|
||||
|
||||
Where `12345678` is your API key.
|
||||
The API key is validated server-side.
|
||||
|
||||
---
|
||||
|
||||
## Common Response Codes
|
||||
|
||||
- `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.
|
||||
|
||||
---
|
||||
All requests must include a valid API key in the URL path as the `:key` parameter. API keys are 8-digit numeric strings.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Get All Items
|
||||
The Base URL for all endpoints is: `https://insta.the1s.de/backend/api`
|
||||
|
||||
**GET** `/api/items/:key`
|
||||
### Get All Items
|
||||
|
||||
Returns a list of all items.
|
||||
`GET /items/:key`
|
||||
|
||||
#### Path Parameters
|
||||
Returns all items in the system.
|
||||
|
||||
- `:key` – API key (8-digit number)
|
||||
|
||||
#### Authentication
|
||||
|
||||
- Either:
|
||||
- Valid `Authorization: Bearer <token>`
|
||||
- Or valid `:key` path parameter
|
||||
|
||||
#### Request Example
|
||||
|
||||
```http
|
||||
GET /api/items/12345678 HTTP/1.1
|
||||
Host: backend.insta.the1s.de
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
```
|
||||
|
||||
#### Successful Response (200)
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"item_name": "DJI 1er Mikro",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"item_name": "Laptop",
|
||||
"can_borrow_role": 1,
|
||||
"in_safe": true,
|
||||
"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",
|
||||
"door_key": 101,
|
||||
"last_borrowed_person": "jdoe",
|
||||
"currently_borrowing": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Response (500)
|
||||
**Response 500:**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Failed to fetch items"
|
||||
}
|
||||
{ "message": "Failed to fetch items" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Toggle Item Safe State
|
||||
### Change Item Safe State
|
||||
|
||||
Toggles `in_safe` between `0` and `1` for a given item.
|
||||
`POST /change-state/:key/:itemId`
|
||||
|
||||
**Keep in mind that when you return a loan by code, the item states are automatically updated.**
|
||||
Toggles the `in_safe` boolean state of an item.
|
||||
|
||||
**POST** `/api/change-state/:key/:itemId`
|
||||
**URL Parameters:**
|
||||
|
||||
#### Path Parameters
|
||||
- **key** - API key
|
||||
- **itemId** - The item's ID
|
||||
|
||||
- `:key` – API key (8-digit number)
|
||||
- `:itemId` – Item ID (integer)
|
||||
**Response 200:** Returns on successful toggle.
|
||||
|
||||
#### Authentication
|
||||
|
||||
- Either Bearer token or `:key` API key.
|
||||
|
||||
#### Request Example
|
||||
|
||||
```http
|
||||
POST /api/change-state/12345678/42 HTTP/1.1
|
||||
Host: backend.insta.the1s.de
|
||||
```
|
||||
|
||||
#### Successful Response (200)
|
||||
**Response 500:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
_(Implementation currently only returns `{ success: true }`, so `data` may be empty.)_
|
||||
|
||||
#### Error Response (500)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Failed to update item state"
|
||||
}
|
||||
{ "message": "Failed to update item state" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Get Loan by Code
|
||||
### Get Loan by Code
|
||||
|
||||
Fetch loan information by `loan_code`.
|
||||
`GET /get-loan-by-code/:key/:loan_code`
|
||||
|
||||
**GET** `/api/get-loan-by-code/:key/:loan_code`
|
||||
Retrieves loan details by its 6-digit loan code.
|
||||
|
||||
#### Path Parameters
|
||||
**URL Parameters:**
|
||||
|
||||
- `:key` – API key (8-digit number)
|
||||
- `:loan_code` – Loan code (string)
|
||||
- **key** - API key
|
||||
- **loan_code** - A 6-digit numeric loan code
|
||||
|
||||
#### Authentication
|
||||
|
||||
- Either Bearer token or `:key` API key.
|
||||
|
||||
#### Request Example
|
||||
|
||||
```http
|
||||
GET /api/get-loan-by-code/12345678/12345 HTTP/1.1
|
||||
Host: backend.insta.the1s.de
|
||||
```
|
||||
|
||||
#### Successful Response (200)
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"username": "john",
|
||||
"username": "jdoe",
|
||||
"returned_date": null,
|
||||
"take_date": "2025-01-01T10:00:00.000Z",
|
||||
"lockers": "[1, 2, 3]"
|
||||
"take_date": "2024-01-15T10:30:00.000Z",
|
||||
"lockers": [1, 3]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Response (404)
|
||||
**Response 404:**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Loan not found"
|
||||
}
|
||||
{ "message": "Loan not found" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Set Loan Return Date
|
||||
### Set Take Date
|
||||
|
||||
Sets `returned_date = NOW()` on a loan and updates related items:
|
||||
`POST /set-take-date/:key/:loan_code`
|
||||
|
||||
- `in_safe = 1`
|
||||
- `currently_borrowing = NULL`
|
||||
- `last_borrowed_person = username`
|
||||
Records when items are physically taken by setting `take_date` to the current timestamp. Updates associated items to `in_safe = false` and sets `currently_borrowing` to the loan's username.
|
||||
|
||||
**POST** `/api/set-return-date/:key/:loan_code`
|
||||
**URL Parameters:**
|
||||
|
||||
#### Path Parameters
|
||||
- **key** - API key
|
||||
- **loan_code** - A 6-digit numeric loan code
|
||||
|
||||
- `:key` – API key (8-digit number)
|
||||
- `:loan_code` – Loan code (string)
|
||||
**Response 200:** Empty JSON object on success.
|
||||
|
||||
#### Authentication
|
||||
|
||||
- Either Bearer token or `:key` API key.
|
||||
|
||||
#### Request Example
|
||||
|
||||
```http
|
||||
POST /api/set-return-date/12345678/12345 HTTP/1.1
|
||||
Host: backend.insta.the1s.de
|
||||
```
|
||||
|
||||
#### Successful Response (200)
|
||||
**Response 500:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {}
|
||||
}
|
||||
{ "message": "Loan not found or already taken" }
|
||||
```
|
||||
|
||||
#### Error Response (500)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Failed to set return date"
|
||||
}
|
||||
```
|
||||
> **Note:** This endpoint will fail if the loan has already been taken or does not exist.
|
||||
|
||||
---
|
||||
|
||||
### 5. Set Loan Take Date
|
||||
### Set Return Date
|
||||
|
||||
Sets `take_date = NOW()` on a loan and updates related items:
|
||||
`POST /set-return-date/:key/:loan_code`
|
||||
|
||||
- `in_safe = 0`
|
||||
- `currently_borrowing = username`
|
||||
Marks a loan as returned by setting `returned_date` to the current timestamp. Also updates all associated items to `in_safe = true`, clears `currently_borrowing`, and sets `last_borrowed_person`. Therefore, keep in mind that you must not call other endpoints that will change the safe state of an item after or before calling this endpoint, otherwise the state of the items will be inconsistent.
|
||||
|
||||
**POST** `/api/set-take-date/:key/:loan_code`
|
||||
**URL Parameters:**
|
||||
|
||||
#### Path Parameters
|
||||
- **key** - API key
|
||||
- **loan_code** - A 6-digit numeric loan code
|
||||
|
||||
- `:key` – API key (8-digit number)
|
||||
- `:loan_code` – Loan code (string)
|
||||
**Response 200:** Empty JSON object on success.
|
||||
|
||||
#### 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)
|
||||
**Response 500:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {}
|
||||
}
|
||||
{ "message": "Failed to set return date" }
|
||||
```
|
||||
|
||||
#### Error Response (500)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Failed to set take date"
|
||||
}
|
||||
```
|
||||
> **Note:** This endpoint will fail if the loan has already been returned (i.e., `returned_date` is not `NULL`).
|
||||
|
||||
---
|
||||
|
||||
### 6. Open Door by Door Key
|
||||
### Open Door
|
||||
|
||||
Looks up an item by its `door_key`, toggles `in_safe`, and returns safe information.
|
||||
`GET /open-door/:key/:doorKey`
|
||||
|
||||
**GET** `/api/open-door/:key/:doorKey`
|
||||
Toggles the safe state of an item identified by its door key and returns the associated safe number.
|
||||
|
||||
#### Path Parameters
|
||||
**URL Parameters:**
|
||||
|
||||
- `:key` – API key (8-digit number)
|
||||
- `:doorKey` – Door key/token (string) used by hardware to identify the locker.
|
||||
- **key** - API key
|
||||
- **doorKey** - The door key identifier assigned to an item
|
||||
|
||||
#### 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)
|
||||
**Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"safe_nr": 5,
|
||||
"id": 42
|
||||
"safe_nr": 3,
|
||||
"id": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Response (500)
|
||||
**Response 500:**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Failed to open door"
|
||||
}
|
||||
{ "message": "Failed to open door" }
|
||||
```
|
||||
|
||||
---
|
||||
## Error Handling
|
||||
|
||||
## Authentication Error Messages
|
||||
|
||||
### Missing credentials
|
||||
|
||||
Status: `401`
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Unauthorized"
|
||||
}
|
||||
```
|
||||
|
||||
### Invalid JWT
|
||||
|
||||
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.
|
||||
All endpoints return a `500` status code for server-side failures and a JSON body with a `message` field, except for **Get Loan by Code** which returns `404` when no matching loan is found.
|
||||
|
||||
@@ -14,7 +14,7 @@ server {
|
||||
}
|
||||
|
||||
location /backend/ {
|
||||
proxy_pass http://borrow_system-backend_v2:8004/;
|
||||
proxy_pass http://borrow_system-backend_v2:8102/;
|
||||
}
|
||||
|
||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { triggerLogoutAtom } from "@/states/Atoms";
|
||||
import { MyLoansPage } from "./pages/MyLoansPage";
|
||||
import Landingpage from "./pages/Landingpage";
|
||||
import { changeLanguage } from "i18next";
|
||||
import { Box, Flex } from "@chakra-ui/react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { Footer } from "./components/footer/Footer";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
@@ -72,8 +72,8 @@ function App() {
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Flex direction="column" minH="100vh">
|
||||
<Box as="main" flex="1">
|
||||
<Flex direction="column" minH="100dvh">
|
||||
<Flex as="main" flex="1" direction="column">
|
||||
<UserContext.Provider value={user}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
@@ -88,7 +88,7 @@ function App() {
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</UserContext.Provider>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Footer />
|
||||
</Flex>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -142,7 +142,7 @@ export const Header = () => {
|
||||
value="help"
|
||||
onSelect={() =>
|
||||
window.open(
|
||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
|
||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
)
|
||||
@@ -279,7 +279,7 @@ export const Header = () => {
|
||||
</Button>
|
||||
|
||||
<a
|
||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
|
||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages"
|
||||
target="_blank"
|
||||
>
|
||||
<Button variant="ghost">
|
||||
|
||||
@@ -5,7 +5,14 @@ export const Footer = () => {
|
||||
const { data: info } = useVersionInfoQuery();
|
||||
|
||||
return (
|
||||
<Box as="footer" py={4} textAlign="center" width="100%">
|
||||
<Box
|
||||
as="footer"
|
||||
py={4}
|
||||
textAlign="center"
|
||||
width="100%"
|
||||
flexShrink={0}
|
||||
fontSize="sm"
|
||||
>
|
||||
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
||||
<br />
|
||||
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode"
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
|
||||
import * as React from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ColorModeProvider as ThemeColorModeProvider } 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 (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
<ColorModeProvider>{children}</ColorModeProvider>
|
||||
</ChakraProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
Card,
|
||||
SimpleGrid,
|
||||
Button,
|
||||
Container,
|
||||
} from "@chakra-ui/react";
|
||||
import MyAlert from "@/components/myChakra/MyAlert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
import Cookies from "js-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Header } from "@/components/Header";
|
||||
|
||||
export const formatDateTime = (value: string | null | undefined) => {
|
||||
if (!value) return "N/A";
|
||||
@@ -32,6 +33,7 @@ type Loan = {
|
||||
returned_date: string | null;
|
||||
take_date: string | null;
|
||||
loaned_items_name: string[] | string;
|
||||
note: string | null;
|
||||
};
|
||||
|
||||
type Device = {
|
||||
@@ -46,7 +48,6 @@ type Device = {
|
||||
|
||||
const Landingpage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loans, setLoans] = useState<Loan[]>([]);
|
||||
@@ -59,7 +60,7 @@ const Landingpage: React.FC = () => {
|
||||
const setError = (
|
||||
status: "error" | "success",
|
||||
message: string,
|
||||
description: string
|
||||
description: string,
|
||||
) => {
|
||||
setIsError(false);
|
||||
setErrorStatus(status);
|
||||
@@ -85,7 +86,7 @@ const Landingpage: React.FC = () => {
|
||||
setError(
|
||||
"error",
|
||||
t("error-by-loading"),
|
||||
t("unexpected-date-format_loan")
|
||||
t("unexpected-date-format_loan"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ const Landingpage: React.FC = () => {
|
||||
setError(
|
||||
"error",
|
||||
t("error-by-loading"),
|
||||
t("unexpected-date-format_device")
|
||||
t("unexpected-date-format_device"),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -115,14 +116,8 @@ const Landingpage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading as="h1" size="lg" mb={2}>
|
||||
Matthias-Claudius-Schule Technik
|
||||
</Heading>
|
||||
|
||||
<Button onClick={() => navigate("/", { replace: true })}>
|
||||
{t("back")}
|
||||
</Button>
|
||||
<Container className="px-6 sm:px-8 pt-10">
|
||||
<Header />
|
||||
|
||||
<Heading as="h2" size="md" mb={4}>
|
||||
{t("all-loans")}
|
||||
@@ -168,6 +163,9 @@ const Landingpage: React.FC = () => {
|
||||
<Table.ColumnHeader>
|
||||
<strong>{t("return-date")}</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>{t("note")}</strong>
|
||||
</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
@@ -184,6 +182,7 @@ const Landingpage: React.FC = () => {
|
||||
</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
||||
<Table.Cell>{loan.note}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
@@ -260,7 +259,7 @@ const Landingpage: React.FC = () => {
|
||||
</HStack>
|
||||
</Button>
|
||||
</HStack>
|
||||
</>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import Cookies from "js-cookie";
|
||||
import { Navigate, useNavigate, useLocation } from "react-router-dom";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Footer } from "@/components/footer/Footer";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
export const LoginPage = () => {
|
||||
@@ -71,7 +70,7 @@ export const LoginPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Card.Root maxW="sm">
|
||||
<Card.Header>
|
||||
@@ -115,7 +114,6 @@ export const LoginPage = () => {
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</form>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const getBorrowableItems = async (
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
||||
method: "POST",
|
||||
@@ -24,7 +21,8 @@ export const getBorrowableItems = async (
|
||||
data: null,
|
||||
status: "error",
|
||||
title: "Server error",
|
||||
description: t("serverError"),
|
||||
description:
|
||||
"An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import path from "node:path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8001,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
allowedHosts: ["insta.the1s.de"],
|
||||
port: 8101,
|
||||
watch: { usePolling: true },
|
||||
hmr: {
|
||||
host: "insta.the1s.de",
|
||||
port: 8101,
|
||||
protocol: "wss",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ server {
|
||||
}
|
||||
|
||||
location /backend/ {
|
||||
proxy_pass http://borrow_system-backend_v2:8004/;
|
||||
proxy_pass http://borrow_system-backend_v2:8102/;
|
||||
}
|
||||
|
||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from "react";
|
||||
import { loginFunc } from "@/utils/loginUser";
|
||||
import MyAlert from "../components/myChakra/MyAlert";
|
||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
|
||||
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||
const [username, setUsername] = useState("");
|
||||
@@ -43,8 +44,7 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||
</Field.Root>
|
||||
<Field.Root>
|
||||
<Field.Label>password</Field.Label>
|
||||
<Input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
159
admin/src/components/ui/password-input.tsx
Normal file
159
admin/src/components/ui/password-input.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import type {
|
||||
ButtonProps,
|
||||
GroupProps,
|
||||
InputProps,
|
||||
StackProps,
|
||||
} from "@chakra-ui/react"
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
Stack,
|
||||
mergeRefs,
|
||||
useControllableState,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuEye, LuEyeOff } from "react-icons/lu"
|
||||
|
||||
export interface PasswordVisibilityProps {
|
||||
/**
|
||||
* The default visibility state of the password input.
|
||||
*/
|
||||
defaultVisible?: boolean
|
||||
/**
|
||||
* The controlled visibility state of the password input.
|
||||
*/
|
||||
visible?: boolean
|
||||
/**
|
||||
* Callback invoked when the visibility state changes.
|
||||
*/
|
||||
onVisibleChange?: (visible: boolean) => void
|
||||
/**
|
||||
* Custom icons for the visibility toggle button.
|
||||
*/
|
||||
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
|
||||
}
|
||||
|
||||
export interface PasswordInputProps
|
||||
extends InputProps,
|
||||
PasswordVisibilityProps {
|
||||
rootProps?: GroupProps
|
||||
}
|
||||
|
||||
export const PasswordInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
PasswordInputProps
|
||||
>(function PasswordInput(props, ref) {
|
||||
const {
|
||||
rootProps,
|
||||
defaultVisible,
|
||||
visible: visibleProp,
|
||||
onVisibleChange,
|
||||
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const [visible, setVisible] = useControllableState({
|
||||
value: visibleProp,
|
||||
defaultValue: defaultVisible || false,
|
||||
onChange: onVisibleChange,
|
||||
})
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
endElement={
|
||||
<VisibilityTrigger
|
||||
disabled={rest.disabled}
|
||||
onPointerDown={(e) => {
|
||||
if (rest.disabled) return
|
||||
if (e.button !== 0) return
|
||||
e.preventDefault()
|
||||
setVisible(!visible)
|
||||
}}
|
||||
>
|
||||
{visible ? visibilityIcon.off : visibilityIcon.on}
|
||||
</VisibilityTrigger>
|
||||
}
|
||||
{...rootProps}
|
||||
>
|
||||
<Input
|
||||
{...rest}
|
||||
ref={mergeRefs(ref, inputRef)}
|
||||
type={visible ? "text" : "password"}
|
||||
/>
|
||||
</InputGroup>
|
||||
)
|
||||
})
|
||||
|
||||
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function VisibilityTrigger(props, ref) {
|
||||
return (
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
me="-2"
|
||||
aspectRatio="square"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
height="calc(100% - {spacing.2})"
|
||||
aria-label="Toggle password visibility"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface PasswordStrengthMeterProps extends StackProps {
|
||||
max?: number
|
||||
value: number
|
||||
}
|
||||
|
||||
export const PasswordStrengthMeter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PasswordStrengthMeterProps
|
||||
>(function PasswordStrengthMeter(props, ref) {
|
||||
const { max = 4, value, ...rest } = props
|
||||
|
||||
const percent = (value / max) * 100
|
||||
const { label, colorPalette } = getColorPalette(percent)
|
||||
|
||||
return (
|
||||
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
|
||||
<HStack width="full" {...rest}>
|
||||
{Array.from({ length: max }).map((_, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
height="1"
|
||||
flex="1"
|
||||
rounded="sm"
|
||||
data-selected={index < value ? "" : undefined}
|
||||
layerStyle="fill.subtle"
|
||||
colorPalette="gray"
|
||||
_selected={{
|
||||
colorPalette,
|
||||
layerStyle: "fill.solid",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
{label && <HStack textStyle="xs">{label}</HStack>}
|
||||
</Stack>
|
||||
)
|
||||
})
|
||||
|
||||
function getColorPalette(percent: number) {
|
||||
switch (true) {
|
||||
case percent < 33:
|
||||
return { label: "Low", colorPalette: "red" }
|
||||
case percent < 66:
|
||||
return { label: "Medium", colorPalette: "orange" }
|
||||
default:
|
||||
return { label: "High", colorPalette: "green" }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
@@ -23,13 +24,10 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
/* Chakra / Pfad Aliases */
|
||||
"baseUrl": ".",
|
||||
/* Path aliases */
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ export default defineConfig({
|
||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8003,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
allowedHosts: ["admin.insta.the1s.de"],
|
||||
port: 8103,
|
||||
watch: { usePolling: true },
|
||||
hmr: {
|
||||
host: "admin.insta.the1s.de",
|
||||
port: 8103,
|
||||
protocol: "wss",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"backend-info": {
|
||||
"version": "v2.1 (dev)"
|
||||
"version": "v2.1.1"
|
||||
},
|
||||
"frontend-info": {
|
||||
"version": "v2.1 (dev)"
|
||||
"version": "v2.1.2"
|
||||
},
|
||||
"admin-panel-info": {
|
||||
"version": "v1.3.2 (dev)"
|
||||
"version": "v1.3.2"
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export const getItemsFromDatabaseV2 = async () => {
|
||||
export const getLoanByCodeV2 = async (loan_code) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
|
||||
[loan_code]
|
||||
[loan_code],
|
||||
);
|
||||
if (result.length > 0) {
|
||||
return { success: true, data: result[0] };
|
||||
@@ -33,7 +33,7 @@ export const getLoanByCodeV2 = async (loan_code) => {
|
||||
export const changeInSafeStateV2 = async (itemId) => {
|
||||
const [result] = await pool.query(
|
||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||
[itemId]
|
||||
[itemId],
|
||||
);
|
||||
if (result.affectedRows > 0) {
|
||||
return { success: true };
|
||||
@@ -42,50 +42,62 @@ export const changeInSafeStateV2 = async (itemId) => {
|
||||
};
|
||||
|
||||
export const setReturnDateV2 = async (loanCode) => {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
try {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id, username FROM loans WHERE loan_code = ?",
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
const [owner] = await pool.query(
|
||||
"SELECT username FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
if (items.length === 0)
|
||||
return { success: false, message: "No items found for loan" };
|
||||
|
||||
if (items.length === 0) return { success: false };
|
||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||
? items[0].loaned_items_id
|
||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||
|
||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||
? items[0].loaned_items_id
|
||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
const [setItemStates] = await pool.query(
|
||||
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
|
||||
[owner[0].username, itemIds]
|
||||
);
|
||||
if (result.affectedRows === 0) return { success: false };
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
if (itemIds.length > 0) {
|
||||
await pool.query(
|
||||
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = ? WHERE id IN (?)",
|
||||
[items[0].username, itemIds],
|
||||
);
|
||||
}
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
return { success: true };
|
||||
return { success: true, data: { returned: true } };
|
||||
} catch (error) {
|
||||
console.error("setReturnDateV2 error:", error);
|
||||
return { success: false, message: "Failed to set return date" };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const setTakeDateV2 = async (loanCode) => {
|
||||
const [isTaken] = await pool.query(
|
||||
"SELECT take_date FROM loans WHERE loan_code = ?",
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
if (isTaken.length === 0 || isTaken[0].take_date !== null) {
|
||||
return { success: false, message: "Loan not found or already taken" };
|
||||
}
|
||||
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
const [owner] = await pool.query(
|
||||
"SELECT username FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
if (items.length === 0) return { success: false };
|
||||
if (items.length === 0)
|
||||
return { success: false, message: "No items found for loan" };
|
||||
|
||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||
? items[0].loaned_items_id
|
||||
@@ -93,18 +105,18 @@ export const setTakeDateV2 = async (loanCode) => {
|
||||
|
||||
const [setItemStates] = await pool.query(
|
||||
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
||||
[owner[0].username, itemIds]
|
||||
[owner[0].username, itemIds],
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
|
||||
[loanCode],
|
||||
);
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
return { message: "Failed to set take date", success: false };
|
||||
};
|
||||
|
||||
export const getAllLoansV2 = async () => {
|
||||
@@ -118,12 +130,12 @@ export const getAllLoansV2 = async () => {
|
||||
export const openDoor = async (doorKey) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
||||
[doorKey]
|
||||
[doorKey],
|
||||
);
|
||||
if (result.length > 0) {
|
||||
const [changeItemSate] = await pool.query(
|
||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||
[result[0].id]
|
||||
[result[0].id],
|
||||
);
|
||||
if (changeItemSate.affectedRows > 0) {
|
||||
return { success: true, data: result[0] };
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get(
|
||||
} else {
|
||||
res.status(404).json({ message: "Loan not found" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Route for API to set the return date by the loan code
|
||||
@@ -58,11 +58,11 @@ router.post(
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setReturnDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
res.status(200).json({});
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set return date" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Route for API to set the take away date by the loan code
|
||||
@@ -73,11 +73,11 @@ router.post(
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setTakeDateV2(loanCode);
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
res.status(200).json({});
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set take date" });
|
||||
res.status(500).json({ message: result.message });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Route for API to open a door
|
||||
|
||||
@@ -62,6 +62,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
||||
mailInfo.data.start_date,
|
||||
mailInfo.data.end_date,
|
||||
mailInfo.data.created_at,
|
||||
mailInfo.data.note,
|
||||
);
|
||||
return res.status(201).json({
|
||||
message: "Loan created successfully",
|
||||
|
||||
@@ -34,7 +34,14 @@ const formatDateTime = (value) => {
|
||||
return "N/A";
|
||||
};
|
||||
|
||||
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
function buildLoanEmail({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
const itemsList =
|
||||
Array.isArray(items) && items.length
|
||||
@@ -116,6 +123,12 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
createdDate,
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 14px; color:#6b7280; vertical-align:top;">Notiz</td>
|
||||
<td style="padding:10px 14px; font-weight:600; color:#111827;">${
|
||||
note || "Keine Notiz"
|
||||
}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin:22px 0 0 0; font-size:14px;">
|
||||
@@ -134,7 +147,14 @@ function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||
function buildLoanEmailText({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}) {
|
||||
const itemsText =
|
||||
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
||||
return [
|
||||
@@ -145,10 +165,18 @@ function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||
`Start: ${formatDateTime(startDate)}`,
|
||||
`Ende: ${formatDateTime(endDate)}`,
|
||||
`Erstellt am: ${formatDateTime(createdDate)}`,
|
||||
`Notiz: ${note || "Keine Notiz"}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||
export function sendMailLoan(
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
@@ -170,8 +198,16 @@ export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}),
|
||||
html: buildLoanEmail({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
note,
|
||||
}),
|
||||
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
||||
});
|
||||
|
||||
console.log("Loan message sent:", info.messageId);
|
||||
|
||||
@@ -20,7 +20,7 @@ import apiRouter from "./routes/api/api.route.js";
|
||||
|
||||
env.config();
|
||||
const app = express();
|
||||
const port = 8004;
|
||||
const port = 8102;
|
||||
|
||||
app.use(cors());
|
||||
// Body-Parser VOR den Routen registrieren
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
services:
|
||||
# usr-frontend_v2:
|
||||
# container_name: borrow_system-usr-frontend
|
||||
# build: ./FrontendV2
|
||||
# ports:
|
||||
# - "8001:80"
|
||||
# restart: unless-stopped
|
||||
usr-frontend_v2:
|
||||
container_name: borrow_system-usr-frontend
|
||||
networks:
|
||||
- proxynet
|
||||
build: ./FrontendV2
|
||||
restart: unless-stopped
|
||||
|
||||
# admin-frontend:
|
||||
# container_name: borrow_system-admin-frontend
|
||||
# build: ./admin
|
||||
# ports:
|
||||
# - "8003:80"
|
||||
# restart: unless-stopped
|
||||
admin-frontend:
|
||||
container_name: borrow_system-admin-frontend
|
||||
networks:
|
||||
- proxynet
|
||||
build: ./admin
|
||||
restart: unless-stopped
|
||||
|
||||
backend_v2:
|
||||
container_name: borrow_system-backend_v2
|
||||
networks:
|
||||
- proxynet
|
||||
build: ./backendV2
|
||||
ports:
|
||||
- "8004:8004"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DB_HOST: mysql_v2
|
||||
@@ -30,6 +30,8 @@ services:
|
||||
|
||||
mysql_v2:
|
||||
container_name: borrow_system-mysql-v2
|
||||
networks:
|
||||
- proxynet
|
||||
image: mysql:8.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -39,9 +41,11 @@ services:
|
||||
volumes:
|
||||
- mysql-v2-data:/var/lib/mysql
|
||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||
ports:
|
||||
- "3310:3306"
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
mysql-v2-data:
|
||||
|
||||
networks:
|
||||
proxynet:
|
||||
external: true
|
||||
|
||||
Reference in New Issue
Block a user