Compare commits

..
8 Commits
Author SHA1 Message Date
theis.gaedigk cc2c3d7f54 Merge pull request 'Refactor1' (#18) from refactor1 into dev
Reviewed-on: #18
2026-10-26 20:16:56 +01:00
theis.gaedigk 8b89f31178 change version number 2026-10-26 20:16:33 +01:00
theis.gaedigk c7933dfa3b add backend versioning 2026-10-26 20:15:06 +01:00
theis.gaedigk c1bb628f22 fix npm bug 2026-10-26 20:07:40 +01:00
theis.gaedigk 941f9105d6 change compose config 2026-10-26 19:56:49 +01:00
theis.gaedigk 0070b0e7a0 change folder name 2026-10-26 19:41:34 +01:00
theis.gaedigk 63d3e2db07 change config of services 2026-10-26 19:35:40 +01:00
theis.gaedigk 9c17b2a495 major changes to docker and npm config 2026-10-26 16:29:17 +01:00
104 changed files with 3526 additions and 9223 deletions
+14
View File
@@ -0,0 +1,14 @@
**/node_modules
**/dist
**/dist-ssr
.git
.idea
.vscode
**/.env
**/.env.*
**/.DS_Store
**/*.log
docs
pics
icon
no-as-a-service
+10
View File
@@ -0,0 +1,10 @@
DB_PASSWORD=
BACKEND_URL=
MAIL_HOST=
MAIL_PORT=
MAIL_USER=
MAIL_PASSWORD=
MAIL_SENDEES_CONTACT=
MAIL_SENDEES=
NAAS_URL=
SECRET_KEY=
+3 -1
View File
@@ -11,6 +11,8 @@ dist/
build/ build/
out/ out/
docker-compose.override.yml
# Environment variables # Environment variables
.env .env
.env.local .env.local
@@ -119,6 +121,6 @@ ToDo.txt
next-env.d.ts next-env.d.ts
# psd files from footage # psd files from footage
footage/*.psd pics/*.psd
icon/ icon/
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2
}
+78
View File
@@ -0,0 +1,78 @@
# Docker
## Prerequisites
You have created an empty `docker-compose.override.yml` file in the root directory of this repository.
## Docker Network
If you want to run the stack in a docker network, you need to change the `BACKEND_HOST` variable in the root `.env` file
to `stockhome-backend`. To add a docker network, create a `docker-compose.override.yml` file in the root directory of
this repository with the following content:
```yaml
services:
admin-frontend:
networks:
- proxynet
backend:
networks:
- proxynet
usr-frontend:
networks:
- proxynet
no-as-a-service:
networks:
- proxynet
mysql:
networks:
- proxynet
networks:
proxynet:
external: true
```
To start off I recommend to set the `BACKEND_HOST` variable to `localhost` and start the stack without a docker network.
After you have started the stack and everything is working, you can add a docker network.
## Ports
### Change Exposed Ports
If you want to change the exposed ports, you can do this in the `docker-compose.yml` file like so:
```yaml
services:
mysql:
ports:
- "3306:3306"
backend:
ports:
- "8004:8004"
usr-frontend:
ports:
- "80:80"
```
### Remove Exposed Ports
If you want to remove the exposed ports, you can do this in the `docker-compose.override.yml` file like so:
```yaml
services:
mysql:
ports: !override [ ]
backend:
ports: !override [ ]
usr-frontend:
ports: !override [ ]
```
-19
View File
@@ -1,19 +0,0 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runner
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/dist .
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-23
View File
@@ -1,23 +0,0 @@
"use client";
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import * as React from "react";
import type { ReactNode } from "react";
import { ColorModeProvider as ThemeColorModeProvider } from "./color-mode";
export interface ColorModeProviderProps {
children: React.ReactNode;
}
export function ColorModeProvider({ children }: ColorModeProviderProps) {
// Wrap children with the real color-mode provider
return <ThemeColorModeProvider>{children}</ThemeColorModeProvider>;
}
export function Provider({ children }: { children: ReactNode }) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider>{children}</ColorModeProvider>
</ChakraProvider>
);
}
-23
View File
@@ -1,23 +0,0 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
export default defineConfig({
plugins: [tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
server: {
host: "0.0.0.0",
allowedHosts: ["insta.the1s.de"],
port: 8101,
watch: { usePolling: true },
hmr: {
host: "insta.the1s.de",
port: 8101,
protocol: "wss",
},
},
});
+23 -20
View File
@@ -19,17 +19,21 @@ A small full‑stack system to log in, view available items, reserve them for a
## Contents ## Contents
- Frontend: [frontend/](frontend) - Frontend: [frontend/](frontend)
- Vite/Tailwind config: [frontend/vite.config.ts](frontend/vite.config.ts), [frontend/tailwind.config.js](frontend/tailwind.config.js) - Vite/Tailwind
- App entry: [frontend/src/main.tsx](frontend/src/main.tsx), [frontend/src/App.tsx](frontend/src/App.tsx) config: [frontend/vite.config.ts](frontend/vite.config.ts), [frontend/tailwind.config.js](frontend/tailwind.config.js)
- UI: [frontend/src/layout/Layout.tsx](frontend/src/layout/Layout.tsx), [frontend/src/components](frontend/src/components) - App entry: [frontend/src/main.tsx](frontend/src/main.tsx), [frontend/src/App.tsx](frontend/src/App.tsx)
- 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) -
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) - Backend: [backend/](backend)
- Server: [backend/server.js](backend/server.js) - Server: [backend/server.js](backend/server.js)
- Routes: [backend/routes/api.js](backend/routes/api.js), [backend/routes/apiV2.js](backend/routes/apiV2.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) - DB +
- Schema/seed: [backend/scheme.sql](backend/scheme.sql) 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) - Docs: [docs/](docs)
- API docs (see below): [docs/backend_API_docs/README.md](docs/backend_API_docs/README.md) - API docs (see below): [docs/backend_API_docs/README.md](docs/backend_API_docs/README.md)
## Features (high‑level) ## Features (high‑level)
@@ -41,7 +45,8 @@ A small full‑stack system to log in, view available items, reserve them for a
Key frontend utilities: Key frontend utilities:
- [`utils.fetchData.fetchAllData`](frontend/src/utils/fetchData.ts): loads items, loans, and user loans after login. - [`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.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.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.userHandler.handleDeleteLoan`](frontend/src/utils/userHandler.ts): deletes a loan and syncs local state.
- [`utils.toastify.myToast`](frontend/src/utils/toastify.ts): toast notifications. - [`utils.toastify.myToast`](frontend/src/utils/toastify.ts): toast notifications.
@@ -54,20 +59,18 @@ UI flow (main screens):
## Development ## Development
- Scripts: see [frontend/package.json](frontend/package.json) and [backend/package.json](backend/package.json) - The NAAS (no-as-a-service) and the mysql database are running in a docker container
- Frontend: `npm run dev`, `npm run build`, `npm run preview`, `npm run lint` - `frontend`, `backend` and `admin` are both in a vite dev server and can be started by `npm run dev` in the root
- Backend: `npm start` directory.
- 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 ## 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). - 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`. - 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). - 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 ## API documentation
Refer to the dedicated API docs: Refer to the dedicated [API-Docs](docs/backend_API_docs/README.md)
`docs/backend_API_docs/README.md`
+13 -6
View File
@@ -1,19 +1,26 @@
FROM node:18 as builder # Build context is the repo root (npm workspaces share one root package-lock.json)
FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci COPY frontend/package.json frontend/
COPY admin/package.json admin/
COPY backend/package.json backend/
RUN npm ci -w admin
COPY . . ARG VITE_BACKEND_URL
RUN npm run build ENV VITE_BACKEND_URL=$VITE_BACKEND_URL
COPY admin/ admin/
RUN npm run build -w admin
FROM nginx:alpine AS runner FROM nginx:alpine AS runner
WORKDIR /usr/share/nginx/html WORKDIR /usr/share/nginx/html
COPY --from=builder /app/dist . COPY --from=builder /app/admin/dist .
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY admin/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+1 -1
View File
@@ -14,7 +14,7 @@ server {
} }
location /backend/ { location /backend/ {
proxy_pass http://demo_borrow_system-backend_v2:8102/; proxy_pass http://borrow-system_backend:8004/;
} }
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
-6066
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "admin", "name": "admin",
"private": true, "private": true,
"version": "v1.3.2 (dev)", "version": "1.3.2-dev",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+3 -7
View File
@@ -8,13 +8,9 @@ export default defineConfig({
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()], plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
allowedHosts: ["admin.insta.the1s.de"], port: 8003,
port: 8103, watch: {
watch: { usePolling: true }, usePolling: true,
hmr: {
host: "admin.insta.the1s.de",
port: 8103,
protocol: "wss",
}, },
}, },
}); });
+28
View File
@@ -0,0 +1,28 @@
# Build context is the repo root (npm workspaces share one root package-lock.json)
FROM node:20-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
COPY frontend/package.json frontend/
COPY admin/package.json admin/
COPY backend/package.json backend/
RUN npm ci -w backend --omit=dev
COPY backend/ backend/
# Generate info.json from the workspace package.json versions, without the "-dev" suffix
RUN node -e ' \
const fs = require("fs"); \
const v = (p) => "v" + JSON.parse(fs.readFileSync(p + "/package.json")).version.replace(/-dev$/, ""); \
fs.writeFileSync("backend/info.json", JSON.stringify({ \
"backend-info": { version: v("backend") }, \
"frontend-info": { version: v("frontend") }, \
"admin-panel-info": { version: v("admin") } \
}, null, 4)); \
'
WORKDIR /app/backend
EXPOSE 8004
CMD ["npm", "start"]
+11
View File
@@ -0,0 +1,11 @@
{
"backend-info": {
"version": "v2.2 (dev)"
},
"frontend-info": {
"version": "v2.2 (dev)"
},
"admin-panel-info": {
"version": "v1.4 (dev)"
}
}
@@ -1,10 +1,11 @@
{ {
"name": "backendv2", "name": "backend",
"version": "v2.1.1 (dev)", "version": "2.1.1-dev",
"private": true,
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "dev": "node --watch server.js",
"start": "node server.js" "start": "NODE_ENV=production node server.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -18,6 +19,6 @@
"express-rate-limit": "^8.4.1", "express-rate-limit": "^8.4.1",
"jose": "^6.0.12", "jose": "^6.0.12",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"nodemailer": "^8.0.6" "nodemailer": "^10.2.1"
} }
} }
@@ -29,14 +29,14 @@ export const createUser = async (
}; };
export const deleteUserById = async (userId) => { export const deleteUserById = async (userId) => {
const [result] = await pool.query("DELETE FROM users WHERE id = ? AND secret_user = false", [userId]); const [result] = await pool.query("DELETE FROM users WHERE id = ?", [userId]);
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
}; };
export const changePassword = async (username, newPassword) => { export const changePassword = async (username, newPassword) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ? AND secret_user = false", "UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?",
[newPassword, username], [newPassword, username],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
@@ -52,7 +52,7 @@ export const editUserById = async (
is_admin, is_admin,
) => { ) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ? AND secret_user = false", "UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?",
[first_name, last_name, role, email, is_admin, userId], [first_name, last_name, role, email, is_admin, userId],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
@@ -61,7 +61,7 @@ export const editUserById = async (
export const getAllUsers = async () => { export const getAllUsers = async () => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users WHERE secret_user = false", "SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users",
); );
if (result.length > 0) return { success: true, data: result }; if (result.length > 0) return { success: true, data: result };
return { success: false }; return { success: false };
@@ -69,7 +69,7 @@ export const getAllUsers = async () => {
export const getUserById = async (userId) => { export const getUserById = async (userId) => {
const [rows] = await pool.query( const [rows] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ? AND secret_user = false", "SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?",
[userId], [userId],
); );
if (rows.length === 0) { if (rows.length === 0) {
@@ -11,7 +11,6 @@ CREATE TABLE users (
is_admin bool NOT NULL DEFAULT false, is_admin bool NOT NULL DEFAULT false,
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
secret_user bool NOT NULL DEFAULT false,
PRIMARY KEY (id) PRIMARY KEY (id)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
+1 -1
View File
@@ -1,7 +1,7 @@
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import dotenv from "dotenv"; import dotenv from "dotenv";
import info from "./info.json" assert { type: "json" }; import info from "./info.json" with { type: "json" };
import { authenticate } from "./services/authentication.js"; import { authenticate } from "./services/authentication.js";
import { rateLimit } from "express-rate-limit"; import { rateLimit } from "express-rate-limit";
-12
View File
@@ -1,12 +0,0 @@
FROM node:20-alpine
ENV NODE_ENV=production
WORKDIR /backend
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8004
CMD ["npm", "start"]
-11
View File
@@ -1,11 +0,0 @@
{
"backend-info": {
"version": "v2.2 (demo)"
},
"frontend-info": {
"version": "v2.2 (demo)"
},
"admin-panel-info": {
"version": "v1.4 (demo)"
}
}
-1141
View File
File diff suppressed because it is too large Load Diff
-100
View File
@@ -1,100 +0,0 @@
USE borrow_system_new;
-- USERS
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin)
VALUES
('user1', 'passwordhash1', 'user1@example.com', 'First1', 'Last1', 1, false),
('user2', 'passwordhash2', 'user2@example.com', 'First2', 'Last2', 1, false),
('user3', 'passwordhash3', 'user3@example.com', 'First3', 'Last3', 2, false),
('admin1', 'passwordhash4', 'admin1@example.com', 'Admin', 'One', 9, true),
('admin2', 'passwordhash5', 'admin2@example.com', 'Admin', 'Two', 9, true);
-- ITEMS
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, door_key, last_borrowed_person, currently_borrowing)
VALUES
('Item1', 1, true, 1, 101, NULL, NULL),
('Item2', 1, true, 2, 102, 'user1', 'user1'),
('Item3', 2, true, 3, 103, 'user2', NULL),
('Item4', 1, false, NULL, NULL, NULL, NULL),
('Item5', 2, false, NULL, NULL, 'user3', 'user3');
-- LOANS
INSERT INTO loans (
username,
lockers,
loan_code,
start_date,
end_date,
take_date,
returned_date,
created_at,
loaned_items_id,
loaned_items_name,
deleted,
note
)
VALUES
(
'user1',
JSON_ARRAY('Locker1', 'Locker2'),
'123456',
'2026-02-01 09:00:00',
'2026-02-10 17:00:00',
'2026-02-01 09:15:00',
NULL,
'2026-02-01 09:00:00',
JSON_ARRAY(1, 2),
JSON_ARRAY('Item1', 'Item2'),
false,
'Erste allgemeine Ausleihe'
),
(
'user2',
JSON_ARRAY('Locker3'),
'234567',
'2026-02-02 10:00:00',
'2026-02-05 16:00:00',
'2026-02-02 10:05:00',
'2026-02-05 15:30:00',
'2026-02-02 10:00:00',
JSON_ARRAY(3),
JSON_ARRAY('Item3'),
false,
'Zurückgegeben vor Enddatum'
),
(
'user3',
JSON_ARRAY(),
'345678',
'2026-02-03 08:30:00',
'2026-02-15 18:00:00',
NULL,
NULL,
'2026-02-03 08:30:00',
JSON_ARRAY(5),
JSON_ARRAY('Item5'),
false,
'Noch ausgeliehen'
),
(
'user1',
JSON_ARRAY('Locker4'),
'456789',
'2025-12-01 09:00:00',
'2025-12-03 17:00:00',
'2025-12-01 09:10:00',
'2025-12-03 16:45:00',
'2025-12-01 09:00:00',
JSON_ARRAY(1),
JSON_ARRAY('Item1'),
true,
'Alte, gelöschte Ausleihe'
);
-- API KEYS
INSERT INTO apiKeys (api_key, entry_name)
VALUES
('10000001', 'Entry1'),
('10000002', 'Entry2'),
('10000003', 'Entry3'),
('10000004', 'Entry4');
-29
View File
@@ -1,29 +0,0 @@
# Changelog for upcoming version: vX.X
Introduction
## New features
-
## Improvements
-
## Fixed bugs
-
---
## New version numbers
**Backend:** vX.X
**Frontend:** vX.X
**Admin panel:** vX.X
---
-[Theis](https://portfolio-theis.de)
+56
View File
@@ -0,0 +1,56 @@
services:
# usr-frontend_v2:
# container_name: borrow_system-usr-frontend
# build: ./FrontendV2
# ports:
# - "8001:80"
# restart: always
# admin-frontend:
# container_name: borrow_system-admin-frontend
# build: ./admin
# ports:
# - "8003:80"
# restart: always
# backend_v2:
# container_name: borrow_system-backend_v2
# build: ./backend
# ports:
# - "8004:8004"
# environment:
# NODE_ENV: production
# DB_HOST: mysql_v2
# DB_USER: root
# DB_PASSWORD: ${DB_PASSWORD_V2}
# DB_NAME: borrow_system_new
# depends_on:
# - mysql_v2
# restart: always
mysql:
container_name: borrow-system_mysql
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
MYSQL_DATABASE: borrow_system_new
TZ: Europe/Berlin
volumes:
- mysql-v2-data:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
ports:
- "3310:3306"
no-as-a-service:
container_name: borrow-system_naas
ports:
- "3000:3000"
build:
context: ./no-as-a-service
dockerfile: Dockerfile
restart: always
volumes:
mysql-data:
mysql-v2-data:
+40 -41
View File
@@ -1,56 +1,59 @@
services: services:
demo_usr_frontend: usr-frontend:
container_name: demo_borrow_system-usr-frontend container_name: borrow-system_usr-frontend
networks: build:
proxynet: context: .
ipv4_address: 172.20.0.51 dockerfile: frontend/Dockerfile
build: ./FrontendV2 args:
restart: unless-stopped VITE_BACKEND_URL: ${BACKEND_URL}
restart: always
demo_admin_frontend: admin-frontend:
container_name: demo_borrow_system-admin-frontend container_name: borrow-system_admin-frontend
networks: build:
proxynet: context: .
ipv4_address: 172.20.0.52 dockerfile: admin/Dockerfile
build: ./admin args:
restart: unless-stopped VITE_BACKEND_URL: ${BACKEND_URL}
restart: always
demo_backend_v2: backend:
container_name: demo_borrow_system-backend_v2 container_name: borrow-system_backend
networks: build:
proxynet: context: .
ipv4_address: 172.20.0.50 dockerfile: backend/Dockerfile
build: ./backendV2
environment: environment:
NODE_ENV: production NODE_ENV: production
DB_HOST: demo_mysql_v2 DB_HOST: borrow-system_mysql
DB_USER: root DB_USER: root
DB_PASSWORD: ${DB_PASSWORD_V2} DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: borrow_system_new DB_NAME: borrow_system_new
MAIL_HOST: ${MAIL_HOST}
MAIL_PORT: ${MAIL_PORT}
MAIL_USER: ${MAIL_USER}
MAIL_PASSWORD: ${MAIL_PASSWORD}
MAIL_SENDEES_CONTACT: ${MAIL_SENDEES_CONTACT}
MAIL_SENDEES: ${MAIL_SENDEES}
NAAS_URL: ${NAAS_URL}
SECRET_KEY: ${SECRET_KEY}
depends_on: depends_on:
- demo_mysql_v2 - mysql
restart: unless-stopped restart: always
demo_mysql_v2: mysql:
container_name: demo_borrow_system-mysql-v2 container_name: borrow-system_mysql
networks:
proxynet:
ipv4_address: 172.20.0.53
image: mysql:8.0 image: mysql:8.0
restart: unless-stopped restart: always
environment: environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: borrow_system_new MYSQL_DATABASE: borrow_system_new
TZ: Europe/Berlin TZ: Europe/Berlin
volumes: volumes:
- demo_mysql-v2-data:/var/lib/mysql - mysql-v2-data:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
no-as-a-service: no-as-a-service:
container_name: demo_borrow_system-naas container_name: borrow-system_naas
networks:
proxynet:
ipv4_address: 172.20.0.54
build: build:
context: ./no-as-a-service context: ./no-as-a-service
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -58,8 +61,4 @@ services:
volumes: volumes:
mysql-data: mysql-data:
demo_mysql-v2-data: mysql-v2-data:
networks:
proxynet:
external: true
+26
View File
@@ -0,0 +1,26 @@
# Build context is the repo root (npm workspaces share one root package-lock.json)
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
COPY frontend/package.json frontend/
COPY admin/package.json admin/
COPY backend/package.json backend/
RUN npm ci -w frontend
ARG VITE_BACKEND_URL
ENV VITE_BACKEND_URL=$VITE_BACKEND_URL
COPY frontend/ frontend/
RUN npm run build -w frontend
FROM nginx:alpine AS runner
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/frontend/dist .
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
@@ -14,7 +14,7 @@ server {
} }
location /backend/ { location /backend/ {
proxy_pass http://demo_borrow_system-backend_v2:8004/; proxy_pass http://borrow-system_backend:8004/;
} }
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
@@ -1,7 +1,7 @@
{ {
"name": "admin", "name": "frontend",
"private": true, "private": true,
"version": "v2.1.2 (dev)", "version": "2.1.2-dev",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -45,6 +45,7 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"prettier": "^3.9.10",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.39.0", "typescript-eslint": "^8.39.0",
"vite": "^7.1.0", "vite": "^7.1.0",

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

@@ -16,6 +16,7 @@ import { Flex } from "@chakra-ui/react";
import { Footer } from "./components/footer/Footer"; import { Footer } from "./components/footer/Footer";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
import { ContactPage } from "./pages/ContactPage";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -80,6 +81,7 @@ function App() {
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/my-loans" element={<MyLoansPage />} /> <Route path="/my-loans" element={<MyLoansPage />} />
<Route path="/landingpage" element={<Landingpage />} /> <Route path="/landingpage" element={<Landingpage />} />
<Route path="/contact" element={<ContactPage />} />
</Route> </Route>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -23,6 +23,7 @@ import {
MoreVertical, MoreVertical,
Languages, Languages,
Table, Table,
ContactRound,
} from "lucide-react"; } from "lucide-react";
import { useUserContext } from "@/states/Context"; import { useUserContext } from "@/states/Context";
import { useState } from "react"; import { useState } from "react";
@@ -155,6 +156,16 @@ export const Header = () => {
</HStack> </HStack>
} }
/> />
<Menu.Item
value="contact"
onSelect={() => navigate("/contact", { replace: true })}
children={
<HStack gap={3}>
<ContactRound size={16} />
<Text as="span">{t("contact")}</Text>
</HStack>
}
/>
<Menu.Separator /> <Menu.Separator />
<Menu.Item <Menu.Item
value="logout" value="logout"
@@ -287,6 +298,17 @@ export const Header = () => {
</HStack> </HStack>
</Button> </Button>
</a> </a>
<Button
variant={"outline"}
onClick={() => navigate("/contact", { replace: true })}
>
<HStack gap={2}>
<ContactRound size={18} />
<Text as="span">{t("contact")}</Text>
</HStack>
</Button>
<Button onClick={logout} variant="outline" colorScheme="red"> <Button onClick={logout} variant="outline" colorScheme="red">
<HStack gap={2}> <HStack gap={2}>
<LogOut size={18} /> <LogOut size={18} />
+15
View File
@@ -0,0 +1,15 @@
"use client"
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
import {
ColorModeProvider,
type ColorModeProviderProps,
} from "./color-mode"
export function Provider(props: ColorModeProviderProps) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}
@@ -14,5 +14,5 @@ createRoot(document.getElementById("root")!).render(
<Provider> <Provider>
<App /> <App />
</Provider> </Provider>
</StrictMode> </StrictMode>,
); );
+111
View File
@@ -0,0 +1,111 @@
import {
Field,
Textarea,
Button,
Alert,
Container,
Text,
VStack,
Spinner,
} from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { API_BASE } from "@/config/api.config";
import Cookies from "js-cookie";
import { Header } from "@/components/Header";
interface Alert {
type: "info" | "warning" | "success" | "error" | "neutral";
headline: string;
text: string;
}
export const ContactPage = () => {
const { t } = useTranslation();
const [message, setMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const [alert, setAlert] = useState<Alert | null>(null);
const sendMessage = async () => {
setIsSending(true);
if (message.trim() === "") {
setAlert({
type: "error",
headline: t("contactPage_messageErrorHeadline"),
text: t("contactPage_messageErrorText2"),
});
setIsSending(false);
return;
}
// Logic to send the message
const result = await fetch(`${API_BASE}/api/users/contact`, {
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ message }),
});
if (result.ok) {
setAlert({
type: "success",
headline: t("contactPage_successHeadline"),
text: t("contactPage_successText"),
});
setMessage("");
} else if (result.status === 503) {
setAlert({
type: "error",
headline: t("serviceDeactivatedHeadline"),
text: t("contactPage_serviceDeactivatedText"),
});
} else {
setAlert({
type: "error",
headline: t("contactPage_errorHeadline"),
text: t("contactPage_errorText"),
});
}
setIsSending(false);
};
return (
<Container className="px-6 sm:px-8 pt-10">
<Header />
<Field.Root invalid={message === ""}>
<Field.Label>
<Text>{t("contactPage_messageDescription")}</Text>
<Field.RequiredIndicator />
</Field.Label>
<Textarea
placeholder={t("contactPage_messagePlaceholder")}
variant="subtle"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
{message === "" && (
<Field.ErrorText>{t("contactPage_messageErrorText")}</Field.ErrorText>
)}
</Field.Root>
{alert && (
<Alert.Root status={alert.type}>
<Alert.Indicator />
<Alert.Content>
<Alert.Title>{alert.headline}</Alert.Title>
<Alert.Description>{alert.text}</Alert.Description>
</Alert.Content>
</Alert.Root>
)}
{isSending && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">{t("loading")}</Text>
</VStack>
)}
<Button onClick={sendMessage}>{t("contactPage_sendButton")}</Button>
</Container>
);
};
@@ -68,7 +68,7 @@
"admin-status": "Admin-Status", "admin-status": "Admin-Status",
"first-name": "Vorname", "first-name": "Vorname",
"last-name": "Nachname", "last-name": "Nachname",
"app-title": "Ausleihsystem (demo)", "app-title": "Ausleihsystem",
"last-borrowed-person": "Zuletzt ausgeliehen von", "last-borrowed-person": "Zuletzt ausgeliehen von",
"currently-borrowed-by": "Derzeit ausgeliehen von", "currently-borrowed-by": "Derzeit ausgeliehen von",
"back": "Zurückgehen", "back": "Zurückgehen",
@@ -68,7 +68,7 @@
"admin-status": "Admin status", "admin-status": "Admin status",
"first-name": "First name", "first-name": "First name",
"last-name": "Last name", "last-name": "Last name",
"app-title": "Borrow System (demo)", "app-title": "Borrow System",
"last-borrowed-person": "Last borrowed by", "last-borrowed-person": "Last borrowed by",
"currently-borrowed-by": "Currently borrowed by", "currently-borrowed-by": "Currently borrowed by",
"back": "Go back", "back": "Go back",
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
server: {
host: "0.0.0.0",
port: 8001,
watch: {
usePolling: true,
},
},
});
+2
View File
@@ -0,0 +1,2 @@
[mysqld]
default-time-zone = Europe/Berlin
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "borrow-system",
"version": "2.3.0",
"private": true,
"workspaces": [
"backend",
"frontend",
"admin"
],
"description": "",
"main": "index.js",
"directories": {
"doc": "docs"
},
"devDependencies": {
"concurrently": "^10.0.6",
"prettier": "^3.9.6"
},
"scripts": {
"dev:docker": "docker compose -f docker-compose.dev.yml up -d",
"dev": "npm run dev:docker && concurrently --kill-others \"npm:dev:frontend\" \"npm:dev:backend\" \"npm:dev:admin\"",
"dev:frontend": "npm run dev -w frontend",
"dev:backend": "npm run dev -w backend",
"dev:admin": "npm run dev -w admin",
"dev:stop": "docker compose -f docker-compose.dev.yml down"
},
"repository": {
"type": "git",
"url": "https://git.the1s.de/theis.gaedigk/borrow-system.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}

Some files were not shown because too many files have changed in this diff Show More