Compare commits
36 Commits
dev_v1-adm
...
7f9ed23a86
Author | SHA1 | Date | |
---|---|---|---|
7f9ed23a86 | |||
21b152ef2b | |||
451a5a92dd | |||
85b519c5b1 | |||
27db4c7390 | |||
1b08344a0f | |||
a0be100db5 | |||
2143d53eb5 | |||
c4d5ebd9ae | |||
938e9000f8 | |||
558a8330af | |||
ad2395f98b | |||
51baf8d970 | |||
d18465ff1d | |||
b36f1ba9ba | |||
784bd1e8ce | |||
ae7aec8d3b | |||
3f9381a80c | |||
1826086186 | |||
af4abfc8f9 | |||
ba0f06e104 | |||
a932144e94 | |||
36ad60b782 | |||
e4467dba32 | |||
410923af92 | |||
24c405386b | |||
d5296bd3fa | |||
3ee2f6b670 | |||
09af4c760c | |||
3fd0fd9584 | |||
27984ebac8 | |||
3d4aab74d5 | |||
4076630eec | |||
6025212e93 | |||
de554048eb | |||
e1d79d2c79 |
@@ -1,4 +1,4 @@
|
||||
# Backend API docs (apiV2)
|
||||
# Backend API docs
|
||||
|
||||
If you want to cooperate with me, or build something new with my backend API, feel free to reach out!
|
||||
|
||||
@@ -6,51 +6,49 @@ On this page you will learn how my API works.
|
||||
|
||||
## General information
|
||||
|
||||
When you look at my backend folder and file structure, you can see that I have two files called `API`. The first file called `api.js` which is for my web frontend, because this file works together with my JWT token service.
|
||||
When you look at my backend folder and file structure, you can see that I have two files called `API`. The first file called `api.js` is for my web frontend, because this file works together with my JWT token service.
|
||||
|
||||
But I have built a second API. You can see the second API file in the same directory, the file is called `apiV2.js`.
|
||||
**\*But I have built a second API. You can see the second API file in the same directory, the file is called `apiV2.js`.**
|
||||
|
||||
But first you have to get an API Key. You can get the API key from my admin dashboard. When you don't have any access to my admin dashboard, please contact your administrator or me.
|
||||
This is the file that you can use to build an API.
|
||||
|
||||
---
|
||||
|
||||
## Base URL
|
||||
|
||||
- Frontend: `https://insta.the1s.de`
|
||||
- Backend: `https://backend.insta.the1s.de`
|
||||
- Base path for this API: `https://backend.insta.the1s.de/apiV2`
|
||||
|
||||
You can see the status of this and all my other services at `https://status.the1s.de`.
|
||||
|
||||
_I have also build a [fallback page](https://git.the1s.de/theis.gaedigk/fallback-page). When only the application is down, you will see a friendly message and a link to the status page. (Only if the server is not down)_
|
||||
But first you have to get the Admin API key, stored in an `.env` file on my server.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require an API key as a path parameter named `:key`.
|
||||
All endpoints require the Admin API key (`ADMIN_ID`) as a URL parameter.
|
||||
|
||||
Example: `/apiV2/items/:key`
|
||||
|
||||
If the key is missing or invalid, the API responds with `401 Unauthorized`.
|
||||
Example: `/apiV2/items/{ADMIN_ID}`
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
## URL
|
||||
|
||||
### 1) Get all items
|
||||
- The frontend is currently running on `https://insta.the1s.de`.
|
||||
|
||||
GET `/apiV2/items/:key`
|
||||
- The backend is currently running on `https://backend.insta.the1s.de`.
|
||||
|
||||
Returns a list of all items wrapped in a `data` object.
|
||||
You can see the status of this and all my other services at `https://status.the1s.de`.
|
||||
|
||||
Example request:
|
||||
---
|
||||
|
||||
## Current endpoints
|
||||
|
||||
### 1. Get All Items
|
||||
|
||||
**GET** `/apiV2/items/:key`
|
||||
|
||||
Returns a list of all items and their details.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
GET https://backend.insta.the1s.de/apiV2/items/12345
|
||||
GET https://backend.insta.the1s.de/apiV2/items/your_admin_key
|
||||
```
|
||||
|
||||
Example response:
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
@@ -61,66 +59,206 @@ Example response:
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"item_name": "DJI 2er Mikro 1",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"item_name": "DJI 2er Mikro 2",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"item_name": "Rode Richt Mikrofon",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"item_name": "Kamera Stativ",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"item_name": "SONY Kamera - inkl. Akkus und Objektiv",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"item_name": "MacBook inkl. Adapter",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"item_name": "SD Karten",
|
||||
"can_borrow_role": 3,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"item_name": "Kameragimbal",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"item_name": "ATEM MINI PRO",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"item_name": "Handygimbal",
|
||||
"can_borrow_role": 4,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"item_name": "Kameralfter",
|
||||
"can_borrow_role": 1,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"item_name": "Kleine Kamera 1 - inkl. Objektiv",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"item_name": "Kleine Kamera 2 - inkl. Objektiv",
|
||||
"can_borrow_role": 2,
|
||||
"inSafe": 1,
|
||||
"entry_created_at": "2025-08-19T22:02:16.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
Each item has the following properties:
|
||||
|
||||
- `id`: Unique identifier
|
||||
- `item_name`: Item name
|
||||
- `can_borrow_role`: Role allowed to borrow
|
||||
- `inSafe`: 1 if in locker, 0 otherwise
|
||||
- `entry_created_at`: Creation timestamp
|
||||
- `id`: The unique identifier for the item.
|
||||
- `item_name`: The name of the item.
|
||||
- `can_borrow_role`: The role ID that is allowed to borrow the item.
|
||||
- `inSafe`: Indicates whether the item is currently in the locker (1) or not (0). This variable/state can change over time.
|
||||
|
||||
Status: 200 on success, 500 on failure.
|
||||
_You also get an http 200 status code._
|
||||
|
||||
---
|
||||
|
||||
### 2) Change item safe state
|
||||
### 2. Change Item Safe State
|
||||
|
||||
POST `/apiV2/controlInSafe/:key/:itemId/:state`
|
||||
**POST** `/apiV2/controlInSafe/:key/:itemId/:state`
|
||||
|
||||
Updates `inSafe` (locker) state of an item.
|
||||
Updates the `inSafe` state of an item (whether it is in the locker).
|
||||
|
||||
- `state` must be `"1"` (in safe) or `"0"` (not in safe)
|
||||
- `state` must be `"1"` (in safe) or `"0"` (not in safe).
|
||||
|
||||
Example request:
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/controlInSafe/12345/123/1
|
||||
POST https://backend.insta.the1s.de/apiV2/controlInSafe/your_admin_key/item_id/new_item_state
|
||||
```
|
||||
|
||||
Example response (shape depends on database service):
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{ "data": { /* update result */ } }
|
||||
{}
|
||||
```
|
||||
|
||||
Status:
|
||||
_An empty object means, that the operation was successful and no further information is returned._
|
||||
|
||||
- 200 on success
|
||||
- 400 if `state` is invalid
|
||||
- 500 on failure
|
||||
|
||||
**You can get the item id on the admin panel, from your system administrator.**
|
||||
_You also get an http 200 status code._
|
||||
|
||||
---
|
||||
|
||||
### 3) Get loan by code
|
||||
### 3. Set Return Date
|
||||
|
||||
GET `/apiV2/getLoanByCode/:key/:loan_code`
|
||||
**POST** `/apiV2/setReturnDate/:key/:loan_code`
|
||||
|
||||
Retrieves the details of a specific loan.
|
||||
Sets the `returned_date` of a loan to the current server time.
|
||||
|
||||
Example request:
|
||||
- `loan_code`: The unique code of the loan.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
GET https://backend.insta.the1s.de/apiV2/getLoanByCode/12345/123456
|
||||
POST https://backend.insta.the1s.de/apiV2/setReturnDate/your_admin_key/your_loan_code
|
||||
```
|
||||
|
||||
Example response:
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
_An empty object means, that the operation was successful and no further information is returned._
|
||||
|
||||
_You also get an http 200 status code._
|
||||
|
||||
---
|
||||
|
||||
### 4. Set Take Date
|
||||
|
||||
**POST** `/apiV2/setTakeDate/:key/:loan_code`
|
||||
|
||||
Sets the `take_date` of a loan to the current server time.
|
||||
|
||||
- `loan_code`: The unique code of the loan.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setTakeDate/your_admin_key/your_loan_code
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
_An empty object means, that the operation was successful and no further information is returned._
|
||||
|
||||
_You also get an http 2xx status code._
|
||||
|
||||
---
|
||||
|
||||
### 5. Get whole loan by loan code
|
||||
|
||||
**POST** `/getLoanByCode/:key/:loan_code`
|
||||
|
||||
Retrieves the details of a specific loan by its unique code.
|
||||
|
||||
- `loan_code`: The unique code of the loan.
|
||||
|
||||
#### Example Request
|
||||
|
||||
```
|
||||
GET https://backend.insta.the1s.de/getLoanByCode/your_admin_key/your_loan_code
|
||||
```
|
||||
|
||||
#### Example Response
|
||||
|
||||
```
|
||||
{
|
||||
@@ -133,78 +271,36 @@ Example response:
|
||||
"take_date": null,
|
||||
"returned_date": null,
|
||||
"created_at": "2025-08-20T11:23:40.000Z",
|
||||
"loaned_items_id": [8, 9],
|
||||
"loaned_items_name": ["SD Karten", "Kameragimbal"]
|
||||
"loaned_items_id": [
|
||||
8,
|
||||
9
|
||||
],
|
||||
"loaned_items_name": [
|
||||
"SD Karten",
|
||||
"Kameragimbal"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Status:
|
||||
_You also get an http 200 status code._
|
||||
|
||||
- 200 on success
|
||||
- 404 if not found
|
||||
If the loan id does not exist, you will receive a 404 status code and an error message.
|
||||
|
||||
```
|
||||
{
|
||||
"message": "Loan not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4) Set return date (now) by loan code
|
||||
## Error Handling
|
||||
|
||||
POST `/apiV2/setReturnDate/:key/:loan_code`
|
||||
|
||||
Sets the `returned_date` to the current server time.
|
||||
|
||||
**Note:** I have updated this API route, so that everytime you return or take a loan, the state of the loaned items is automatically updated.
|
||||
|
||||
**DO NOT UPDATE THE STATE MANUALLY! (only if the item was taken with an admin key)**
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setReturnDate/12345/123456
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{ "data": { /* update result */ } }
|
||||
```
|
||||
|
||||
Status: 200 on success, 500 on failure.
|
||||
- `403 Forbidden`: Invalid or missing API key.
|
||||
- `400 Bad Request`: Invalid parameters (e.g., wrong state value).
|
||||
- `500 Internal Server Error`: Database or server error.
|
||||
|
||||
---
|
||||
|
||||
### 5) Set take date (now) by loan code
|
||||
|
||||
POST `/apiV2/setTakeDate/:key/:loan_code`
|
||||
|
||||
Sets the `take_date` to the current server time.
|
||||
|
||||
**Note:** I have updated this API route, so that everytime you return or take a loan, the state of the loaned items is automatically updated.
|
||||
|
||||
**DO NOT UPDATE THE STATE MANUALLY! (only if the item was taken with an admin key)**
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
POST https://backend.insta.the1s.de/apiV2/setTakeDate/12345/123456
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
{ "data": { /* update result */ } }
|
||||
```
|
||||
|
||||
Status: 200 on success, 500 on failure.
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
- 401 Unauthorized: Missing or invalid API key
|
||||
- 400 Bad Request: Invalid parameters (e.g., wrong state value)
|
||||
- 404 Not Found: Loan not found
|
||||
- 500 Internal Server Error: Database or server error
|
||||
|
||||
---
|
||||
|
||||
If you have questions or want to collaborate, please reach out!
|
||||
If you have questions or want to collaborate, please reach out to me!
|
||||
|
72
README.md
72
README.md
@@ -1,73 +1,7 @@
|
||||
# Borrow System
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
**You have reached the `debian12` branch.**
|
||||
|
||||
A small full‑stack system to log in, view available items, reserve them for a time window, and manage personal loans.
|
||||
Here you will find the source code of exactly the application that I have hosted.
|
||||
|
||||
- Frontend: React + TypeScript + Vite + Tailwind CSS
|
||||
- Backend: Node.js + Express + MySQL + JWT (jose)
|
||||
- Orchestration: Docker Compose (backend + MySQL)
|
||||
|
||||
## Contents
|
||||
|
||||
- Frontend: [frontend/](frontend)
|
||||
- Vite/Tailwind config: [frontend/vite.config.ts](frontend/vite.config.ts), [frontend/tailwind.config.js](frontend/tailwind.config.js)
|
||||
- App entry: [frontend/src/main.tsx](frontend/src/main.tsx), [frontend/src/App.tsx](frontend/src/App.tsx)
|
||||
- UI: [frontend/src/layout/Layout.tsx](frontend/src/layout/Layout.tsx), [frontend/src/components](frontend/src/components)
|
||||
- Data/utilities: [frontend/src/utils/fetchData.ts](frontend/src/utils/fetchData.ts), [frontend/src/utils/userHandler.ts](frontend/src/utils/userHandler.ts), [frontend/src/utils/toastify.ts](frontend/src/utils/toastify.ts)
|
||||
- Backend: [backend/](backend)
|
||||
- Server: [backend/server.js](backend/server.js)
|
||||
- Routes: [backend/routes/api.js](backend/routes/api.js), [backend/routes/apiV2.js](backend/routes/apiV2.js)
|
||||
- DB + services: [backend/services/database.js](backend/services/database.js), [backend/services/tokenService.js](backend/services/tokenService.js)
|
||||
- Schema/seed: [backend/scheme.sql](backend/scheme.sql)
|
||||
- Docs: [docs/](docs)
|
||||
- API docs (see below): [docs/backend_API_docs/README.md](docs/backend_API_docs/README.md)
|
||||
|
||||
## Features (high‑level)
|
||||
|
||||
- Auth via JWT (login -> token cookie) using the backend route in [backend/routes/api.js](backend/routes/api.js).
|
||||
- After login, the app loads items, loans, and user loans and keeps them in localStorage.
|
||||
- Choose a date range to fetch borrowable items, select items, and create a loan.
|
||||
- Manage personal loans list (and delete a loan).
|
||||
|
||||
Key frontend utilities:
|
||||
|
||||
- [`utils.fetchData.fetchAllData`](frontend/src/utils/fetchData.ts): loads items, loans, and user loans after login.
|
||||
- [`utils.fetchData.getBorrowableItems`](frontend/src/utils/fetchData.ts): fetches borrowable items for the selected time range.
|
||||
- [`utils.userHandler.createLoan`](frontend/src/utils/userHandler.ts): creates a new loan for selected items.
|
||||
- [`utils.userHandler.handleDeleteLoan`](frontend/src/utils/userHandler.ts): deletes a loan and syncs local state.
|
||||
- [`utils.toastify.myToast`](frontend/src/utils/toastify.ts): toast notifications.
|
||||
|
||||
UI flow (main screens):
|
||||
|
||||
- Period selection: [frontend/src/components/Form1.tsx](frontend/src/components/Form1.tsx)
|
||||
- Borrowable items + selection: [frontend/src/components/Form2.tsx](frontend/src/components/Form2.tsx)
|
||||
- User loans table: [frontend/src/components/Form4.tsx](frontend/src/components/Form4.tsx)
|
||||
|
||||
## Development
|
||||
|
||||
- Scripts: see [frontend/package.json](frontend/package.json) and [backend/package.json](backend/package.json)
|
||||
- Frontend: `npm run dev`, `npm run build`, `npm run preview`, `npm run lint`
|
||||
- Backend: `npm start`
|
||||
- Linting: ESLint configured via [frontend/eslint.config.js](frontend/eslint.config.js)
|
||||
- TypeScript configs: [frontend/tsconfig.app.json](frontend/tsconfig.app.json), [frontend/tsconfig.node.json](frontend/tsconfig.node.json)
|
||||
|
||||
## Configuration notes
|
||||
|
||||
- Vite/Tailwind integration via [frontend/vite.config.ts](frontend/vite.config.ts) and `@tailwindcss/vite`; CSS entry uses `@import "tailwindcss"` in [frontend/src/index.css](frontend/src/index.css).
|
||||
- Toasts wired in [frontend/src/main.tsx](frontend/src/main.tsx) with `react-toastify`.
|
||||
- Local state is stored in `localStorage` keys: `allItems`, `allLoans`, `userLoans`, `borrowableItems`. Cross‑component updates are signaled via window events from [`utils.fetchData`](frontend/src/utils/fetchData.ts).
|
||||
|
||||
## API documentation
|
||||
|
||||
Refer to the dedicated API docs:
|
||||
|
||||
`docs/backend_API_docs/README.md`
|
||||
The main branch or the branch that I am developing on, is the `dev` branch.
|
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Box, Heading, Text, Flex, Button } from "@chakra-ui/react";
|
||||
import Sidebar from "./Sidebar";
|
||||
import UserTable from "../components/UserTable";
|
||||
@@ -18,24 +17,6 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
||||
|
||||
const [activeView, setActiveView] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const raw = window.location.pathname.slice(1);
|
||||
if (raw) {
|
||||
setActiveView(decodeURIComponent(raw));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sync URL when activeView changes, without reloading
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!activeView) return;
|
||||
const desired = `/${encodeURIComponent(activeView)}`;
|
||||
if (window.location.pathname !== desired) {
|
||||
window.history.replaceState(null, "", desired);
|
||||
}
|
||||
}, [activeView]);
|
||||
|
||||
return (
|
||||
<Flex h="100vh">
|
||||
<Sidebar
|
||||
|
@@ -5,11 +5,6 @@ import Login from "./Login";
|
||||
import Cookies from "js-cookie";
|
||||
import Landingpage from "@/components/API/Landingpage";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
const Layout: React.FC = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [showAPI, setShowAPI] = useState(false);
|
||||
@@ -24,7 +19,7 @@ const Layout: React.FC = () => {
|
||||
|
||||
if (Cookies.get("token")) {
|
||||
const verifyToken = async () => {
|
||||
const response = await fetch(`${API_BASE}/api/verifyToken`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/verifyToken", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
@@ -44,7 +39,6 @@ const Layout: React.FC = () => {
|
||||
|
||||
const handleLogout = () => {
|
||||
Cookies.remove("token");
|
||||
window.location.pathname = "/";
|
||||
setIsLoggedIn(false);
|
||||
};
|
||||
|
||||
|
@@ -24,7 +24,6 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Card.Root maxW="sm">
|
||||
<Card.Header>
|
||||
<Card.Title>Login</Card.Title>
|
||||
@@ -55,12 +54,11 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||
{isError && (
|
||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
||||
)}
|
||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
||||
<Button onClick={() => handleLogin()} variant="solid">
|
||||
Login
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -14,11 +14,6 @@ import { Lock, LockOpen } from "lucide-react";
|
||||
import MyAlert from "../myChakra/MyAlert";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
type Loan = {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -62,7 +57,9 @@ const Landingpage: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const loanRes = await fetch(`${API_BASE}/apiV2/allLoans`);
|
||||
const loanRes = await fetch(
|
||||
"https://backend.insta.the1s.de/apiV2/allLoans"
|
||||
);
|
||||
const loanData = await loanRes.json();
|
||||
if (Array.isArray(loanData)) {
|
||||
setLoans(loanData);
|
||||
@@ -74,7 +71,9 @@ const Landingpage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const deviceRes = await fetch(`${API_BASE}/apiV2/allItems`);
|
||||
const deviceRes = await fetch(
|
||||
"https://backend.insta.the1s.de/apiV2/allItems"
|
||||
);
|
||||
const deviceData = await deviceRes.json();
|
||||
if (Array.isArray(deviceData)) {
|
||||
setDevices(deviceData);
|
||||
@@ -213,7 +212,7 @@ const Landingpage: React.FC = () => {
|
||||
borderRadius="full"
|
||||
>
|
||||
<HStack gap={2}>
|
||||
<LockOpen size={16} />
|
||||
<Lock size={16} />
|
||||
<Text>Im Schließfach</Text>
|
||||
</HStack>
|
||||
</Button>
|
||||
@@ -226,7 +225,7 @@ const Landingpage: React.FC = () => {
|
||||
borderRadius="full"
|
||||
>
|
||||
<HStack gap={2}>
|
||||
<Lock size={16} />
|
||||
<LockOpen size={16} />
|
||||
<Text>Nicht im Schließfach</Text>
|
||||
</HStack>
|
||||
</Button>
|
||||
|
@@ -18,11 +18,6 @@ import { deleteAPKey } from "@/utils/userActions";
|
||||
import AddAPIKey from "./AddAPIKey";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
apiKey: string;
|
||||
@@ -56,7 +51,7 @@ const APIKeyTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/apiKeys`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/apiKeys", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
|
@@ -59,14 +59,6 @@ const AddAPIKey: React.FC<AddAPIKeyProps> = ({ onClose, alert }) => {
|
||||
"Der API Key wurde erfolgreich erstellt."
|
||||
);
|
||||
onClose();
|
||||
} else {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler beim Erstellen des API Keys",
|
||||
res.message ||
|
||||
"Beim Erstellen des API Keys ist ein Fehler aufgetreten. (frontend bug)"
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@@ -33,7 +33,7 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
|
||||
<Input
|
||||
id="can_borrow_role"
|
||||
type="number"
|
||||
placeholder="Zahl (1 - 4)"
|
||||
placeholder="Zahl (z.B. 2)"
|
||||
/>
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
@@ -68,10 +68,8 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
|
||||
alert(
|
||||
"error",
|
||||
"Fehler",
|
||||
res.message ||
|
||||
"Der Gegenstand konnte nicht erstellt werden. (frontend bug)"
|
||||
"Der Gegenstand konnte nicht erstellt werden."
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@@ -55,9 +55,7 @@ const ChangePWform: React.FC<ChangePWformProps> = ({
|
||||
</Field.Root>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer gap="2">
|
||||
<Stack w="full" gap="3">
|
||||
<Stack direction="row" justify="flex-end" gap="2">
|
||||
<Card.Footer justifyContent="flex-end" gap="2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
@@ -66,9 +64,7 @@ const ChangePWform: React.FC<ChangePWformProps> = ({
|
||||
onClick={async () => {
|
||||
const newPassword =
|
||||
(
|
||||
document.getElementById(
|
||||
"new_password"
|
||||
) as HTMLInputElement
|
||||
document.getElementById("new_password") as HTMLInputElement
|
||||
)?.value.trim() || "";
|
||||
const confirmNewPassword =
|
||||
(
|
||||
@@ -102,8 +98,6 @@ const ChangePWform: React.FC<ChangePWformProps> = ({
|
||||
>
|
||||
Ändern
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{showSubAlert && (
|
||||
<Alert.Root status="error">
|
||||
<Alert.Indicator />
|
||||
@@ -112,7 +106,6 @@ const ChangePWform: React.FC<ChangePWformProps> = ({
|
||||
</Alert.Content>
|
||||
</Alert.Root>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
@@ -31,11 +31,6 @@ import {
|
||||
import AddItemForm from "./AddItemForm";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
item_name: string;
|
||||
@@ -82,7 +77,7 @@ const ItemTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/allItems`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/allItems", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
|
@@ -18,11 +18,6 @@ import { formatDateTime } from "@/utils/userFuncs";
|
||||
import { Trash2, RefreshCcwDot } from "lucide-react";
|
||||
import { deleteLoan } from "@/utils/userActions";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
const LoanTable: React.FC = () => {
|
||||
const [items, setItems] = useState<Loan[]>([]);
|
||||
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
|
||||
@@ -60,7 +55,7 @@ const LoanTable: React.FC = () => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/allLoans`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/allLoans", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
|
@@ -1,12 +1,7 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export const fetchUserData = async () => {
|
||||
const response = await fetch(`${API_BASE}/api/allUsers`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/allUsers", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
|
@@ -1,10 +1,5 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export type LoginSuccess = { success: true };
|
||||
export type LoginFailure = {
|
||||
success: false;
|
||||
@@ -18,7 +13,7 @@ export const loginFunc = async (
|
||||
password: string
|
||||
): Promise<LoginResult> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/loginAdmin`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/loginAdmin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
|
@@ -1,14 +1,9 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export const handleDelete = async (userId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteUser/${userId}`,
|
||||
`https://backend.insta.the1s.de/api/deleteUser/${userId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -33,7 +28,7 @@ export const handleEdit = async (
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/editUser/${userId}`,
|
||||
`https://backend.insta.the1s.de/api/editUser/${userId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -59,14 +54,17 @@ export const createUser = async (
|
||||
password: string
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/createUser`, {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/createUser`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ username, role, password }),
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create user");
|
||||
}
|
||||
@@ -79,14 +77,17 @@ export const createUser = async (
|
||||
|
||||
export const changePW = async (newPassword: string, username: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/changePWadmin`, {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/changePWadmin`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ newPassword, username }),
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to change password");
|
||||
}
|
||||
@@ -100,7 +101,7 @@ export const changePW = async (newPassword: string, username: string) => {
|
||||
export const deleteLoan = async (loanId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteLoan/${loanId}`,
|
||||
`https://backend.insta.the1s.de/api/deleteLoan/${loanId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -121,7 +122,7 @@ export const deleteLoan = async (loanId: number) => {
|
||||
export const deleteItem = async (itemId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteItem/${itemId}`,
|
||||
`https://backend.insta.the1s.de/api/deleteItem/${itemId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -144,20 +145,19 @@ export const createItem = async (
|
||||
can_borrow_role: number
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/createItem`, {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/createItem`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ item_name, can_borrow_role }),
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes darf nicht mehrmals vergeben werden.",
|
||||
};
|
||||
throw new Error("Failed to create item");
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -172,14 +172,17 @@ export const handleEditItems = async (
|
||||
can_borrow_role: string
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/updateItemByID`, {
|
||||
const response = await fetch(
|
||||
"https://backend.insta.the1s.de/api/updateItemByID",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ itemId, item_name, can_borrow_role }),
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to edit item");
|
||||
}
|
||||
@@ -193,7 +196,7 @@ export const handleEditItems = async (
|
||||
export const changeSafeState = async (itemId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/changeSafeState/${itemId}`,
|
||||
`https://backend.insta.the1s.de/api/changeSafeState/${itemId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
@@ -213,7 +216,7 @@ export const changeSafeState = async (itemId: number) => {
|
||||
|
||||
export const createAPIentry = async (apiKey: string, user: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/createAPIentry`, {
|
||||
const response = await fetch(`https://backend.insta.the1s.de/api/createAPIentry`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -222,11 +225,7 @@ export const createAPIentry = async (apiKey: string, user: string) => {
|
||||
body: JSON.stringify({ apiKey, user }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"Fehler beim Erstellen des API Keys. Achten Sie darauf, dass alle Felder ausgefüllt sind und der API Key nicht doppelt vergeben wird.",
|
||||
};
|
||||
throw new Error("Failed to create API entry");
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -238,7 +237,7 @@ export const createAPIentry = async (apiKey: string, user: string) => {
|
||||
export const deleteAPKey = async (apiKeyId: number) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteAPKey/${apiKeyId}`,
|
||||
`https://backend.insta.the1s.de/api/deleteAPKey/${apiKeyId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
|
@@ -29,8 +29,7 @@
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"ignoreDeprecations": "6.0"
|
||||
"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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -7,6 +7,6 @@ RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8002
|
||||
EXPOSE 8102
|
||||
|
||||
CMD ["npm", "start"]
|
12
backend/package-lock.json
generated
12
backend/package-lock.json
generated
@@ -14,8 +14,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6"
|
||||
"mysql2": "^3.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -714,15 +713,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz",
|
||||
"integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
@@ -16,7 +16,6 @@
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6"
|
||||
"mysql2": "^3.14.3"
|
||||
}
|
||||
}
|
||||
|
@@ -25,191 +25,9 @@ import {
|
||||
getAllApiKeys,
|
||||
createAPIentry,
|
||||
deleteAPKey,
|
||||
getLoanInfoWithID,
|
||||
} from "../services/database.js";
|
||||
import { authenticate, generateToken } from "../services/tokenService.js";
|
||||
const router = express.Router();
|
||||
import nodemailer from "nodemailer";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
// Nice HTML + text templates for the loan email
|
||||
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
const itemsList =
|
||||
Array.isArray(items) && items.length
|
||||
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
|
||||
.map(
|
||||
(i) =>
|
||||
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`
|
||||
)
|
||||
.join("")}</ul>`
|
||||
: "<span style='color:#111827;'>N/A</span>";
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="supported-color-schemes" content="light">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
:root { color-scheme: light; supported-color-schemes: light; }
|
||||
body { margin:0; padding:0; }
|
||||
/* Mobile stacking */
|
||||
@media (max-width:480px) {
|
||||
.outer { width:100% !important; }
|
||||
.pad-sm { padding:16px !important; }
|
||||
.w-label { width:120px !important; }
|
||||
}
|
||||
/* Dark-mode override safety */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body, table, td, p, a, h1, h2, h3 { background:#ffffff !important; color:#111827 !important; }
|
||||
.brand-header { background:${brand} !important; color:#ffffff !important; }
|
||||
a { color:${brand} !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body bgcolor="#ffffff" style="background:#ffffff; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; color:#111827; -webkit-text-size-adjust:100%;">
|
||||
<!-- Preheader (hidden) -->
|
||||
<div style="display:none; max-height:0; overflow:hidden; opacity:0; mso-hide:all;">
|
||||
Neue Ausleihe erstellt – Übersicht der Buchung.
|
||||
</div>
|
||||
<div role="article" aria-roledescription="email" lang="de" style="padding:24px; background:#f2f4f7;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" class="outer" style="max-width:600px; margin:0 auto; background:#ffffff; border:1px solid #e5e7eb; border-radius:14px; overflow:hidden;">
|
||||
<tr>
|
||||
<td class="brand-header" style="padding:22px 26px; background:${brand}; color:#ffffff;">
|
||||
<h1 style="margin:0; font-size:18px; line-height:1.35; font-weight:600;">Neue Ausleihe erstellt</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pad-sm" style="padding:24px 26px; color:#111827;">
|
||||
<p style="margin:0 0 14px 0; line-height:1.4;">Es wurde eine neue Ausleihe angelegt. Hier sind die Details:</p>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="border-collapse:collapse; font-size:14px; line-height:1.3; background:#fcfcfd; border:1px solid #e5e7eb; border-radius:10px; overflow:hidden;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-label" style="padding:10px 14px; color:#6b7280; width:170px; border-bottom:1px solid #ececec;">Benutzer</td>
|
||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${
|
||||
user || "N/A"
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 14px; color:#6b7280; vertical-align:top; border-bottom:1px solid #ececec;">Ausgeliehene Gegenstände</td>
|
||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${itemsList}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
|
||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||
startDate
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
|
||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||
endDate
|
||||
)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
|
||||
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
|
||||
createdDate
|
||||
)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin:22px 0 0 0; font-size:14px;">
|
||||
<a href="https://admin.insta.the1s.de/api" style="display:inline-block; background:${brand}; color:#ffffff; text-decoration:none; padding:10px 16px; border-radius:6px; font-weight:600; font-size:14px;" target="_blank" rel="noopener noreferrer">
|
||||
Übersicht öffnen
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin:18px 0 0 0; font-size:12px; color:#6b7280; line-height:1.4;">
|
||||
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet. Bitte nicht antworten.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||
const itemsText =
|
||||
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
||||
return [
|
||||
"Neue Ausleihe erstellt",
|
||||
"",
|
||||
`Benutzer: ${user || "N/A"}`,
|
||||
`Gegenstände: ${itemsText}`,
|
||||
`Start: ${formatDateTime(startDate)}`,
|
||||
`Ende: ${formatDateTime(endDate)}`,
|
||||
`Erstellt am: ${formatDateTime(createdDate)}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.MAIL_USER,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const info = await transporter.sendMail({
|
||||
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
|
||||
to: process.env.MAIL_SENDEES,
|
||||
subject: "Eine neue Ausleihe wurde erstellt!",
|
||||
text: buildLoanEmailText({
|
||||
user,
|
||||
items,
|
||||
startDate,
|
||||
endDate,
|
||||
createdDate,
|
||||
}),
|
||||
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
||||
});
|
||||
|
||||
console.log("Message sent:", info.messageId);
|
||||
})();
|
||||
console.log("sendMailLoan called");
|
||||
}
|
||||
|
||||
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";
|
||||
};
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
const result = await loginFunc(req.body.username, req.body.password);
|
||||
@@ -225,6 +43,7 @@ router.post("/login", async (req, res) => {
|
||||
});
|
||||
|
||||
router.get("/items", authenticate, async (req, res) => {
|
||||
console.log(req);
|
||||
const result = await getItemsFromDatabase(req.user.role);
|
||||
if (result.success) {
|
||||
res.status(200).json(result.data);
|
||||
@@ -339,15 +158,6 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const mailInfo = await getLoanInfoWithID(result.data.id);
|
||||
console.log(mailInfo);
|
||||
sendMailLoan(
|
||||
mailInfo.data.username,
|
||||
mailInfo.data.loaned_items_name,
|
||||
mailInfo.data.start_date,
|
||||
mailInfo.data.end_date,
|
||||
mailInfo.data.created_at
|
||||
);
|
||||
return res.status(201).json({
|
||||
message: "Loan created successfully",
|
||||
loanId: result.data.id,
|
||||
|
@@ -3,8 +3,8 @@ import dotenv from "dotenv";
|
||||
import {
|
||||
getItemsFromDatabaseV2,
|
||||
changeInSafeStateV2,
|
||||
setTakeDateV2,
|
||||
setReturnDateV2,
|
||||
setTakeDateV2,
|
||||
getLoanByCodeV2,
|
||||
getAllLoansV2,
|
||||
getAPIkey,
|
||||
@@ -111,8 +111,8 @@ router.post("/setTakeDate/:key/:loan_code", apiKeyGuard, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Route for API to get ALL loans from the database without sensitive info (only for landingpage)
|
||||
router.get("/allLoans", async (req, res) => {
|
||||
// Route for API to get ALL loans from the database without sensitive info
|
||||
router.get("/allLoans/:key", apiKeyGuard, async (req, res) => {
|
||||
const result = await getAllLoansV2();
|
||||
if (result.success) {
|
||||
return res.status(200).json(result.data);
|
||||
@@ -120,8 +120,8 @@ router.get("/allLoans", async (req, res) => {
|
||||
return res.status(500).json({ message: "Failed to fetch loans" });
|
||||
});
|
||||
|
||||
// Route for API to get ALL items from the database (only for landingpage)
|
||||
router.get("/allItems", async (req, res) => {
|
||||
// Route for API to get ALL items form the database
|
||||
router.get("/allItems/:key", apiKeyGuard, async (req, res) => {
|
||||
const result = await getItemsFromDatabaseV2();
|
||||
if (result.success) {
|
||||
res.status(200).json(result.data);
|
||||
|
@@ -63,7 +63,8 @@ CREATE TABLE `apiKeys` (
|
||||
`apiKey` int NOT NULL UNIQUE,
|
||||
`user` VARCHAR(255) NOT NULL,
|
||||
`entry_created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `apiKey` (`apiKey`)
|
||||
);
|
||||
|
||||
INSERT INTO `items` (`item_name`, `can_borrow_role`, `inSafe`) VALUES
|
||||
|
@@ -5,7 +5,7 @@ import apiRouter from "./routes/api.js";
|
||||
import apiRouterV2 from "./routes/apiV2.js";
|
||||
env.config();
|
||||
const app = express();
|
||||
const port = 8002;
|
||||
const port = 8102;
|
||||
|
||||
app.use(cors());
|
||||
// Increase body size limits to support large CSV JSON payloads
|
||||
|
@@ -8,6 +8,7 @@ const pool = mysql
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
port: process.env.DB_PORT,
|
||||
})
|
||||
.promise();
|
||||
|
||||
@@ -51,56 +52,22 @@ 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]
|
||||
);
|
||||
|
||||
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 [setItemStates] = await pool.query(
|
||||
"UPDATE items SET inSafe = 1 WHERE id IN (?)",
|
||||
[itemIds]
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
if (result.affectedRows > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const setTakeDateV2 = async (loanCode) => {
|
||||
const [items] = await pool.query(
|
||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
|
||||
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 [setItemStates] = await pool.query(
|
||||
"UPDATE items SET inSafe = 0 WHERE id IN (?)",
|
||||
[itemIds]
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||
[loanCode]
|
||||
);
|
||||
|
||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||
if (result.affectedRows > 0) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
@@ -182,16 +149,6 @@ export const getBorrowableItemsFromDatabase = async (
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const getLoanInfoWithID = async (loanId) => {
|
||||
const [rows] = await pool.query("SELECT * FROM loans WHERE id = ?;", [
|
||||
loanId,
|
||||
]);
|
||||
if (rows.length > 0) {
|
||||
return { success: true, data: rows[0] };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const createLoanInDatabase = async (
|
||||
username,
|
||||
startDate,
|
||||
|
@@ -9,6 +9,7 @@ export async function generateToken(payload) {
|
||||
.setIssuedAt()
|
||||
.setExpirationTime("2h") // Token valid for 2 hours
|
||||
.sign(secret);
|
||||
console.log("Generated token: ", newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
|
@@ -1,35 +1,45 @@
|
||||
services:
|
||||
# borrow_system-frontend:
|
||||
# container_name: borrow_system-frontend
|
||||
# build: ./frontend
|
||||
# ports:
|
||||
# - "8001:8001"
|
||||
# environment:
|
||||
# - CHOKIDAR_USEPOLLING=true
|
||||
# volumes:
|
||||
# - ./frontend:/app
|
||||
# - /app/node_modules
|
||||
# restart: unless-stopped
|
||||
borrow_system-frontend:
|
||||
container_name: borrow_system-frontend
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "8101:8101"
|
||||
networks:
|
||||
- proxynet
|
||||
- borrow_system-internal
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
restart: unless-stopped
|
||||
|
||||
# admin-frontend:
|
||||
# container_name: admin-frontend
|
||||
# build: ./admin
|
||||
# ports:
|
||||
# - "8003:8003"
|
||||
# environment:
|
||||
# - CHOKIDAR_USEPOLLING=true
|
||||
# volumes:
|
||||
# - ./admin:/app
|
||||
# - /app/node_modules
|
||||
# restart: unless-stopped
|
||||
admin-frontend:
|
||||
container_name: admin-frontend
|
||||
build: ./admin
|
||||
networks:
|
||||
- proxynet
|
||||
- borrow_system-internal
|
||||
ports:
|
||||
- "8103:8103"
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
volumes:
|
||||
- ./admin:/app
|
||||
- /app/node_modules
|
||||
restart: unless-stopped
|
||||
|
||||
borrow_system-backend:
|
||||
container_name: borrow_system-backend
|
||||
build: ./backend
|
||||
ports:
|
||||
- "8002:8002"
|
||||
- "8102:8102"
|
||||
networks:
|
||||
- proxynet
|
||||
- borrow_system-internal
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_PORT: 3306
|
||||
DB_USER: root
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_NAME: borrow_system
|
||||
@@ -52,6 +62,14 @@ services:
|
||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||
ports:
|
||||
- "3309:3306"
|
||||
networks:
|
||||
- borrow_system-internal
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
|
||||
networks:
|
||||
proxynet:
|
||||
external: true
|
||||
borrow_system-internal:
|
||||
external: false
|
||||
|
@@ -7,6 +7,6 @@ RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8001
|
||||
EXPOSE 8101
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
@@ -19,11 +19,6 @@ type Loan = {
|
||||
loaned_items_name: string[];
|
||||
};
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
const formatDate = (iso: string | null) => {
|
||||
if (!iso) return "-";
|
||||
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
||||
@@ -33,7 +28,7 @@ const formatDate = (iso: string | null) => {
|
||||
};
|
||||
|
||||
async function fetchUserLoans(): Promise<Loan[]> {
|
||||
const res = await fetch(`${API_BASE}/api/userLoans`, {
|
||||
const res = await fetch("https://backend.insta.the1s.de/api/userLoans", {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${Cookies.get("token") || ""}` },
|
||||
});
|
||||
|
@@ -6,11 +6,6 @@ export const ALL_ITEMS_UPDATED_EVENT = "allItemsUpdated";
|
||||
export const BORROWABLE_ITEMS_UPDATED_EVENT = "borrowableItemsUpdated";
|
||||
export const AUTH_LOGOUT_EVENT = "authLogout";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
let sendError = false;
|
||||
|
||||
function logout() {
|
||||
@@ -30,7 +25,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
if (!token) return;
|
||||
// First we fetch all items that are potentially available for borrowing
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/items`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/items", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -62,7 +57,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
|
||||
// get all loans
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/loans`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/loans", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -94,7 +89,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
|
||||
// get user loans
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/userLoans`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/userLoans", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -127,7 +122,7 @@ export const fetchAllData = async (token: string | undefined) => {
|
||||
|
||||
export const loginUser = async (username: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/login`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -163,7 +158,7 @@ export const getBorrowableItems = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/borrowableItems`, {
|
||||
const response = await fetch("https://backend.insta.the1s.de/api/borrowableItems", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
|
@@ -2,15 +2,10 @@ import { myToast } from "./toastify";
|
||||
import Cookies from "js-cookie";
|
||||
import { queryClient } from "./queryClient";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export const handleDeleteLoan = async (loanID: number): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteLoan/${loanID}`,
|
||||
`https://backend.insta.the1s.de/api/deleteLoan/${loanID}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -80,14 +75,17 @@ export const rmFromRemove = (itemID: number) => {
|
||||
|
||||
export const createLoan = async (startDate: string, endDate: string) => {
|
||||
const items = removeArr;
|
||||
const response = await fetch(`${API_BASE}/api/createLoan`, {
|
||||
const response = await fetch(
|
||||
"https://backend.insta.the1s.de/api/createLoan",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ items, startDate, endDate }),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Erstellen der Ausleihe", "error");
|
||||
@@ -108,7 +106,7 @@ export const createLoan = async (startDate: string, endDate: string) => {
|
||||
|
||||
export const onReturn = async (loanID: number) => {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/returnLoan/${loanID}`,
|
||||
`https://backend.insta.the1s.de/api/returnLoan/${loanID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -127,12 +125,15 @@ export const onReturn = async (loanID: number) => {
|
||||
};
|
||||
|
||||
export const onTake = async (loanID: number) => {
|
||||
const response = await fetch(`${API_BASE}/api/takeLoan/${loanID}`, {
|
||||
const response = await fetch(
|
||||
`https://backend.insta.the1s.de/api/takeLoan/${loanID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Ausleihen der Ausleihe", "error");
|
||||
@@ -144,14 +145,17 @@ export const onTake = async (loanID: number) => {
|
||||
};
|
||||
|
||||
export const changePW = async (oldPassword: string, newPassword: string) => {
|
||||
const response = await fetch(`${API_BASE}/api/changePassword`, {
|
||||
const response = await fetch(
|
||||
"https://backend.insta.the1s.de/api/changePassword",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ oldPassword, newPassword }),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Ändern des Passworts", "error");
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), svgr(), tailwindcss()],
|
||||
plugins: [tailwindcss()],
|
||||
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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user