14 Commits

35 changed files with 687 additions and 237 deletions

View File

@@ -152,6 +152,10 @@ 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:
```
@@ -174,6 +178,10 @@ 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:
```

View File

@@ -1,7 +1,73 @@
# Borrow System
**You have reached the `debian12` branch.**
![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)
![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white)
![TailwindCSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?logo=tailwind-css&logoColor=white)
![Node.js](https://img.shields.io/badge/Node.js-339933?logo=node.js&logoColor=white)
![Express](https://img.shields.io/badge/Express-000000?logo=express&logoColor=white)
![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white)
![JWT](https://img.shields.io/badge/JWT-000000?logo=jsonwebtokens&logoColor=white)
Here you will find the source code of exactly the application that I have hosted.
A small fullstack system to log in, view available items, reserve them for a time window, and manage personal loans.
The main branch or the branch that I am developing on, is the `dev` branch.
- 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 (highlevel)
- 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`. Crosscomponent 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`

View File

@@ -12,6 +12,7 @@
"@emotion/react": "^11.14.0",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.85.5",
"jotai": "^2.15.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.539.0",
"next-themes": "^0.4.6",
@@ -4420,6 +4421,35 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jotai": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz",
"integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0",
"@babel/template": ">=7.0.0",
"@types/react": ">=17.0.0",
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@babel/template": {
"optional": true
},
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -5645,13 +5675,13 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
@@ -5849,9 +5879,9 @@
}
},
"node_modules/vite": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@@ -5859,7 +5889,7 @@
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.14"
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"

View File

@@ -14,6 +14,7 @@
"@emotion/react": "^11.14.0",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.85.5",
"jotai": "^2.15.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.539.0",
"next-themes": "^0.4.6",

View File

@@ -5,6 +5,11 @@ 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);
@@ -19,7 +24,7 @@ const Layout: React.FC = () => {
if (Cookies.get("token")) {
const verifyToken = async () => {
const response = await fetch("https://backend.insta.the1s.de/api/verifyToken", {
const response = await fetch(`${API_BASE}/api/verifyToken`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,

View File

@@ -0,0 +1,3 @@
import { atom } from "jotai";
export const testAtom = atom<number>(0);

View File

@@ -0,0 +1,36 @@
# How to use Atoms
Atoms are the fundamental building blocks of state management in this system. They represent individual pieces of state that can be shared and manipulated across different components.
You can also name it global state.
## Creating an Atom
to create an atom you have to declare an atom like this:
```ts
import { atom } from 'jotai';
export const NAME_OF_YOUR_ATOM = atom<type_of_your_atom>(initial_value);
```
In this project we declare all atoms in the `States/Atoms.tsx`file. Which you can find above this README file.
## Using an Atom
To use an atom in your component, you can use the `useAtom` hook provided by Jotai. Here's an example of how to use an atom in a React component:
```tsx
import { useAtom } from 'jotai';
import { NAME_OF_YOUR_ATOM } from '@/States/Atoms';
const MyComponent = () => {
const [value, setValue] = useAtom(NAME_OF_YOUR_ATOM);
return (
<div>
<p>Current value: {value}</p>
<button onClick={() => setValue(newValue)}>Update Value</button>
</div>
);
};
```
As you can see, you can use `useAtom` like `useState` but the state is global. In this example `value` is the current state of the atom, and `setValue` is a function to update the state, which is also known as the setter function.

View File

@@ -14,6 +14,11 @@ 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;
@@ -57,9 +62,7 @@ const Landingpage: React.FC = () => {
const fetchData = async () => {
setIsLoading(true);
try {
const loanRes = await fetch(
"https://backend.insta.the1s.de/apiV2/allLoans"
);
const loanRes = await fetch(`${API_BASE}/apiV2/allLoans`);
const loanData = await loanRes.json();
if (Array.isArray(loanData)) {
setLoans(loanData);
@@ -71,9 +74,7 @@ const Landingpage: React.FC = () => {
);
}
const deviceRes = await fetch(
"https://backend.insta.the1s.de/apiV2/allItems"
);
const deviceRes = await fetch(`${API_BASE}/apiV2/allItems`);
const deviceData = await deviceRes.json();
if (Array.isArray(deviceData)) {
setDevices(deviceData);
@@ -212,7 +213,7 @@ const Landingpage: React.FC = () => {
borderRadius="full"
>
<HStack gap={2}>
<Lock size={16} />
<LockOpen size={16} />
<Text>Im Schließfach</Text>
</HStack>
</Button>
@@ -225,7 +226,7 @@ const Landingpage: React.FC = () => {
borderRadius="full"
>
<HStack gap={2}>
<LockOpen size={16} />
<Lock size={16} />
<Text>Nicht im Schließfach</Text>
</HStack>
</Button>

View File

@@ -18,6 +18,11 @@ 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;
@@ -51,7 +56,7 @@ const APIKeyTable: React.FC = () => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch("https://backend.insta.the1s.de/api/apiKeys", {
const response = await fetch(`${API_BASE}/api/apiKeys`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,

View File

@@ -59,6 +59,14 @@ 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();
}
}}
>

View File

@@ -33,7 +33,7 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
<Input
id="can_borrow_role"
type="number"
placeholder="Zahl (z.B. 2)"
placeholder="Zahl (1 - 4)"
/>
</Field.Root>
</Stack>
@@ -68,8 +68,10 @@ const AddItemForm: React.FC<AddItemFormProps> = ({ onClose, alert }) => {
alert(
"error",
"Fehler",
"Der Gegenstand konnte nicht erstellt werden."
res.message ||
"Der Gegenstand konnte nicht erstellt werden. (frontend bug)"
);
onClose();
}
}}
>

View File

@@ -55,57 +55,64 @@ const ChangePWform: React.FC<ChangePWformProps> = ({
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end" gap="2">
<Button variant="outline" onClick={onClose}>
Abbrechen
</Button>
<Button
variant="solid"
onClick={async () => {
const newPassword =
(
document.getElementById("new_password") as HTMLInputElement
)?.value.trim() || "";
const confirmNewPassword =
(
document.getElementById(
"confirm_new_password"
) as HTMLInputElement
)?.value.trim() || "";
<Card.Footer gap="2">
<Stack w="full" gap="3">
<Stack direction="row" justify="flex-end" gap="2">
<Button variant="outline" onClick={onClose}>
Abbrechen
</Button>
<Button
variant="solid"
onClick={async () => {
const newPassword =
(
document.getElementById(
"new_password"
) as HTMLInputElement
)?.value.trim() || "";
const confirmNewPassword =
(
document.getElementById(
"confirm_new_password"
) as HTMLInputElement
)?.value.trim() || "";
if (!newPassword || newPassword !== confirmNewPassword) {
subAlert("Passwörter stimmen nicht überein!");
return;
}
if (!newPassword || newPassword !== confirmNewPassword) {
subAlert("Passwörter stimmen nicht überein!");
return;
}
const res = await changePW(newPassword, username);
if (res.success) {
alert(
"success",
"Passwort geändert",
"Das Passwort wurde erfolgreich geändert."
);
onClose();
} else {
alert(
"error",
"Fehler",
"Das Passwort konnte nicht geändert werden."
);
onClose();
}
}}
>
Ändern
</Button>
{showSubAlert && (
<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Title>{subAlertMessage}</Alert.Title>
</Alert.Content>
</Alert.Root>
)}
const res = await changePW(newPassword, username);
if (res.success) {
alert(
"success",
"Passwort geändert",
"Das Passwort wurde erfolgreich geändert."
);
onClose();
} else {
alert(
"error",
"Fehler",
"Das Passwort konnte nicht geändert werden."
);
onClose();
}
}}
>
Ändern
</Button>
</Stack>
{showSubAlert && (
<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Title>{subAlertMessage}</Alert.Title>
</Alert.Content>
</Alert.Root>
)}
</Stack>
</Card.Footer>
</Card.Root>
</div>

View File

@@ -31,6 +31,11 @@ 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;
@@ -77,7 +82,7 @@ const ItemTable: React.FC = () => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch("https://backend.insta.the1s.de/api/allItems", {
const response = await fetch(`${API_BASE}/api/allItems`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,

View File

@@ -18,6 +18,11 @@ 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");
@@ -55,7 +60,7 @@ const LoanTable: React.FC = () => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch("https://backend.insta.the1s.de/api/allLoans", {
const response = await fetch(`${API_BASE}/api/allLoans`, {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,

View File

@@ -1,7 +1,12 @@
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("https://backend.insta.the1s.de/api/allUsers", {
const response = await fetch(`${API_BASE}/api/allUsers`, {
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},

View File

@@ -1,5 +1,10 @@
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;
@@ -13,7 +18,7 @@ export const loginFunc = async (
password: string
): Promise<LoginResult> => {
try {
const response = await fetch("https://backend.insta.the1s.de/api/loginAdmin", {
const response = await fetch(`${API_BASE}/api/loginAdmin`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),

View File

@@ -1,9 +1,14 @@
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(
`https://backend.insta.the1s.de/api/deleteUser/${userId}`,
`${API_BASE}/api/deleteUser/${userId}`,
{
method: "DELETE",
headers: {
@@ -28,7 +33,7 @@ export const handleEdit = async (
) => {
try {
const response = await fetch(
`https://backend.insta.the1s.de/api/editUser/${userId}`,
`${API_BASE}/api/editUser/${userId}`,
{
method: "POST",
headers: {
@@ -54,17 +59,14 @@ export const createUser = async (
password: string
) => {
try {
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 }),
}
);
const response = await fetch(`${API_BASE}/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");
}
@@ -77,17 +79,14 @@ export const createUser = async (
export const changePW = async (newPassword: string, username: string) => {
try {
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 }),
}
);
const response = await fetch(`${API_BASE}/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");
}
@@ -101,7 +100,7 @@ export const changePW = async (newPassword: string, username: string) => {
export const deleteLoan = async (loanId: number) => {
try {
const response = await fetch(
`https://backend.insta.the1s.de/api/deleteLoan/${loanId}`,
`${API_BASE}/api/deleteLoan/${loanId}`,
{
method: "DELETE",
headers: {
@@ -122,7 +121,7 @@ export const deleteLoan = async (loanId: number) => {
export const deleteItem = async (itemId: number) => {
try {
const response = await fetch(
`https://backend.insta.the1s.de/api/deleteItem/${itemId}`,
`${API_BASE}/api/deleteItem/${itemId}`,
{
method: "DELETE",
headers: {
@@ -145,19 +144,20 @@ export const createItem = async (
can_borrow_role: number
) => {
try {
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 }),
}
);
const response = await fetch(`${API_BASE}/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) {
throw new Error("Failed to create item");
return {
success: false,
message:
"Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes darf nicht mehrmals vergeben werden.",
};
}
return { success: true };
} catch (error) {
@@ -172,17 +172,14 @@ export const handleEditItems = async (
can_borrow_role: string
) => {
try {
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 }),
}
);
const response = await fetch(`${API_BASE}/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");
}
@@ -196,7 +193,7 @@ export const handleEditItems = async (
export const changeSafeState = async (itemId: number) => {
try {
const response = await fetch(
`https://backend.insta.the1s.de/api/changeSafeState/${itemId}`,
`${API_BASE}/api/changeSafeState/${itemId}`,
{
method: "PUT",
headers: {
@@ -216,7 +213,7 @@ export const changeSafeState = async (itemId: number) => {
export const createAPIentry = async (apiKey: string, user: string) => {
try {
const response = await fetch(`https://backend.insta.the1s.de/api/createAPIentry`, {
const response = await fetch(`${API_BASE}/api/createAPIentry`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -225,7 +222,11 @@ export const createAPIentry = async (apiKey: string, user: string) => {
body: JSON.stringify({ apiKey, user }),
});
if (!response.ok) {
throw new Error("Failed to create API entry");
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.",
};
}
return { success: true };
} catch (error) {
@@ -237,7 +238,7 @@ export const createAPIentry = async (apiKey: string, user: string) => {
export const deleteAPKey = async (apiKeyId: number) => {
try {
const response = await fetch(
`https://backend.insta.the1s.de/api/deleteAPKey/${apiKeyId}`,
`${API_BASE}/api/deleteAPKey/${apiKeyId}`,
{
method: "DELETE",
headers: {

View File

@@ -29,7 +29,8 @@
"@/*": ["./src/*"]
},
"forceConsistentCasingInFileNames": true
"forceConsistentCasingInFileNames": true,
"ignoreDeprecations": "6.0"
},
"include": ["src"]
}

View File

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

View File

@@ -7,6 +7,6 @@ RUN npm install
COPY . .
EXPOSE 8102
EXPOSE 8002
CMD ["npm", "start"]

View File

@@ -14,7 +14,8 @@
"ejs": "^3.1.10",
"express": "^5.1.0",
"jose": "^6.0.12",
"mysql2": "^3.14.3"
"mysql2": "^3.14.3",
"nodemailer": "^7.0.6"
}
},
"node_modules/accepts": {
@@ -713,6 +714,15 @@
"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",

View File

@@ -16,6 +16,7 @@
"ejs": "^3.1.10",
"express": "^5.1.0",
"jose": "^6.0.12",
"mysql2": "^3.14.3"
"mysql2": "^3.14.3",
"nodemailer": "^7.0.6"
}
}

View File

@@ -25,9 +25,191 @@ 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);
@@ -43,7 +225,6 @@ 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);
@@ -158,6 +339,15 @@ 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,

View File

@@ -3,8 +3,8 @@ import dotenv from "dotenv";
import {
getItemsFromDatabaseV2,
changeInSafeStateV2,
setReturnDateV2,
setTakeDateV2,
setReturnDateV2,
getLoanByCodeV2,
getAllLoansV2,
getAPIkey,

View File

@@ -5,7 +5,7 @@ import apiRouter from "./routes/api.js";
import apiRouterV2 from "./routes/apiV2.js";
env.config();
const app = express();
const port = 8102;
const port = 8002;
app.use(cors());
// Increase body size limits to support large CSV JSON payloads

View File

@@ -8,7 +8,6 @@ const pool = mysql
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
})
.promise();
@@ -52,22 +51,56 @@ 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) {
if (result.affectedRows > 0 && setItemStates.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) {
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true };
}
return { success: false };
@@ -149,6 +182,16 @@ 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,

View File

@@ -9,7 +9,6 @@ export async function generateToken(payload) {
.setIssuedAt()
.setExpirationTime("2h") // Token valid for 2 hours
.sign(secret);
console.log("Generated token: ", newToken);
return newToken;
}

View File

@@ -1,45 +1,35 @@
services:
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
# 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
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
# admin-frontend:
# container_name: admin-frontend
# build: ./admin
# ports:
# - "8003:8003"
# environment:
# - CHOKIDAR_USEPOLLING=true
# volumes:
# - ./admin:/app
# - /app/node_modules
# restart: unless-stopped
borrow_system-backend:
container_name: borrow_system-backend
build: ./backend
ports:
- "8102:8102"
networks:
- proxynet
- borrow_system-internal
- "8002:8002"
environment:
DB_HOST: mysql
DB_PORT: 3306
DB_USER: root
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: borrow_system
@@ -62,14 +52,6 @@ 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

View File

@@ -7,6 +7,6 @@ RUN npm install
COPY . .
EXPOSE 8101
EXPOSE 8001
CMD ["npm", "run", "dev"]

View File

@@ -10,6 +10,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.85.5",
"jotai": "^2.15.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.539.0",
"primeicons": "^7.0.0",
@@ -3164,6 +3165,35 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jotai": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz",
"integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0",
"@babel/template": ">=7.0.0",
"@types/react": ">=17.0.0",
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@babel/template": {
"optional": true
},
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.85.5",
"jotai": "^2.15.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.539.0",
"primeicons": "^7.0.0",

View File

@@ -19,6 +19,11 @@ 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})/);
@@ -28,7 +33,7 @@ const formatDate = (iso: string | null) => {
};
async function fetchUserLoans(): Promise<Loan[]> {
const res = await fetch("https://backend.insta.the1s.de/api/userLoans", {
const res = await fetch(`${API_BASE}/api/userLoans`, {
method: "GET",
headers: { Authorization: `Bearer ${Cookies.get("token") || ""}` },
});

View File

@@ -6,6 +6,11 @@ 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() {
@@ -25,7 +30,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("https://backend.insta.the1s.de/api/items", {
const response = await fetch(`${API_BASE}/api/items`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
@@ -57,7 +62,7 @@ export const fetchAllData = async (token: string | undefined) => {
// get all loans
try {
const response = await fetch("https://backend.insta.the1s.de/api/loans", {
const response = await fetch(`${API_BASE}/api/loans`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
@@ -89,7 +94,7 @@ export const fetchAllData = async (token: string | undefined) => {
// get user loans
try {
const response = await fetch("https://backend.insta.the1s.de/api/userLoans", {
const response = await fetch(`${API_BASE}/api/userLoans`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
@@ -122,7 +127,7 @@ export const fetchAllData = async (token: string | undefined) => {
export const loginUser = async (username: string, password: string) => {
try {
const response = await fetch("https://backend.insta.the1s.de/api/login", {
const response = await fetch(`${API_BASE}/api/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -158,7 +163,7 @@ export const getBorrowableItems = async () => {
}
try {
const response = await fetch("https://backend.insta.the1s.de/api/borrowableItems", {
const response = await fetch(`${API_BASE}/api/borrowableItems`, {
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,

View File

@@ -2,10 +2,15 @@ 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(
`https://backend.insta.the1s.de/api/deleteLoan/${loanID}`,
`${API_BASE}/api/deleteLoan/${loanID}`,
{
method: "DELETE",
headers: {
@@ -75,17 +80,14 @@ export const rmFromRemove = (itemID: number) => {
export const createLoan = async (startDate: string, endDate: string) => {
const items = removeArr;
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 }),
}
);
const response = await fetch(`${API_BASE}/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");
@@ -106,7 +108,7 @@ export const createLoan = async (startDate: string, endDate: string) => {
export const onReturn = async (loanID: number) => {
const response = await fetch(
`https://backend.insta.the1s.de/api/returnLoan/${loanID}`,
`${API_BASE}/api/returnLoan/${loanID}`,
{
method: "POST",
headers: {
@@ -125,15 +127,12 @@ export const onReturn = async (loanID: number) => {
};
export const onTake = async (loanID: number) => {
const response = await fetch(
`https://backend.insta.the1s.de/api/takeLoan/${loanID}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
},
}
);
const response = await fetch(`${API_BASE}/api/takeLoan/${loanID}`, {
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
},
});
if (!response.ok) {
myToast("Fehler beim Ausleihen der Ausleihe", "error");
@@ -145,17 +144,14 @@ export const onTake = async (loanID: number) => {
};
export const changePW = async (oldPassword: string, newPassword: string) => {
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 }),
}
);
const response = await fetch(`${API_BASE}/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");

View File

@@ -1,17 +1,15 @@
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: [tailwindcss()],
plugins: [react(), svgr(), tailwindcss()],
server: {
host: "0.0.0.0",
allowedHosts: ["insta.the1s.de"],
port: 8101,
watch: { usePolling: true },
hmr: {
host: "insta.the1s.de",
port: 8101,
protocol: "wss",
port: 8001,
watch: {
usePolling: true,
},
},
});