Compare commits
18 Commits
24cfcd61ed
...
v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
1ba4f865de | |||
acd9fda529 | |||
f63e04f7b0 | |||
ff2f37f718 | |||
134e601d57 | |||
842c541c13 | |||
41cee8af1d | |||
190cded4e5 | |||
4ecdd40613 | |||
7c73ac7749 | |||
afc6bcae83 | |||
ecf5cf9773 | |||
33b2a2d966 | |||
e12f631c4a | |||
3c71785849 | |||
9308b914ff | |||
4fccd54805 | |||
83d8644ef9 |
123
.gitea/workflows/release.yml
Normal file
123
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,123 @@
|
||||
# Git(tea) Actions workflow: Build and publish standalone binaries **plus** bundled `static/` assets
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
# ✧ Builds the Go‑based WoL server for four targets **and** packt das Verzeichnis
|
||||
# `static` zusammen mit der Binary, sodass es relativ zur ausführbaren Datei
|
||||
# liegt (wichtig für die eingebauten Bootstrap‑Assets & favicon).
|
||||
#
|
||||
# • linux/amd64 → wol-server-linux-amd64.tar.gz
|
||||
# • linux/arm64 → wol-server-linux-arm64.tar.gz
|
||||
# • linux/arm/v7 → wol-server-linux-armv7.tar.gz
|
||||
# • windows/amd64 → wol-server-windows-amd64.zip
|
||||
#
|
||||
# ✧ Artefakte landen im Workflow und – bei Tag‑Push (vX.Y.Z) – als Release‑Assets.
|
||||
#
|
||||
# Secrets/variables:
|
||||
# GITEA_TOKEN – optional, falls default token keine Release‑Rechte hat.
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
|
||||
name: build-binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "v*" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-fast
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
ext: ""
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
ext: ""
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
goarm: "7"
|
||||
ext: ""
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
ext: ".exe"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.24"
|
||||
BINARY_NAME: trading-server
|
||||
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Build ${{ matrix.goos }}/${{ matrix.goarch }}${{ matrix.goarm && format('/v{0}', matrix.goarm) || '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p dist/package
|
||||
if [ -n "${{ matrix.goarm }}" ]; then export GOARM=${{ matrix.goarm }}; fi
|
||||
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -trimpath -ldflags "-s -w" \
|
||||
-o "dist/package/${BINARY_NAME}${{ matrix.ext }}" .
|
||||
# Assets: statisches Verzeichnis beilegen
|
||||
cp -r static dist/package/
|
||||
|
||||
- name: Package archive with static assets
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
cd dist
|
||||
if [ "${{ matrix.goos }}" == "windows" ]; then
|
||||
ZIP_NAME="${BINARY_NAME}-windows-amd64.zip"
|
||||
(cd package && zip -r "../$ZIP_NAME" .)
|
||||
else
|
||||
ARCH_SUFFIX="${{ matrix.goarch }}"
|
||||
if [ "${{ matrix.goarch }}" == "arm" ]; then ARCH_SUFFIX="armv${{ matrix.goarm }}"; fi
|
||||
TAR_NAME="${BINARY_NAME}-${{ matrix.goos }}-${ARCH_SUFFIX}.tar.gz"
|
||||
tar -czf "$TAR_NAME" -C package .
|
||||
fi
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm && format('v{0}', matrix.goarm) || '' }}
|
||||
path: dist/*.tar.gz
|
||||
if-no-files-found: ignore
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-amd64
|
||||
path: dist/*.zip
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Release Schritt für Tag‑Pushes
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
runs-on: ubuntu-fast
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: ./dist
|
||||
|
||||
- name: Create / Update release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
|
||||
with:
|
||||
name: "Release ${{ github.ref_name }}"
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
dist/**/${BINARY_NAME}-*.tar.gz
|
||||
dist/**/${BINARY_NAME}-*.zip
|
BIN
dynamicsrc/footerlower.webp
Normal file
BIN
dynamicsrc/footerlower.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
dynamicsrc/footerupper.webp
Normal file
BIN
dynamicsrc/footerupper.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
3
go.mod
3
go.mod
@@ -10,8 +10,9 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
30
go.sum
30
go.sum
@@ -1,5 +1,7 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -8,16 +10,40 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
378
main.go
378
main.go
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -12,8 +14,10 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
|
||||
)
|
||||
|
||||
@@ -42,28 +46,29 @@ var (
|
||||
productive = Enabled("KT_PRODUCTIVE", false)
|
||||
hasimpressum = Enabled("KT_HASIMPRESSUM", false)
|
||||
impressum = GetENV("KT_IMPRESSUM", "")
|
||||
hashedPassword = ""
|
||||
orte = []string{}
|
||||
schiffe = []string{
|
||||
"", "100i", "125a", "135c", "Arrow", "Aurora CL", "Aurora ES", "Aurora LN", "Aurora LX", "Aurora MR",
|
||||
"Avenger Stalker", "Avenger Titan", "Avenger Titan Renegade", "Avenger Warlock",
|
||||
"Blade", "Buccaneer", "C1 Spirit", "C2 Hercules Starlifter", "C8 Pisces", "C8R Pisces Rescue",
|
||||
"Blade", "Buccaneer", "C1 Spirit", "C2 Hercules Starlifter", "M2 Hercules Starlifter", "A2 Hercules Starlifter", "C8 Pisces", "C8R Pisces Rescue",
|
||||
"C8X Pisces Expedition", "Carrack", "Caterpillar", "Constellation Andromeda",
|
||||
"Constellation Aquila", "Constellation Phoenix", "Constellation Taurus", "Corsair",
|
||||
"Cutlass Black", "Cutlass Blue", "Cutlass Red", "Cutter", "Defender", "Eclipse",
|
||||
"Freelancer", "Freelancer DUR", "Freelancer MAX", "Gladiator", "Gladius", "Glaive",
|
||||
"Hammerhead", "Hawk", "Herald", "Hurricane", "Idris-K", "Idris-M", "Javelin",
|
||||
"Hammerhead", "Hawk", "Herald", "Hurricane", "Idris-P", "Idris-K", "Idris-M", "Javelin",
|
||||
"Khartu-Al", "Kraken", "M50", "Merchantman", "Mercury Star Runner", "Mustang Alpha",
|
||||
"Mustang Beta", "Mustang Delta", "Mustang Gamma", "Mustang Omega", "Nomad",
|
||||
"Orion", "P-52 Merlin", "P-72 Archimedes", "Prospector", "Prowler", "Prowler Utility", "Raft",
|
||||
"Reclaimer", "Redeemer", "Reliant Kore", "Reliant Mako", "Reliant Sen", "Reliant Tana",
|
||||
"Retaliator Bomber", "Sabre Peregrine", "Starfarer Gemini", "Talon", "Talon Shrike",
|
||||
"Retaliator", "Sabre Peregrine", "Starfarer Gemini", "Talon", "Talon Shrike",
|
||||
"Terrapin", "Vulture", "Hull-A", "Hull-C", "Zeus ES", "Zeus CL",
|
||||
// …weitere Capital- und Concept-Schiffe sind ebenfalls bekannt!
|
||||
}
|
||||
waren = []string{
|
||||
"", "Laranite", "Titanium", "Medical Supplies", "Gold", "Agricium", "Hydrogen", "Nitrogen", "Astatine",
|
||||
"Iodine", "Aluminium", "Copper", "Lithium", "Silicon", "Tungsten", "Aphorite", "Beryl", "Bexalite",
|
||||
"Corundum", "Diamond", "Dolivine", "Hadanite", "Hephaestanite", "Laranite", "Quartz", "Taranite",
|
||||
"", "Laranite", "Titanium", "Medical Supplies", "Gold", "Agricium", "Hydrogen", "Hydrogen Fuel", "Nitrogen", "Astatine", "Processed Food", "Scrap", "Recycled Material Composite", "Agricultural Supplies",
|
||||
"Iodine", "Aluminium", "Copper", "Lithium", "Silicon", "Tungsten", "Aphorite", "Beryl", "Bexalite", "Waste", "Osoian hides", "Borase", "WiDoW", "Fresh Food", "Heart of the Woods", "Pressurized Ice", "Atlasium",
|
||||
"Corundum", "Diamond", "Dolivine", "Hadanite", "Hephaestanite", "Laranite", "Quartz", "Taranite", "Stims", "Carbon", "Slam", "Distilled Spirits", "Maze", "Gasping Weevil Eggs", "E'tam", "Iron", "Methane",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -143,6 +148,9 @@ type Monatsstatistik struct {
|
||||
Monat string // z. B. "07.2025"
|
||||
Summe float64 // bezahlte
|
||||
SummeOffen float64 // noch nicht bezahlt
|
||||
Prozent float64
|
||||
ProzentOffen float64
|
||||
Eintraege []Entry
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{
|
||||
@@ -186,7 +194,37 @@ func reverse(s string) string {
|
||||
|
||||
func isAuthenticated(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("session")
|
||||
return err == nil && cookie.Value == "authenticated"
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Prüfen, ob der Token im sessionStore existiert
|
||||
_, ok := sessionStore[cookie.Value]
|
||||
return ok
|
||||
}
|
||||
|
||||
var sessionStore = make(map[string]string) // token → username
|
||||
var loginAttempts = make(map[string]int)
|
||||
var loginLastAttempt = make(map[string]time.Time)
|
||||
var loginBlockedUntil = make(map[string]time.Time)
|
||||
var loginMutex sync.Mutex
|
||||
|
||||
func hashPassword(pw string) string {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
func checkPasswordHash(pw, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func generateSessionToken() string {
|
||||
b := make([]byte, 32)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "" // handle error besser im echten Code
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -216,6 +254,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
hashedPassword = hashPassword(password)
|
||||
|
||||
var pois []POI
|
||||
if err := json.Unmarshal(data, &pois); err != nil {
|
||||
panic(err)
|
||||
@@ -242,34 +282,86 @@ func main() {
|
||||
}
|
||||
|
||||
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := strings.Split(r.RemoteAddr, ":")[0]
|
||||
|
||||
loginMutex.Lock()
|
||||
blockUntil, blocked := loginBlockedUntil[ip]
|
||||
if blocked && time.Now().Before(blockUntil) {
|
||||
loginMutex.Unlock()
|
||||
http.Error(w, "Zu viele Fehlversuche. Bitte versuch es später erneut.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
loginMutex.Unlock()
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
r.ParseForm()
|
||||
user := r.FormValue("username")
|
||||
pass := r.FormValue("password")
|
||||
if user == username && pass == password {
|
||||
|
||||
if user == username && checkPasswordHash(pass, hashedPassword) {
|
||||
token := generateSessionToken()
|
||||
|
||||
// Speichere Session
|
||||
sessionStore[token] = user
|
||||
|
||||
// Cookie setzen
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "authenticated",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
// Erfolgreich -> Versuche zurücksetzen
|
||||
loginMutex.Lock()
|
||||
delete(loginAttempts, ip)
|
||||
delete(loginLastAttempt, ip)
|
||||
delete(loginBlockedUntil, ip)
|
||||
loginMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Fehlversuch behandeln
|
||||
loginMutex.Lock()
|
||||
loginAttempts[ip]++
|
||||
loginLastAttempt[ip] = time.Now()
|
||||
if loginAttempts[ip] >= 5 {
|
||||
loginBlockedUntil[ip] = time.Now().Add(10 * time.Minute)
|
||||
}
|
||||
loginMutex.Unlock()
|
||||
|
||||
http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// GET: Login-Formular
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(loginForm))
|
||||
})
|
||||
|
||||
http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err == nil {
|
||||
token := cookie.Value
|
||||
// Token aus dem serverseitigen Store löschen
|
||||
delete(sessionStore, token)
|
||||
|
||||
// Cookie ungültig machen
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
@@ -282,6 +374,11 @@ func main() {
|
||||
if id != "" {
|
||||
db.Exec("DELETE FROM eintraege WHERE id = ?", id)
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.LastComputed = time.Time{} // auf null zurücksetzen
|
||||
cacheMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
@@ -295,10 +392,16 @@ func main() {
|
||||
|
||||
// Auto-Increment-Zähler zurücksetzen
|
||||
db.Exec("DELETE FROM sqlite_sequence WHERE name='eintraege'")
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.LastComputed = time.Time{} // auf null zurücksetzen
|
||||
cacheMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/markaspaid", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Method", "/markaspaid", r.Method)
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -308,10 +411,16 @@ func main() {
|
||||
if id != "" {
|
||||
db.Exec("UPDATE eintraege SET bezahlt = 1 WHERE id = ?", id)
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.LastComputed = time.Time{} // auf null zurücksetzen
|
||||
cacheMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/unmarkaspaid", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Method", "/unmarkaspaid", r.Method)
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -321,10 +430,16 @@ func main() {
|
||||
if id != "" {
|
||||
db.Exec("UPDATE eintraege SET bezahlt = 0 WHERE id = ?", id)
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.LastComputed = time.Time{} // auf null zurücksetzen
|
||||
cacheMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Method", "/", r.Method)
|
||||
if r.Method == http.MethodPost {
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
@@ -343,15 +458,47 @@ func main() {
|
||||
wareStr := strings.Join(ware, ", ")
|
||||
zeitaufwand, _ := strconv.ParseFloat(r.FormValue("zeitaufwand"), 64)
|
||||
|
||||
e := Entry{
|
||||
Anfangsbestand: anfang,
|
||||
Endbestand: ende,
|
||||
Gesamtwert: diff,
|
||||
Prozentwert: prozent,
|
||||
Abgabe: abgabe,
|
||||
Startort: startort,
|
||||
Zielort: zielort,
|
||||
Schiff: schiff,
|
||||
Ware: wareStr,
|
||||
Zeitaufwand: zeitaufwand,
|
||||
}
|
||||
|
||||
go sendDiscordWebhook(e)
|
||||
|
||||
_, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at, startort, zielort, schiff, ware, zeitaufwand) VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?)`, anfang, ende, prozent, abgabe, startort, zielort, schiff, wareStr, zeitaufwand)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.LastComputed = time.Time{} // auf null zurücksetzen
|
||||
cacheMutex.Unlock()
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
cacheMutex.RLock()
|
||||
validCache := time.Since(cache.LastComputed) < 6*time.Hour
|
||||
cachedData := cache.Data
|
||||
cacheMutex.RUnlock()
|
||||
|
||||
fmt.Println("validCache:", validCache)
|
||||
if validCache {
|
||||
cachedData.LoggedIn = isAuthenticated(r)
|
||||
tmpl.Execute(w, cachedData)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt, created_at, startort, zielort, schiff, ware, zeitaufwand FROM eintraege`)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
|
||||
@@ -437,11 +584,14 @@ func main() {
|
||||
if _, ok := monatsMap[monatKey]; !ok {
|
||||
monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey}
|
||||
}
|
||||
monatsMap[monatKey].Eintraege = append(monatsMap[monatKey].Eintraege, e)
|
||||
if e.Bezahlt {
|
||||
monatsMap[monatKey].Summe += e.Abgabe
|
||||
} else {
|
||||
monatsMap[monatKey].SummeOffen += e.Abgabe
|
||||
}
|
||||
monatsMap[monatKey].Prozent = monatsMap[monatKey].Summe / (monatsMap[monatKey].Summe + monatsMap[monatKey].SummeOffen) * 100
|
||||
monatsMap[monatKey].ProzentOffen = monatsMap[monatKey].SummeOffen / (monatsMap[monatKey].Summe + monatsMap[monatKey].SummeOffen) * 100
|
||||
}
|
||||
|
||||
var monatsStat []Monatsstatistik
|
||||
@@ -458,7 +608,7 @@ func main() {
|
||||
//test
|
||||
// Dynamische Abteilungen – frei anpassbar
|
||||
abteilungen := []Abteilung{
|
||||
{Name: "Raumkampf", Anteil: 15, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Raumkampf (+8)", Anteil: 23, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Bodenkampf", Anteil: 8, Beispiel: "P4-AR Rifle", WertItem: 5900},
|
||||
{Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300},
|
||||
{Name: "Medical", Anteil: 5, Beispiel: "ParaMed Medical Device", WertItem: 1250},
|
||||
@@ -471,7 +621,7 @@ func main() {
|
||||
{Name: "Basebuilding (+10)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Crafting (+8)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Forschung (+5)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Events (-23)", Anteil: 38, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Events (-15)", Anteil: 30, Beispiel: "CF-337 Panther", WertItem: 36308},
|
||||
{Name: "Roleplay", Anteil: 3, Beispiel: "Clothing", WertItem: 8400},
|
||||
{Name: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000},
|
||||
}
|
||||
@@ -481,7 +631,28 @@ func main() {
|
||||
abteilungen[i].WertOffen = (abteilungen[i].Anteil / 100) * offeneSumme
|
||||
}
|
||||
|
||||
tmpl.Execute(w, struct {
|
||||
computed := TemplateData{
|
||||
Entries: eintraege,
|
||||
Summe: summe,
|
||||
OffeneSumme: offeneSumme,
|
||||
Abteilungen: abteilungen,
|
||||
Monatsstatistik: monatsStat,
|
||||
Member: membername,
|
||||
HasImpressum: hasimpressum,
|
||||
Impressum: impressum,
|
||||
Orte: orte,
|
||||
Schiffe: schiffe,
|
||||
Waren: waren,
|
||||
}
|
||||
|
||||
cacheMutex.Lock()
|
||||
cache.Data = computed
|
||||
cache.LastComputed = time.Now()
|
||||
cacheMutex.Unlock()
|
||||
computed.LoggedIn = isAuthenticated(r)
|
||||
tmpl.Execute(w, computed)
|
||||
|
||||
/*tmpl.Execute(w, struct {
|
||||
Entries []Entry
|
||||
Summe float64
|
||||
OffeneSumme float64
|
||||
@@ -507,7 +678,7 @@ func main() {
|
||||
Orte: orte,
|
||||
Schiffe: schiffe,
|
||||
Waren: waren,
|
||||
})
|
||||
})*/
|
||||
|
||||
})
|
||||
|
||||
@@ -515,6 +686,63 @@ func main() {
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
Entries []Entry
|
||||
Summe float64
|
||||
OffeneSumme float64
|
||||
Abteilungen []Abteilung
|
||||
Monatsstatistik []Monatsstatistik
|
||||
LoggedIn bool
|
||||
Member string
|
||||
HasImpressum bool
|
||||
Impressum string
|
||||
Orte []string
|
||||
Schiffe []string
|
||||
Waren []string
|
||||
}
|
||||
|
||||
type CachedData struct {
|
||||
Data TemplateData // das Struct, das du an tmpl.Execute übergibst
|
||||
LastComputed time.Time
|
||||
}
|
||||
|
||||
var cache CachedData
|
||||
var cacheMutex sync.RWMutex
|
||||
|
||||
var discordWebhook = GetENV("DISCORD_WEBHOOK_URL", "")
|
||||
|
||||
func sendDiscordWebhook(entry Entry) {
|
||||
if discordWebhook == "" {
|
||||
return
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(
|
||||
"📦 **Neuer Abgabe-Eintrag**\n"+
|
||||
"**UEC:** %s → %s (%s UEC Profit)\n"+
|
||||
"**Abgabe:** %s UEC (%s%%)\n"+
|
||||
"**Route:** %s → %s mit %s\n"+
|
||||
"**Ware:** %s\n"+
|
||||
"**Dauer:** %.0f Minuten",
|
||||
formatNumber(entry.Anfangsbestand),
|
||||
formatNumber(entry.Endbestand),
|
||||
formatNumber(entry.Gesamtwert),
|
||||
formatNumber(entry.Abgabe),
|
||||
formatNumber(entry.Prozentwert),
|
||||
entry.Startort,
|
||||
entry.Zielort,
|
||||
entry.Schiff,
|
||||
entry.Ware,
|
||||
entry.Zeitaufwand,
|
||||
)
|
||||
|
||||
payload := map[string]string{
|
||||
"content": message,
|
||||
}
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
http.Post(discordWebhook, "application/json", strings.NewReader(string(jsonData)))
|
||||
}
|
||||
|
||||
func createTable(db *sql.DB) {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS eintraege (
|
||||
@@ -628,7 +856,7 @@ const htmlTemplate = `
|
||||
<div class="col">
|
||||
<label class="form-label">Prozentwert</label>
|
||||
<select name="prozentwert" class="form-select">
|
||||
<option value="30">30%</option>
|
||||
<option value="30">30% (Standard)</option>
|
||||
<option value="10">10%</option>
|
||||
<option value="15">15%</option>
|
||||
<option value="20">20%</option>
|
||||
@@ -684,10 +912,68 @@ const htmlTemplate = `
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
|
||||
</form>
|
||||
<hr />
|
||||
{{end}}
|
||||
|
||||
<h2 class="mb-3">Auswertungen</h2>
|
||||
|
||||
<h2 class="mb-3">Gespeicherte Einträge</h2>
|
||||
<ul class="nav nav-tabs" id="auswertungTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="monat-tab" data-bs-toggle="tab" data-bs-target="#monat" type="button" role="tab" aria-controls="monat" aria-selected="true">
|
||||
Monatliche Übersicht
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="abteilung-tab" data-bs-toggle="tab" data-bs-target="#abteilung" type="button" role="tab" aria-controls="abteilung" aria-selected="false">
|
||||
Verteilung auf Abteilungen
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab" aria-controls="items" aria-selected="false">
|
||||
Gegenwert in Items
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content border border-top-0 p-4 bg-white" id="auswertungTabsContent">
|
||||
|
||||
<!-- Monatliche Übersicht -->
|
||||
<div class="tab-pane fade show active" id="monat" role="tabpanel" aria-labelledby="monat-tab">
|
||||
<h5 class="mb-3">Monatliche Übersicht</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monat</th>
|
||||
<th>Abgaben verteilt</th>
|
||||
<th>Abgaben offen</th>
|
||||
<th>Statistik</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Monatsstatistik}}
|
||||
<tr>
|
||||
<td>{{.Monat}}</td>
|
||||
<td>{{formatNumber .Summe}} UEC</td>
|
||||
<td>{{formatNumber .SummeOffen}} UEC</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped bg-success" role="progressbar" style="width: {{.Prozent}}%" aria-valuenow="{{.Prozent}}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<div class="progress-bar progress-bar-striped bg-danger" role="progressbar" style="width: {{.ProzentOffen}}%" aria-valuenow="{{.ProzentOffen}}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#monat-{{.Monat}}" aria-expanded="false" aria-controls="monat-{{.Monat}}">
|
||||
Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="9" class="p-0 border-0">
|
||||
<div class="collapse" id="monat-{{.Monat}}">
|
||||
<div class="bg-light p-3 border-top">
|
||||
<strong>Interne Infos (Details):</strong>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -699,11 +985,11 @@ const htmlTemplate = `
|
||||
<th>Prozent</th>
|
||||
<th>UEC Abgabe</th>
|
||||
<th>Status</th>
|
||||
{{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
|
||||
{{if $.LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="eintragsTabelle">
|
||||
{{range .Entries}}
|
||||
{{range .Eintraege}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{formatDate .CreatedAt}}</td>
|
||||
@@ -715,15 +1001,15 @@ const htmlTemplate = `
|
||||
<td>
|
||||
{{if .Bezahlt}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als unverteilt markieren</a>
|
||||
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-danger">✗ stornieren</a>
|
||||
{{else}}
|
||||
<span class="badge bg-success">✓ verteilt</span>
|
||||
<span class="badge bg-success">✓ Erledigt</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als verteilt markieren</a>
|
||||
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">✓ abgeben</a>
|
||||
{{else}}
|
||||
<span class="badge bg-danger">✗ nicht verteilt</span>
|
||||
<span class="badge bg-danger">✗ Offen</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
@@ -768,48 +1054,9 @@ const htmlTemplate = `
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2 class="mb-3">Auswertungen</h2>
|
||||
|
||||
<ul class="nav nav-tabs" id="auswertungTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="monat-tab" data-bs-toggle="tab" data-bs-target="#monat" type="button" role="tab" aria-controls="monat" aria-selected="true">
|
||||
Monatliche Übersicht
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="abteilung-tab" data-bs-toggle="tab" data-bs-target="#abteilung" type="button" role="tab" aria-controls="abteilung" aria-selected="false">
|
||||
Verteilung auf Abteilungen
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab" aria-controls="items" aria-selected="false">
|
||||
Gegenwert in Items
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content border border-top-0 p-4 bg-white" id="auswertungTabsContent">
|
||||
|
||||
<!-- Monatliche Übersicht -->
|
||||
<div class="tab-pane fade show active" id="monat" role="tabpanel" aria-labelledby="monat-tab">
|
||||
<h5 class="mb-3">Monatliche Übersicht</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monat</th>
|
||||
<th>Abgaben verteilt</th>
|
||||
<th>Abgaben offen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Monatsstatistik}}
|
||||
<tr>
|
||||
<td>{{.Monat}}</td>
|
||||
<td>{{formatNumber .Summe}} UEC</td>
|
||||
<td>{{formatNumber .SummeOffen}} UEC</td>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
@@ -892,6 +1139,11 @@ const htmlTemplate = `
|
||||
{{end}}
|
||||
<hr />
|
||||
</div>
|
||||
<style>
|
||||
.max {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/tom-select.complete.min.js"></script>
|
||||
|
||||
|
BIN
static/img/footerlower.webp
Normal file
BIN
static/img/footerlower.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
static/img/footerupper.webp
Normal file
BIN
static/img/footerupper.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
Reference in New Issue
Block a user