Compare commits

..

18 Commits

Author SHA1 Message Date
96f4ca6fd9 edited docker config 2026-01-21 16:32:45 +01:00
e3e1037a85 feat: update SuccessPage with countdown and return button; localize return button text 2026-01-21 16:27:32 +01:00
9445f5d417 refactor: update MainForm layout and payment method selection; localize submit button text 2026-01-21 16:15:52 +01:00
308b21ae6c improved choosing payment method 2026-01-21 15:51:38 +01:00
697e84b5ea fixed Bug: Database 2026-01-21 14:27:24 +01:00
09bbec6ce3 added proxynet 2026-01-21 14:24:53 +01:00
adbf60df6c edited dockerfile again 2026-01-21 14:04:37 +01:00
acc1ecc88c removed hash 2026-01-21 14:02:40 +01:00
ed48d7552e docker 2026-01-21 14:01:38 +01:00
9f22f4aa35 docker 2026-01-21 13:59:52 +01:00
0b1e46779e docker 2026-01-21 13:56:22 +01:00
0a7f83f6e8 edited docker compose 2026-01-21 13:45:40 +01:00
4970f80b70 removed password 2026-01-21 13:38:31 +01:00
2feb1ec8c8 hash 2026-01-21 13:05:41 +01:00
ef4734f886 edited hash 2026-01-21 13:02:44 +01:00
82edf655a3 edited port config 2026-01-21 12:54:33 +01:00
81a32faea3 edited docker config 2026-01-21 12:47:16 +01:00
8396840149 fixed bug: ids are not updating 2026-01-21 12:46:56 +01:00
8 changed files with 265 additions and 56 deletions

View File

@@ -12,7 +12,7 @@ const pool = mysql
.promise(); .promise();
export const getUser = async () => { export const getUser = async () => {
const [rows] = await pool.query("SELECT username FROM usersNEW"); const [rows] = await pool.query("SELECT username FROM users");
if (rows.length > 0) { if (rows.length > 0) {
const users = rows.map((r) => r.username); const users = rows.map((r) => r.username);
@@ -23,7 +23,7 @@ export const getUser = async () => {
}; };
export const confirmUser = async (username) => { export const confirmUser = async (username) => {
const [rows] = await pool.query("SELECT * FROM usersNEW WHERE username = ?", [ const [rows] = await pool.query("SELECT * FROM users WHERE username = ?", [
username, username,
]); ]);

View File

@@ -4,11 +4,15 @@ services:
hostname: lose-verkaufen hostname: lose-verkaufen
build: ./frontend build: ./frontend
networks: networks:
- ca-lose-internal ca-lose-internal:
ipv4_address: 172.25.0.2
proxynet:
ipv4_address: 172.20.0.61
restart: unless-stopped restart: unless-stopped
backend: backend:
container_name: ca-lose-backend container_name: ca-lose-backend
hostname: backend
build: ./backend build: ./backend
environment: environment:
NODE_ENV: production NODE_ENV: production
@@ -19,11 +23,13 @@ services:
depends_on: depends_on:
- database - database
networks: networks:
- ca-lose-internal ca-lose-internal:
ipv4_address: 172.25.0.3
restart: unless-stopped restart: unless-stopped
database: database:
container_name: ca-lose-mysql container_name: ca-lose-mysql
hostname: database
image: mysql:8.0 image: mysql:8.0
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -34,37 +40,65 @@ services:
- ca-lose_mysql:/var/lib/mysql - ca-lose_mysql:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
networks: networks:
- ca-lose-internal ca-lose-internal:
ipv4_address: 172.25.0.4
proxynet:
ipv4_address: 172.20.0.60
# DNS Server für Hostname-Auflösung innerhalb des VPN
dnsmasq:
container_name: ca-lose-dns
image: andyshinn/dnsmasq:latest
restart: unless-stopped
cap_add:
- NET_ADMIN
command: >
--no-daemon
--log-queries
--address=/lose-verkaufen/172.25.0.2
--address=/frontend/172.25.0.2
--address=/backend/172.25.0.3
--address=/database/172.25.0.4
--address=/wg-admin/172.25.0.10
networks:
ca-lose-internal:
ipv4_address: 172.25.0.53
# WireGuard VPN mit Web-UI (wg-easy)
wireguard: wireguard:
image: lscr.io/linuxserver/wireguard:latest image: ghcr.io/wg-easy/wg-easy:latest
container_name: ca-lose-wireguard container_name: ca-lose-wireguard
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- SYS_MODULE #optional - SYS_MODULE
environment: environment:
- PUID=1000 LANG: de
- PGID=1000 WG_HOST: dus3.the1s.de
- TZ=Etc/UTC WG_PORT: "51830"
- SERVERURL=dus3.the1s.de #optional PORT: "51821"
- SERVERPORT=51830 #optional WG_DEFAULT_ADDRESS: 10.14.14.x
- PEERS=2 #optional WG_DEFAULT_DNS: "172.25.0.53"
- PEERDNS=auto #optional WG_ALLOWED_IPS: 172.25.0.0/24
- INTERNAL_SUBNET=10.13.14.0 #optional WG_PERSISTENT_KEEPALIVE: "25"
- ALLOWEDIPS=10.13.14.0/24,172.25.0.0/24 #optional WG_POST_UP: "iptables -t nat -A POSTROUTING -s 10.14.14.0/24 -o eth0 -j MASQUERADE; iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -A FORWARD -i wg0 -d 172.25.0.2 -j ACCEPT; iptables -A FORWARD -i wg0 -d 172.25.0.53 -j ACCEPT; iptables -A FORWARD -i wg0 -j DROP"
- PERSISTENTKEEPALIVE_PEERS= #optional WG_POST_DOWN: "iptables -t nat -D POSTROUTING -s 10.14.14.0/24 -o eth0 -j MASQUERADE; iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -D FORWARD -i wg0 -d 172.25.0.2 -j ACCEPT; iptables -D FORWARD -i wg0 -d 172.25.0.53 -j ACCEPT; iptables -D FORWARD -i wg0 -j DROP"
- LOG_CONFS=true #optional
volumes: volumes:
- ./config:/config - wireguard-data:/etc/wireguard
- /lib/modules:/lib/modules #optional - /lib/modules:/lib/modules:ro
ports: ports:
- 51830:51830/udp - "51830:51830/udp"
sysctls: sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1 - net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped restart: unless-stopped
depends_on:
- dnsmasq
- frontend
networks: networks:
ca-lose-internal: ca-lose-internal:
ipv4_address: 172.25.0.10 ipv4_address: 172.25.0.10
proxynet:
ipv4_address: 172.20.0.50
volumes: volumes:
ca-lose_mysql: ca-lose_mysql:
@@ -76,3 +110,6 @@ networks:
ipam: ipam:
config: config:
- subnet: 172.25.0.0/24 - subnet: 172.25.0.0/24
gateway: 172.25.0.1
proxynet:
external: true

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>Lose verkaufen</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -9,11 +9,13 @@ import {
Chip, Chip,
Box, Box,
Paper, Paper,
Typography,
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { submitFormData } from "../utils/sender"; import { submitFormData } from "../utils/sender";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import * as React from "react";
interface Message { interface Message {
type: "error" | "info" | "success" | "warning"; type: "error" | "info" | "success" | "warning";
@@ -69,7 +71,7 @@ export const MainForm = () => {
setSelectedUser(cookieUser); setSelectedUser(cookieUser);
confirmUser(cookieUser); confirmUser(cookieUser);
} }
}, []); }, [isLoading]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value }); setFormData({ ...formData, [e.target.name]: e.target.value });
@@ -113,11 +115,33 @@ export const MainForm = () => {
}; };
return ( return (
<Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4"> <Box
className="bg-gray-100 py-10 px-4"
sx={{
minHeight: "100vh",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Paper <Paper
elevation={3} elevation={6}
className="w-full max-w-md p-6 rounded-lg" className="w-full rounded-2xl"
sx={{ backgroundColor: "#fff" }} sx={{
backgroundColor: "#fff",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
width: "100%",
maxWidth: {
xs: 360, // kompakte Handy-Ansicht
sm: 420, // kleine Tablets / große Phones
md: 480, // Desktop, bleibt angenehm schmal
},
padding: {
xs: "1.5rem",
sm: "2rem",
},
}}
> >
<form <form
onSubmit={(e) => { onSubmit={(e) => {
@@ -149,10 +173,14 @@ export const MainForm = () => {
color="primary" color="primary"
sx={{ sx={{
alignSelf: "flex-start", alignSelf: "flex-start",
fontWeight: "bold", fontWeight: 500,
fontSize: "1rem", fontSize: "0.9rem",
py: 2, mt: 0.5,
px: 1, mb: 0.5,
py: 0.5,
px: 1.25,
borderRadius: "999px",
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
}} }}
/> />
@@ -234,7 +262,14 @@ export const MainForm = () => {
{/* Invoice Fields */} {/* Invoice Fields */}
{invoice && ( {invoice && (
<Box className="flex flex-col gap-3 pt-2 border-t border-gray-200"> <Box
className="flex flex-col gap-2 pt-3 mt-2"
sx={{
borderTop: "2px solid",
borderColor: "primary.light",
borderRadius: "0",
}}
>
<TextField <TextField
required required
id="company-name" id="company-name"
@@ -317,26 +352,118 @@ export const MainForm = () => {
/> />
</Box> </Box>
)} )}
{/* Payment Methods */} {/* Payment Methods */}
<Box className="flex justify-center gap-4 pt-2"> <Box className="flex flex-col gap-2 mt-2">
<FormControlLabel control={<Checkbox />} label={t("cash")} /> <Typography
<FormControlLabel control={<Checkbox />} label={t("paypal")} /> component="label"
<FormControlLabel control={<Checkbox />} label={t("transfer")} /> sx={{
fontSize: "0.875rem",
fontWeight: 500,
color: "text.secondary",
display: "flex",
alignItems: "center",
gap: 0.5,
}}
>
{t("select-payment-method")} *
</Typography>
<Box className="flex gap-2 flex-wrap">
<Button
variant={
formData.paymentMethod === "bar" ? "contained" : "outlined"
}
onClick={() =>
setFormData({ ...formData, paymentMethod: "bar" })
}
sx={{
flex: 1,
minWidth: "100px",
py: 1.5,
borderRadius: "12px",
textTransform: "none",
fontWeight: formData.paymentMethod === "bar" ? 600 : 400,
}}
>
{t("cash")}
</Button>
<Button
variant={
formData.paymentMethod === "paypal" ? "contained" : "outlined"
}
onClick={() =>
setFormData({ ...formData, paymentMethod: "paypal" })
}
sx={{
flex: 1,
minWidth: "100px",
py: 1.5,
borderRadius: "12px",
textTransform: "none",
fontWeight: formData.paymentMethod === "paypal" ? 600 : 400,
}}
>
{t("paypal")}
</Button>
<Button
variant={
formData.paymentMethod === "andere" ? "contained" : "outlined"
}
onClick={() =>
setFormData({ ...formData, paymentMethod: "andere" })
}
sx={{
flex: 1,
minWidth: "100px",
py: 1.5,
borderRadius: "12px",
textTransform: "none",
fontWeight: formData.paymentMethod === "andere" ? 600 : 400,
}}
>
{t("transfer")}
</Button>
</Box>
{!formData.paymentMethod && (
<input
tabIndex={-1}
autoComplete="off"
style={{
opacity: 0,
width: 0,
height: 0,
position: "absolute",
}}
required
value={formData.paymentMethod}
onChange={() => {}}
/>
)}
</Box> </Box>
{/* Submit Button */} {/* Submit Button */}
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
disabled={isLoading} disabled={isLoading || !formData.paymentMethod}
fullWidth fullWidth
size="large" size="large"
sx={{ sx={{
mt: 2, mt: 3,
py: 1.5, py: 2,
textTransform: "uppercase", textTransform: "uppercase",
fontWeight: "bold", fontWeight: "bold",
borderRadius: "12px",
fontSize: "1rem",
background: "linear-gradient(135deg, #1976d2 0%, #1565c0 100%)",
boxShadow: "0 4px 14px 0 rgba(25, 118, 210, 0.39)",
"&:hover": {
background: "linear-gradient(135deg, #1565c0 0%, #0d47a1 100%)",
boxShadow: "0 6px 20px 0 rgba(25, 118, 210, 0.5)",
},
"&:disabled": {
background: "#e0e0e0",
boxShadow: "none",
},
}} }}
> >
{isLoading ? ( {isLoading ? (

View File

@@ -1,4 +1,4 @@
import { Box, Paper, Typography, Chip } from "@mui/material"; import { Box, Paper, Typography, Chip, Button } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { CircleCheck } from "lucide-react"; import { CircleCheck } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -8,6 +8,7 @@ export const SuccessPage = () => {
const [tickets, setNumberOfTickets] = useState<number>(0); const [tickets, setNumberOfTickets] = useState<number>(0);
const [animate, setAnimate] = useState(false); const [animate, setAnimate] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [seconds, setSeconds] = useState(30);
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
@@ -20,6 +21,17 @@ export const SuccessPage = () => {
setTimeout(() => setAnimate(true), 100); setTimeout(() => setAnimate(true), 100);
}, []); }, []);
useEffect(() => {
if (seconds === 0) {
window.location.href = "/";
return;
}
const timer = setTimeout(() => setSeconds(seconds - 1), 1000);
return () => clearTimeout(timer);
}, [seconds]);
return ( return (
<Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4"> <Box className="min-h-screen bg-gray-800 flex items-center justify-center p-4">
<Paper <Paper
@@ -132,6 +144,30 @@ export const SuccessPage = () => {
</Box> </Box>
)} )}
{/* Return button */}
<Box
sx={{
mb: 3,
transition: "all 0.5s ease-in-out 0.4s",
transform: animate ? "translateY(0)" : "translateY(20px)",
opacity: animate ? 1 : 0,
}}
>
<Button
href="/"
variant="contained"
color="primary"
sx={{
fontWeight: "bold",
fontSize: "1.25rem",
py: 3,
px: 2,
}}
>
{seconds + " " + t("return-to-homepage")}
</Button>
</Box>
{/* Additional Info */} {/* Additional Info */}
<Box <Box
sx={{ sx={{

View File

@@ -8,7 +8,7 @@
"street": "Straße + Haus Nr.", "street": "Straße + Haus Nr.",
"postal-code": "Plz + Stadt", "postal-code": "Plz + Stadt",
"email": "E-Mail", "email": "E-Mail",
"submit": "Kaufen", "submit": "Abschicken",
"failed-to-load-users": "Das Laden der Benutzer ist fehlgeschlagen.", "failed-to-load-users": "Das Laden der Benutzer ist fehlgeschlagen.",
"user": "Benutzer", "user": "Benutzer",
"next-id": "Nächste Eintragsnummer: ", "next-id": "Nächste Eintragsnummer: ",
@@ -18,8 +18,11 @@
"error": "Fehler", "error": "Fehler",
"cash": "Bar", "cash": "Bar",
"paypal": "PayPal", "paypal": "PayPal",
"transfer": "Überweisung", "transfer": "Andere (notieren)",
"ticket-payment": "Sie haben erflogreich {count} {count, plural, one {Los} other {Lose}} gekauft.", "ticket-payment_one": "Sie haben erfolgreich {{count}} Los gekauft.",
"ticket-payment_other": "Sie haben erfolgreich {{count}} Lose gekauft.",
"entry-id": "Eintrags-ID", "entry-id": "Eintrags-ID",
"thank-you": "Vielen Dank für Ihre Unterstützung der Claudius Akademie! Wir wünschen Ihnen viel Glück mit dem Los." "thank-you": "Vielen Dank für Ihre Unterstützung der Claudius Akademie! Wir wünschen Ihnen viel Glück mit dem Los.",
"select-payment-method": "Zahlungsmethode auswählen",
"return-to-homepage": "Zurück"
} }

View File

@@ -7,7 +7,7 @@
"street": "Street + House No.", "street": "Street + House No.",
"postal-code": "Postal Code + City", "postal-code": "Postal Code + City",
"email": "Email", "email": "Email",
"submit": "Buy", "submit": "Submit Form",
"failed-to-load-users": "Failed to load users.", "failed-to-load-users": "Failed to load users.",
"user": "User", "user": "User",
"next-id": "Next Entry Number: ", "next-id": "Next Entry Number: ",
@@ -17,11 +17,13 @@
"error": "Error", "error": "Error",
"cash": "Cash", "cash": "Cash",
"paypal": "PayPal", "paypal": "PayPal",
"transfer": "Bank Transfer", "transfer": "Other (note down)",
"ticket-payment_one": "You have successfully purchased {{count}} ticket.", "ticket-payment_one": "You have successfully purchased {{count}} ticket.",
"ticket-payment_other": "You have successfully purchased {{count}} tickets.", "ticket-payment_other": "You have successfully purchased {{count}} tickets.",
"ticket": "ticket", "ticket": "Ticket",
"tickets": "tickets", "tickets": "Tickets",
"entry-id": "Entry ID", "entry-id": "Entry ID",
"thank-you": "Thank you for supporting the Claudius Akademie! We wish you the best of luck with your ticket." "thank-you": "Thank you for supporting the Claudius Akademie! We wish you the best of luck with your ticket.",
"select-payment-method": "Select Payment Method",
"return-to-homepage": "Return"
} }

View File

@@ -15,14 +15,18 @@ interface FormData {
} }
export const submitFormData = async (data: FormData, username: string) => { export const submitFormData = async (data: FormData, username: string) => {
console.log(data);
try { try {
const response = await fetch(`http://localhost:8004/default/new-entry?username=${username}`, { const response = await fetch(
method: "POST", `http://localhost:8004/default/new-entry?username=${username}`,
headers: { {
"Content-Type": "application/json", method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}, },
body: JSON.stringify(data), );
});
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();