Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc2c3d7f54 | ||
|
|
8b89f31178 | ||
|
|
c7933dfa3b | ||
|
|
c1bb628f22 | ||
|
|
941f9105d6 | ||
|
|
0070b0e7a0 | ||
|
|
63d3e2db07 | ||
|
|
9c17b2a495 | ||
|
|
929ec5ef10 | ||
|
|
67c704009e | ||
|
|
831de8b610 | ||
|
|
fd43e84c0c | ||
|
|
1ce8d33b0d | ||
|
|
c48da71cd5 | ||
|
|
8e35e81e8f | ||
|
|
95aae1c050 | ||
|
|
2cc5545ea8 |
@@ -0,0 +1,14 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/dist-ssr
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
**/.DS_Store
|
||||||
|
**/*.log
|
||||||
|
docs
|
||||||
|
pics
|
||||||
|
icon
|
||||||
|
no-as-a-service
|
||||||
@@ -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=
|
||||||
@@ -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/
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
@@ -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 [ ]
|
||||||
|
```
|
||||||
@@ -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;"]
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -19,14 +19,18 @@ 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
|
||||||
|
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)
|
- 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)
|
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 +
|
||||||
|
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)
|
- 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)
|
||||||
@@ -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`
|
|
||||||
|
|||||||
@@ -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;"]
|
||||||
@@ -14,7 +14,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /backend/ {
|
location /backend/ {
|
||||||
proxy_pass http://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?)$ {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ const pool = mysql
|
|||||||
.promise();
|
.promise();
|
||||||
|
|
||||||
export const getAllLoans = async () => {
|
export const getAllLoans = async () => {
|
||||||
const [rows] = await pool.query("SELECT * FROM loans");
|
const [rows] = await pool.query("SELECT * FROM loans WHERE deleted_admin = 0;");
|
||||||
return { success: true, data: rows };
|
return { success: true, data: rows };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,14 +119,6 @@ export const setTakeDateV2 = async (loanCode) => {
|
|||||||
return { message: "Failed to set take date", success: false };
|
return { message: "Failed to set take date", success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoansV2 = async () => {
|
|
||||||
const [result] = await pool.query("SELECT * FROM loans;");
|
|
||||||
if (result.length > 0) {
|
|
||||||
return { success: true, data: result };
|
|
||||||
}
|
|
||||||
return { success: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const openDoor = async (doorKey) => {
|
export const openDoor = async (doorKey) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
||||||
@@ -63,11 +63,9 @@ export const createLoanInDatabase = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemNames = itemIds
|
const itemNames = itemIds.map(
|
||||||
.map(
|
(id) => itemsRows.find((r) => Number(r.id) === Number(id)).item_name,
|
||||||
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name,
|
);
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
// Build lockers array (unique, only 2-digit numbers from safe_nr)
|
// Build lockers array (unique, only 2-digit numbers from safe_nr)
|
||||||
const lockers = [
|
const lockers = [
|
||||||
@@ -109,27 +107,24 @@ export const createLoanInDatabase = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique loan_code (retry a few times)
|
let index = false;
|
||||||
let loanCode = null;
|
let candidate;
|
||||||
for (let i = 0; i < 6; i++) {
|
let loanCode;
|
||||||
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
|
||||||
const [exists] = await conn.query(
|
// Generates 6-digit loan code
|
||||||
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
do {
|
||||||
|
candidate = Math.floor(100000 + Math.random() * 899999);
|
||||||
|
|
||||||
|
const [rows] = await conn.query(
|
||||||
|
"SELECT 1 FROM loans where loan_code = ? LIMIT 1",
|
||||||
[candidate],
|
[candidate],
|
||||||
);
|
);
|
||||||
if (exists.length === 0) {
|
|
||||||
|
if (rows.length == 0) {
|
||||||
|
index = true;
|
||||||
loanCode = candidate;
|
loanCode = candidate;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!loanCode) {
|
|
||||||
await conn.rollback();
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
code: "SERVER_ERROR",
|
|
||||||
message: "Failed to generate unique loan code",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
} while (index === false);
|
||||||
|
|
||||||
// Insert loan (now includes lockers)
|
// Insert loan (now includes lockers)
|
||||||
const [insertRes] = await conn.query(
|
const [insertRes] = await conn.query(
|
||||||
@@ -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";
|
||||||
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Changelog for upcoming version: v2.2
|
|
||||||
|
|
||||||
This update provides some new features for the design. It also contains some improvements and I have also fixed some bugs.
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- **Deactivatable services:** I have added the ability to deactivate services, which can be useful for maintenance or other purposes. The admin can activate and deactivate services in the admin panel. If a service is deactivated, it will not be available for users and they will get an error message if they try to use it. They will also get an warning banner on the homepage.
|
|
||||||
- **New Animations:** I have added some new animations to the frontend, which make the user experience more enjoyable.
|
|
||||||
- **New Icon:** I have added a new icon for the frontend, which is now also used in the header and the favicon. It is a dark version of the old icon, which fits better to the overall design. I have made it with Icon Composer. The old icon is still used for the admin panel, which has a light design. (Maybe I will change the admin panel design in the future...)
|
|
||||||
- **New Button:** When you go to your user card (over the user icon in the header) you have a new button "Click me". If you click it, you will get an message... _I am just saying: I have implemented the no-as-a-service code in to my Backend._
|
|
||||||
|
|
||||||
## Improvements
|
|
||||||
|
|
||||||
- The overview page now shows the note column and is overall better organised.
|
|
||||||
- Improved error logging
|
|
||||||
- If you try to delete a loan that has not been returned yet, you will get an 507 error code.
|
|
||||||
- When the admin deletes a loan, the loan will be still visible in the database, but it will be marked as deleted. This is to prevent data loss and to keep track of deleted loans.
|
|
||||||
- Mailer improvements: The mailer is now more clearly organised. Two large code files are now split into five smaller code files which are easier to maintain. Also the design of the mails has improved.
|
|
||||||
|
|
||||||
## Fixed bugs
|
|
||||||
|
|
||||||
- Fixed bug: #13
|
|
||||||
- Fixed bug for messaging when server has an error
|
|
||||||
- Fixed footer height
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## New version numbers
|
|
||||||
|
|
||||||
**Backend:** v2.2
|
|
||||||
|
|
||||||
**Frontend:** v2.2
|
|
||||||
|
|
||||||
**Admin panel:** v1.4
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
-[Theis](https://portfolio-theis.de)
|
|
||||||
@@ -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:
|
||||||
@@ -1,53 +1,59 @@
|
|||||||
services:
|
services:
|
||||||
# usr-frontend_v2:
|
usr-frontend:
|
||||||
# container_name: borrow_system-usr-frontend
|
container_name: borrow-system_usr-frontend
|
||||||
# build: ./FrontendV2
|
build:
|
||||||
# ports:
|
context: .
|
||||||
# - "8001:80"
|
dockerfile: frontend/Dockerfile
|
||||||
# restart: always
|
args:
|
||||||
|
VITE_BACKEND_URL: ${BACKEND_URL}
|
||||||
# admin-frontend:
|
|
||||||
# container_name: borrow_system-admin-frontend
|
|
||||||
# build: ./admin
|
|
||||||
# ports:
|
|
||||||
# - "8003:80"
|
|
||||||
# restart: always
|
|
||||||
|
|
||||||
backend_v2:
|
|
||||||
container_name: borrow_system-backend_v2
|
|
||||||
networks:
|
|
||||||
- proxynet
|
|
||||||
build: ./backendV2
|
|
||||||
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
|
restart: always
|
||||||
|
|
||||||
mysql_v2:
|
admin-frontend:
|
||||||
container_name: borrow_system-mysql-v2
|
container_name: borrow-system_admin-frontend
|
||||||
networks:
|
build:
|
||||||
- proxynet
|
context: .
|
||||||
|
dockerfile: admin/Dockerfile
|
||||||
|
args:
|
||||||
|
VITE_BACKEND_URL: ${BACKEND_URL}
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
backend:
|
||||||
|
container_name: borrow-system_backend
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DB_HOST: borrow-system_mysql
|
||||||
|
DB_USER: root
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
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:
|
||||||
|
- mysql
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
container_name: borrow-system_mysql
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: always
|
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:
|
||||||
- mysql-v2-data:/var/lib/mysql
|
- mysql-v2-data:/var/lib/mysql
|
||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||||
ports:
|
|
||||||
- "3310:3306"
|
|
||||||
|
|
||||||
no-as-a-service:
|
no-as-a-service:
|
||||||
container_name: borrow_system-naas
|
container_name: borrow-system_naas
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
build:
|
build:
|
||||||
context: ./no-as-a-service
|
context: ./no-as-a-service
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -56,7 +62,3 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
mysql-v2-data:
|
mysql-v2-data:
|
||||||
|
|
||||||
networks:
|
|
||||||
proxynet:
|
|
||||||
external: true
|
|
||||||
|
|||||||
@@ -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://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?)$ {
|
||||||
@@ -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 |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -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>,
|
||||||
);
|
);
|
||||||
@@ -121,10 +121,28 @@ export const MyLoansPage = () => {
|
|||||||
|
|
||||||
const formatDate = (iso: string | null) => {
|
const formatDate = (iso: string | null) => {
|
||||||
if (!iso) return "-";
|
if (!iso) return "-";
|
||||||
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
const date = new Date(iso);
|
||||||
if (!m) return iso;
|
if (isNaN(date.getTime())) return iso;
|
||||||
const [, y, M, d, h, min] = m;
|
return date.toLocaleString("de-DE", {
|
||||||
return `${d}.${M}.${y} ${h}:${min}`;
|
timeZone: "Europe/Berlin",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateAndTime = (isISO: boolean) => {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
if (isISO) {
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isISO) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTakeAction = async (loanCode: string) => {
|
const handleTakeAction = async (loanCode: string) => {
|
||||||
@@ -151,7 +169,7 @@ export const MyLoansPage = () => {
|
|||||||
setLoans((prev) =>
|
setLoans((prev) =>
|
||||||
prev.map((loan) =>
|
prev.map((loan) =>
|
||||||
loan.loan_code === loanCode
|
loan.loan_code === loanCode
|
||||||
? { ...loan, take_date: new Date().toISOString() }
|
? { ...loan, take_date: dateAndTime(true) }
|
||||||
: loan,
|
: loan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -191,7 +209,7 @@ export const MyLoansPage = () => {
|
|||||||
setLoans((prev) =>
|
setLoans((prev) =>
|
||||||
prev.map((loan) =>
|
prev.map((loan) =>
|
||||||
loan.loan_code === loanCode
|
loan.loan_code === loanCode
|
||||||
? { ...loan, returned_date: new Date().toISOString() }
|
? { ...loan, returned_date: dateAndTime(true) }
|
||||||
: loan,
|
: loan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"sure-delete-loan-2": "Für den Admin bleibt sie weiterhin sichtbar.",
|
"sure-delete-loan-2": "Für den Admin bleibt sie weiterhin sichtbar.",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"change-language": "Sprache ändern",
|
"change-language": "Sprache ändern",
|
||||||
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
|
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden. Das gesamte System ist auf die deutsche Zeitzone eingestellt.",
|
||||||
"optional-note": "Optionale Notiz",
|
"optional-note": "Optionale Notiz",
|
||||||
"note": "Notiz",
|
"note": "Notiz",
|
||||||
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und das Passwort ändern. Falls Sie weitere Änderungen benötigen, wenden Sie sich bitte an einen Administrator.",
|
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und das Passwort ändern. Falls Sie weitere Änderungen benötigen, wenden Sie sich bitte an einen Administrator.",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"sure-delete-loan-2": "It will remain visible to the admin.",
|
"sure-delete-loan-2": "It will remain visible to the admin.",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"change-language": "Change language",
|
"change-language": "Change language",
|
||||||
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
|
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such. The entire system is set to Berlin timezone.",
|
||||||
"optional-note": "Optional note",
|
"optional-note": "Optional note",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
"user-info-desc": "Here you can view your personal information and change your password. If you need to make further changes, please contact an administrator.",
|
"user-info-desc": "Here you can view your personal information and change your password. If you need to make further changes, please contact an administrator.",
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[mysqld]
|
||||||
|
default-time-zone = Europe/Berlin
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |